rubocop 1.52.1 → 1.53.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/config/default.yml +38 -1
- data/lib/rubocop/cli/command/lsp.rb +19 -0
- data/lib/rubocop/cli.rb +3 -0
- data/lib/rubocop/config_loader_resolver.rb +4 -3
- data/lib/rubocop/cop/bundler/gem_comment.rb +1 -1
- data/lib/rubocop/cop/bundler/gem_version.rb +2 -2
- data/lib/rubocop/cop/gemspec/dependency_version.rb +2 -2
- data/lib/rubocop/cop/internal_affairs/node_matcher_directive.rb +3 -3
- data/lib/rubocop/cop/layout/class_structure.rb +7 -0
- data/lib/rubocop/cop/layout/closing_heredoc_indentation.rb +1 -1
- data/lib/rubocop/cop/layout/empty_line_between_defs.rb +1 -1
- data/lib/rubocop/cop/layout/empty_lines_around_exception_handling_keywords.rb +2 -0
- data/lib/rubocop/cop/layout/indentation_style.rb +1 -1
- data/lib/rubocop/cop/layout/indentation_width.rb +2 -2
- data/lib/rubocop/cop/layout/redundant_line_break.rb +1 -1
- data/lib/rubocop/cop/layout/space_inside_range_literal.rb +1 -1
- data/lib/rubocop/cop/lint/debugger.rb +1 -1
- data/lib/rubocop/cop/lint/duplicate_hash_key.rb +2 -1
- data/lib/rubocop/cop/lint/duplicate_regexp_character_class_element.rb +46 -19
- data/lib/rubocop/cop/lint/heredoc_method_call_position.rb +1 -1
- data/lib/rubocop/cop/lint/missing_super.rb +31 -5
- data/lib/rubocop/cop/lint/mixed_case_range.rb +109 -0
- data/lib/rubocop/cop/lint/number_conversion.rb +5 -0
- data/lib/rubocop/cop/lint/redundant_regexp_quantifiers.rb +120 -0
- data/lib/rubocop/cop/lint/redundant_require_statement.rb +8 -3
- data/lib/rubocop/cop/lint/suppressed_exception.rb +1 -1
- data/lib/rubocop/cop/lint/symbol_conversion.rb +1 -1
- data/lib/rubocop/cop/lint/void.rb +1 -1
- data/lib/rubocop/cop/migration/department_name.rb +2 -2
- data/lib/rubocop/cop/mixin/comments_help.rb +1 -1
- data/lib/rubocop/cop/mixin/end_keyword_alignment.rb +1 -1
- data/lib/rubocop/cop/mixin/percent_literal.rb +1 -1
- data/lib/rubocop/cop/naming/block_forwarding.rb +1 -1
- data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +3 -3
- data/lib/rubocop/cop/style/block_comments.rb +1 -1
- data/lib/rubocop/cop/style/block_delimiters.rb +3 -3
- data/lib/rubocop/cop/style/conditional_assignment.rb +3 -1
- data/lib/rubocop/cop/style/document_dynamic_eval_definition.rb +1 -1
- data/lib/rubocop/cop/style/identical_conditional_branches.rb +6 -2
- data/lib/rubocop/cop/style/invertible_unless_condition.rb +1 -1
- data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +2 -2
- data/lib/rubocop/cop/style/preferred_hash_methods.rb +1 -1
- data/lib/rubocop/cop/style/redundant_begin.rb +1 -1
- data/lib/rubocop/cop/style/redundant_conditional.rb +1 -1
- data/lib/rubocop/cop/style/redundant_current_directory_in_path.rb +38 -0
- data/lib/rubocop/cop/style/redundant_line_continuation.rb +2 -2
- data/lib/rubocop/cop/style/redundant_parentheses.rb +1 -1
- data/lib/rubocop/cop/style/redundant_regexp_argument.rb +97 -0
- data/lib/rubocop/cop/style/redundant_regexp_escape.rb +2 -1
- data/lib/rubocop/cop/style/redundant_self_assignment_branch.rb +3 -1
- data/lib/rubocop/cop/style/redundant_sort.rb +1 -1
- data/lib/rubocop/cop/style/redundant_string_escape.rb +2 -0
- data/lib/rubocop/cop/style/return_nil_in_predicate_method_definition.rb +81 -0
- data/lib/rubocop/cop/style/signal_exception.rb +1 -1
- data/lib/rubocop/cop/style/yaml_file_read.rb +66 -0
- data/lib/rubocop/cop/util.rb +1 -1
- data/lib/rubocop/cop/utils/regexp_ranges.rb +100 -0
- data/lib/rubocop/cops_documentation_generator.rb +1 -1
- data/lib/rubocop/ext/regexp_parser.rb +4 -1
- data/lib/rubocop/lsp/logger.rb +22 -0
- data/lib/rubocop/lsp/routes.rb +223 -0
- data/lib/rubocop/lsp/runtime.rb +79 -0
- data/lib/rubocop/lsp/server.rb +62 -0
- data/lib/rubocop/lsp/severity.rb +27 -0
- data/lib/rubocop/options.rb +11 -1
- data/lib/rubocop/version.rb +1 -1
- data/lib/rubocop.rb +8 -0
- metadata +30 -3
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Style
|
6
|
+
# Checks if `return` or `return nil` is used in predicate method definitions.
|
7
|
+
#
|
8
|
+
# @safety
|
9
|
+
# Autocorrection is marked as unsafe because the change of the return value
|
10
|
+
# from `nil` to `false` could potentially lead to incompatibility issues.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# # bad
|
14
|
+
# def foo?
|
15
|
+
# return if condition
|
16
|
+
#
|
17
|
+
# do_something?
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# # bad
|
21
|
+
# def foo?
|
22
|
+
# return nil if condition
|
23
|
+
#
|
24
|
+
# do_something?
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# # good
|
28
|
+
# def foo?
|
29
|
+
# return false if condition
|
30
|
+
#
|
31
|
+
# do_something?
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# @example AllowedMethod: ['foo?']
|
35
|
+
# # good
|
36
|
+
# def foo?
|
37
|
+
# return if condition
|
38
|
+
#
|
39
|
+
# do_something?
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# @example AllowedPattern: [/foo/]
|
43
|
+
# # good
|
44
|
+
# def foo?
|
45
|
+
# return if condition
|
46
|
+
#
|
47
|
+
# do_something?
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
class ReturnNilInPredicateMethodDefinition < Base
|
51
|
+
extend AutoCorrector
|
52
|
+
include AllowedMethods
|
53
|
+
include AllowedPattern
|
54
|
+
|
55
|
+
MSG = 'Use `return false` instead of `%<prefer>s` in the predicate method.'
|
56
|
+
|
57
|
+
# @!method return_nil?(node)
|
58
|
+
def_node_matcher :return_nil?, <<~PATTERN
|
59
|
+
{(return) (return (nil))}
|
60
|
+
PATTERN
|
61
|
+
|
62
|
+
def on_def(node)
|
63
|
+
return unless node.predicate_method?
|
64
|
+
return if allowed_method?(node.method_name) || matches_allowed_pattern?(node.method_name)
|
65
|
+
return unless (body = node.body)
|
66
|
+
|
67
|
+
body.each_descendant(:return) do |return_node|
|
68
|
+
next unless return_nil?(return_node)
|
69
|
+
|
70
|
+
message = format(MSG, prefer: return_node.source)
|
71
|
+
|
72
|
+
add_offense(return_node, message: message) do |corrector|
|
73
|
+
corrector.replace(return_node, 'return false')
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
alias on_defs on_def
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Style
|
6
|
+
# Checks for the use of `YAML.load`, `YAML.safe_load`, and `YAML.parse` with
|
7
|
+
# `File.read` argument.
|
8
|
+
#
|
9
|
+
# NOTE: `YAML.safe_load_file` was introduced in Ruby 3.0.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
#
|
13
|
+
# # bad
|
14
|
+
# YAML.load(File.read(path))
|
15
|
+
# YAML.parse(File.read(path))
|
16
|
+
#
|
17
|
+
# # good
|
18
|
+
# YAML.load_file(path)
|
19
|
+
# YAML.parse_file(path)
|
20
|
+
#
|
21
|
+
# # bad
|
22
|
+
# YAML.safe_load(File.read(path)) # Ruby 3.0 and newer
|
23
|
+
#
|
24
|
+
# # good
|
25
|
+
# YAML.safe_load_file(path) # Ruby 3.0 and newer
|
26
|
+
#
|
27
|
+
class YAMLFileRead < Base
|
28
|
+
extend AutoCorrector
|
29
|
+
|
30
|
+
MSG = 'Use `%<prefer>s` instead.'
|
31
|
+
RESTRICT_ON_SEND = %i[load safe_load parse].freeze
|
32
|
+
|
33
|
+
# @!method yaml_file_read?(node)
|
34
|
+
def_node_matcher :yaml_file_read?, <<~PATTERN
|
35
|
+
(send
|
36
|
+
(const {cbase nil?} :YAML) _
|
37
|
+
(send
|
38
|
+
(const {cbase nil?} :File) :read $_) $...)
|
39
|
+
PATTERN
|
40
|
+
|
41
|
+
def on_send(node)
|
42
|
+
return if node.method?(:safe_load) && target_ruby_version <= 2.7
|
43
|
+
return unless (file_path, rest_arguments = yaml_file_read?(node))
|
44
|
+
|
45
|
+
range = offense_range(node)
|
46
|
+
rest_arguments = if rest_arguments.empty?
|
47
|
+
''
|
48
|
+
else
|
49
|
+
", #{rest_arguments.map(&:source).join(', ')}"
|
50
|
+
end
|
51
|
+
prefer = "#{node.method_name}_file(#{file_path.source}#{rest_arguments})"
|
52
|
+
|
53
|
+
add_offense(range, message: format(MSG, prefer: prefer)) do |corrector|
|
54
|
+
corrector.replace(range, prefer)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def offense_range(node)
|
61
|
+
node.loc.selector.join(node.source_range.end)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/rubocop/cop/util.rb
CHANGED
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Utils
|
6
|
+
# Helper to abstract complexity of building range pairs
|
7
|
+
# with octal escape reconstruction (needed for regexp_parser < 2.7).
|
8
|
+
class RegexpRanges
|
9
|
+
attr_reader :compound_token, :root
|
10
|
+
|
11
|
+
def initialize(root)
|
12
|
+
@root = root
|
13
|
+
@compound_token = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def pairs
|
17
|
+
unless @pairs
|
18
|
+
@pairs = []
|
19
|
+
populate(root)
|
20
|
+
end
|
21
|
+
|
22
|
+
# If either bound is a compound the first one is an escape
|
23
|
+
# and that's all we need to work with.
|
24
|
+
# If there are any cops that wanted to operate on the compound
|
25
|
+
# expression we could wrap it with a facade class.
|
26
|
+
@pairs.map { |pair| pair.map(&:first) }
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def populate(expr)
|
32
|
+
expressions = expr.expressions.to_a
|
33
|
+
|
34
|
+
until expressions.empty?
|
35
|
+
current = expressions.shift
|
36
|
+
|
37
|
+
if escaped_octal?(current)
|
38
|
+
compound_token << current
|
39
|
+
compound_token.concat(pop_octal_digits(expressions))
|
40
|
+
# If we have all the digits we can discard.
|
41
|
+
end
|
42
|
+
|
43
|
+
next unless current.type == :set
|
44
|
+
|
45
|
+
process_set(expressions, current)
|
46
|
+
compound_token.clear
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def process_set(expressions, current)
|
51
|
+
case current.token
|
52
|
+
when :range
|
53
|
+
@pairs << compose_range(expressions, current)
|
54
|
+
when :character
|
55
|
+
# Child expressions may include the range we are looking for.
|
56
|
+
populate(current)
|
57
|
+
when :intersection
|
58
|
+
# Each child expression could have child expressions that lead to ranges.
|
59
|
+
current.expressions.each do |intersected|
|
60
|
+
populate(intersected)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def compose_range(expressions, current)
|
66
|
+
range_start, range_end = current.expressions
|
67
|
+
range_start = if compound_token.size.between?(1, 2) && octal_digit?(range_start.text)
|
68
|
+
compound_token.dup << range_start
|
69
|
+
else
|
70
|
+
[range_start]
|
71
|
+
end
|
72
|
+
range_end = [range_end]
|
73
|
+
range_end.concat(pop_octal_digits(expressions)) if escaped_octal?(range_end.first)
|
74
|
+
[range_start, range_end]
|
75
|
+
end
|
76
|
+
|
77
|
+
def escaped_octal?(expr)
|
78
|
+
expr.text =~ /^\\[0-7]$/
|
79
|
+
end
|
80
|
+
|
81
|
+
def octal_digit?(char)
|
82
|
+
('0'..'7').cover?(char)
|
83
|
+
end
|
84
|
+
|
85
|
+
def pop_octal_digits(expressions)
|
86
|
+
digits = []
|
87
|
+
|
88
|
+
2.times do
|
89
|
+
next unless (next_child = expressions.first)
|
90
|
+
next unless next_child.type == :literal && next_child.text =~ /^[0-7]$/
|
91
|
+
|
92
|
+
digits << expressions.shift
|
93
|
+
end
|
94
|
+
|
95
|
+
digits
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -198,7 +198,7 @@ class CopsDocumentationGenerator # rubocop:disable Metrics/ClassLength
|
|
198
198
|
table = ['|===', "| #{header.join(' | ')}\n\n"].join("\n")
|
199
199
|
marked_contents = content.map do |plain_content|
|
200
200
|
# Escape `|` with backslash to prevent the regexp `|` is not used as a table separator.
|
201
|
-
plain_content.map { |c| "| #{c.gsub(
|
201
|
+
plain_content.map { |c| "| #{c.gsub('|', '\|')}" }.join("\n")
|
202
202
|
end
|
203
203
|
table << marked_contents.join("\n\n")
|
204
204
|
table << "\n|===\n"
|
@@ -68,7 +68,9 @@ module RuboCop
|
|
68
68
|
return { body: expression } unless (q = quantifier)
|
69
69
|
|
70
70
|
body = expression.adjust(end_pos: -q.text.length)
|
71
|
-
|
71
|
+
q.origin = origin
|
72
|
+
q.source = source if q.respond_to?(:source=) # for regexp_parser 1.8
|
73
|
+
q_loc = q.expression
|
72
74
|
{ body: body, quantifier: q_loc }
|
73
75
|
end
|
74
76
|
end
|
@@ -86,6 +88,7 @@ module RuboCop
|
|
86
88
|
end
|
87
89
|
end
|
88
90
|
::Regexp::Expression::Base.include Expression::Base
|
91
|
+
::Regexp::Expression::Quantifier.include Expression::Base
|
89
92
|
::Regexp::Expression::CharacterSet.include Expression::CharacterSet
|
90
93
|
end
|
91
94
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# This code is based on https://github.com/standardrb/standard.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2023 Test Double, Inc.
|
7
|
+
#
|
8
|
+
# The MIT License (MIT)
|
9
|
+
#
|
10
|
+
# https://github.com/standardrb/standard/blob/main/LICENSE.txt
|
11
|
+
#
|
12
|
+
module RuboCop
|
13
|
+
module Lsp
|
14
|
+
# Log for Language Server Protocol of RuboCop.
|
15
|
+
# @api private
|
16
|
+
class Logger
|
17
|
+
def self.log(message)
|
18
|
+
warn("[server] #{message}")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,223 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'severity'
|
4
|
+
|
5
|
+
#
|
6
|
+
# This code is based on https://github.com/standardrb/standard.
|
7
|
+
#
|
8
|
+
# Copyright (c) 2023 Test Double, Inc.
|
9
|
+
#
|
10
|
+
# The MIT License (MIT)
|
11
|
+
#
|
12
|
+
# https://github.com/standardrb/standard/blob/main/LICENSE.txt
|
13
|
+
#
|
14
|
+
module RuboCop
|
15
|
+
module Lsp
|
16
|
+
# Routes for Language Server Protocol of RuboCop.
|
17
|
+
# @api private
|
18
|
+
class Routes
|
19
|
+
def self.handle(name, &block)
|
20
|
+
define_method("handle_#{name}", &block)
|
21
|
+
end
|
22
|
+
|
23
|
+
private_class_method :handle
|
24
|
+
|
25
|
+
def initialize(server)
|
26
|
+
@server = server
|
27
|
+
|
28
|
+
@text_cache = {}
|
29
|
+
end
|
30
|
+
|
31
|
+
def for(name)
|
32
|
+
name = "handle_#{name}"
|
33
|
+
return unless respond_to?(name)
|
34
|
+
|
35
|
+
method(name)
|
36
|
+
end
|
37
|
+
|
38
|
+
handle 'initialize' do |request|
|
39
|
+
@server.write(
|
40
|
+
id: request[:id],
|
41
|
+
result: LanguageServer::Protocol::Interface::InitializeResult.new(
|
42
|
+
capabilities: LanguageServer::Protocol::Interface::ServerCapabilities.new(
|
43
|
+
document_formatting_provider: true,
|
44
|
+
diagnostic_provider: true,
|
45
|
+
text_document_sync: LanguageServer::Protocol::Interface::TextDocumentSyncOptions.new(
|
46
|
+
change: LanguageServer::Protocol::Constant::TextDocumentSyncKind::FULL,
|
47
|
+
open_close: true
|
48
|
+
)
|
49
|
+
)
|
50
|
+
)
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
handle 'initialized' do |_request|
|
55
|
+
version = RuboCop::Version::STRING
|
56
|
+
|
57
|
+
Logger.log("RuboCop #{version} language server initialized, PID #{Process.pid}")
|
58
|
+
end
|
59
|
+
|
60
|
+
handle 'shutdown' do |request|
|
61
|
+
Logger.log('Client asked to shutdown RuboCop language server.')
|
62
|
+
@server.stop do
|
63
|
+
@server.write(id: request[:id], result: nil)
|
64
|
+
Logger.log('Exiting...')
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
handle 'textDocument/diagnostic' do |request|
|
69
|
+
doc = request[:params][:textDocument]
|
70
|
+
result = diagnostic(doc[:uri], doc[:text])
|
71
|
+
@server.write(result)
|
72
|
+
end
|
73
|
+
|
74
|
+
handle 'textDocument/didChange' do |request|
|
75
|
+
params = request[:params]
|
76
|
+
result = diagnostic(params[:textDocument][:uri], params[:contentChanges][0][:text])
|
77
|
+
@server.write(result)
|
78
|
+
end
|
79
|
+
|
80
|
+
handle 'textDocument/didOpen' do |request|
|
81
|
+
doc = request[:params][:textDocument]
|
82
|
+
result = diagnostic(doc[:uri], doc[:text])
|
83
|
+
@server.write(result)
|
84
|
+
end
|
85
|
+
|
86
|
+
handle 'textDocument/didClose' do |request|
|
87
|
+
@text_cache.delete(request.dig(:params, :textDocument, :uri))
|
88
|
+
end
|
89
|
+
|
90
|
+
handle 'textDocument/formatting' do |request|
|
91
|
+
uri = request[:params][:textDocument][:uri]
|
92
|
+
@server.write(id: request[:id], result: format_file(uri))
|
93
|
+
end
|
94
|
+
|
95
|
+
handle 'workspace/didChangeConfiguration' do |_request|
|
96
|
+
Logger.log('Ignoring workspace/didChangeConfiguration')
|
97
|
+
end
|
98
|
+
|
99
|
+
handle 'workspace/didChangeWatchedFiles' do |request|
|
100
|
+
changed = request[:params][:changes].any? do |change|
|
101
|
+
change[:uri].end_with?(RuboCop::ConfigFinder::DOTFILE)
|
102
|
+
end
|
103
|
+
|
104
|
+
if changed
|
105
|
+
Logger.log('Configuration file changed; restart required')
|
106
|
+
@server.stop
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
handle 'workspace/executeCommand' do |request|
|
111
|
+
if request[:params][:command] == 'rubocop.formatAutocorrects'
|
112
|
+
uri = request[:params][:arguments][0][:uri]
|
113
|
+
@server.write(
|
114
|
+
id: request[:id],
|
115
|
+
method: 'workspace/applyEdit',
|
116
|
+
params: {
|
117
|
+
label: 'Format with RuboCop autocorrects',
|
118
|
+
edit: {
|
119
|
+
changes: {
|
120
|
+
uri => format_file(uri)
|
121
|
+
}
|
122
|
+
}
|
123
|
+
}
|
124
|
+
)
|
125
|
+
else
|
126
|
+
handle_unsupported_method(request, request[:params][:command])
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
handle 'textDocument/willSave' do |_request|
|
131
|
+
# Nothing to do
|
132
|
+
end
|
133
|
+
|
134
|
+
handle 'textDocument/didSave' do |_request|
|
135
|
+
# Nothing to do
|
136
|
+
end
|
137
|
+
|
138
|
+
handle '$/cancelRequest' do |_request|
|
139
|
+
# Can't cancel anything because single-threaded
|
140
|
+
end
|
141
|
+
|
142
|
+
handle '$/setTrace' do |_request|
|
143
|
+
# No-op, we log everything
|
144
|
+
end
|
145
|
+
|
146
|
+
def handle_unsupported_method(request, method = request[:method])
|
147
|
+
@server.write(
|
148
|
+
id: request[:id],
|
149
|
+
error: LanguageServer::Protocol::Interface::ResponseError.new(
|
150
|
+
code: LanguageServer::Protocol::Constant::ErrorCodes::METHOD_NOT_FOUND,
|
151
|
+
message: "Unsupported Method: #{method}"
|
152
|
+
)
|
153
|
+
)
|
154
|
+
Logger.log("Unsupported Method: #{method}")
|
155
|
+
end
|
156
|
+
|
157
|
+
def handle_method_missing(request)
|
158
|
+
return unless request.key?(:id)
|
159
|
+
|
160
|
+
@server.write(id: request[:id], result: nil)
|
161
|
+
end
|
162
|
+
|
163
|
+
private
|
164
|
+
|
165
|
+
def format_file(file_uri)
|
166
|
+
unless (text = @text_cache[file_uri])
|
167
|
+
Logger.log("Format request arrived before text synchronized; skipping: `#{file_uri}'")
|
168
|
+
|
169
|
+
return []
|
170
|
+
end
|
171
|
+
|
172
|
+
new_text = @server.format(remove_file_protocol_from(file_uri), text)
|
173
|
+
|
174
|
+
return [] if new_text == text
|
175
|
+
|
176
|
+
[
|
177
|
+
newText: new_text,
|
178
|
+
range: {
|
179
|
+
start: { line: 0, character: 0 },
|
180
|
+
end: { line: text.count("\n") + 1, character: 0 }
|
181
|
+
}
|
182
|
+
]
|
183
|
+
end
|
184
|
+
|
185
|
+
def diagnostic(file_uri, text)
|
186
|
+
@text_cache[file_uri] = text
|
187
|
+
offenses = @server.offenses(remove_file_protocol_from(file_uri), text)
|
188
|
+
diagnostics = offenses.map { |offense| to_diagnostic(offense) }
|
189
|
+
|
190
|
+
{
|
191
|
+
method: 'textDocument/publishDiagnostics',
|
192
|
+
params: {
|
193
|
+
uri: file_uri,
|
194
|
+
diagnostics: diagnostics
|
195
|
+
}
|
196
|
+
}
|
197
|
+
end
|
198
|
+
|
199
|
+
def remove_file_protocol_from(uri)
|
200
|
+
uri.delete_prefix('file://')
|
201
|
+
end
|
202
|
+
|
203
|
+
def to_diagnostic(offense)
|
204
|
+
code = offense[:cop_name]
|
205
|
+
message = offense[:message]
|
206
|
+
loc = offense[:location]
|
207
|
+
rubocop_severity = offense[:severity]
|
208
|
+
severity = Severity.find_by(rubocop_severity)
|
209
|
+
|
210
|
+
{
|
211
|
+
code: code, message: message, range: to_range(loc), severity: severity, source: 'rubocop'
|
212
|
+
}
|
213
|
+
end
|
214
|
+
|
215
|
+
def to_range(location)
|
216
|
+
{
|
217
|
+
start: { character: location[:start_column] - 1, line: location[:start_line] - 1 },
|
218
|
+
end: { character: location[:last_column] - 1, line: location[:last_line] - 1 }
|
219
|
+
}
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# This code is based on https://github.com/standardrb/standard.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2023 Test Double, Inc.
|
7
|
+
#
|
8
|
+
# The MIT License (MIT)
|
9
|
+
#
|
10
|
+
# https://github.com/standardrb/standard/blob/main/LICENSE.txt
|
11
|
+
#
|
12
|
+
module RuboCop
|
13
|
+
module Lsp
|
14
|
+
# Runtime for Language Server Protocol of RuboCop.
|
15
|
+
# @api private
|
16
|
+
class Runtime
|
17
|
+
def initialize(config_store)
|
18
|
+
@config_store = config_store
|
19
|
+
@logged_paths = []
|
20
|
+
end
|
21
|
+
|
22
|
+
# This abuses the `--stdin` option of rubocop and reads the formatted text
|
23
|
+
# from the `options[:stdin]` that rubocop mutates. This depends on
|
24
|
+
# `parallel: false` as well as the fact that RuboCop doesn't otherwise dup
|
25
|
+
# or reassign that options object. Risky business!
|
26
|
+
#
|
27
|
+
# Reassigning `options[:stdin]` is done here:
|
28
|
+
# https://github.com/rubocop/rubocop/blob/v1.52.0/lib/rubocop/cop/team.rb#L131
|
29
|
+
# Printing `options[:stdin]`
|
30
|
+
# https://github.com/rubocop/rubocop/blob/v1.52.0/lib/rubocop/cli/command/execute_runner.rb#L95
|
31
|
+
# Setting `parallel: true` would break this here:
|
32
|
+
# https://github.com/rubocop/rubocop/blob/v1.52.0/lib/rubocop/runner.rb#L72
|
33
|
+
def format(path, text)
|
34
|
+
formatting_options = {
|
35
|
+
stdin: text, force_exclusion: true, autocorrect: true, safe_autocorrect: true
|
36
|
+
}
|
37
|
+
|
38
|
+
redirect_stdout { run_rubocop(formatting_options, path) }
|
39
|
+
|
40
|
+
formatting_options[:stdin]
|
41
|
+
end
|
42
|
+
|
43
|
+
def offenses(path, text)
|
44
|
+
diagnostic_options = {
|
45
|
+
stdin: text, force_exclusion: true, formatters: ['json'], format: 'json'
|
46
|
+
}
|
47
|
+
|
48
|
+
json = redirect_stdout { run_rubocop(diagnostic_options, path) }
|
49
|
+
results = JSON.parse(json, symbolize_names: true)
|
50
|
+
|
51
|
+
if results[:files].empty?
|
52
|
+
unless @logged_paths.include?(path)
|
53
|
+
Logger.log "Ignoring file, per configuration: #{path}"
|
54
|
+
@logged_paths << path
|
55
|
+
end
|
56
|
+
return []
|
57
|
+
end
|
58
|
+
|
59
|
+
results.dig(:files, 0, :offenses)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def redirect_stdout(&block)
|
65
|
+
stdout = StringIO.new
|
66
|
+
|
67
|
+
RuboCop::Server::Helper.redirect(stdout: stdout, &block)
|
68
|
+
|
69
|
+
stdout.string
|
70
|
+
end
|
71
|
+
|
72
|
+
def run_rubocop(options, path)
|
73
|
+
runner = RuboCop::Runner.new(options, @config_store)
|
74
|
+
|
75
|
+
runner.run([path])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'language_server-protocol'
|
4
|
+
require_relative 'logger'
|
5
|
+
require_relative 'routes'
|
6
|
+
require_relative 'runtime'
|
7
|
+
|
8
|
+
#
|
9
|
+
# This code is based on https://github.com/standardrb/standard.
|
10
|
+
#
|
11
|
+
# Copyright (c) 2023 Test Double, Inc.
|
12
|
+
#
|
13
|
+
# The MIT License (MIT)
|
14
|
+
#
|
15
|
+
# https://github.com/standardrb/standard/blob/main/LICENSE.txt
|
16
|
+
#
|
17
|
+
module RuboCop
|
18
|
+
module Lsp
|
19
|
+
# Language Server Protocol of RuboCop.
|
20
|
+
# @api private
|
21
|
+
class Server
|
22
|
+
def initialize(config_store)
|
23
|
+
@reader = LanguageServer::Protocol::Transport::Io::Reader.new($stdin)
|
24
|
+
@writer = LanguageServer::Protocol::Transport::Io::Writer.new($stdout)
|
25
|
+
@runtime = RuboCop::Lsp::Runtime.new(config_store)
|
26
|
+
@routes = Routes.new(self)
|
27
|
+
end
|
28
|
+
|
29
|
+
def start
|
30
|
+
@reader.read do |request|
|
31
|
+
if !request.key?(:method)
|
32
|
+
@routes.handle_method_missing(request)
|
33
|
+
elsif (route = @routes.for(request[:method]))
|
34
|
+
route.call(request)
|
35
|
+
else
|
36
|
+
@routes.handle_unsupported_method(request)
|
37
|
+
end
|
38
|
+
rescue StandardError => e
|
39
|
+
log("Error #{e.class} #{e.message[0..100]}")
|
40
|
+
log(e.backtrace.inspect)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def write(response)
|
45
|
+
@writer.write(response)
|
46
|
+
end
|
47
|
+
|
48
|
+
def format(path, text)
|
49
|
+
@runtime.format(path, text)
|
50
|
+
end
|
51
|
+
|
52
|
+
def offenses(path, text)
|
53
|
+
@runtime.offenses(path, text)
|
54
|
+
end
|
55
|
+
|
56
|
+
def stop(&block)
|
57
|
+
at_exit(&block) if block
|
58
|
+
exit
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|