rubocop 1.52.0 → 1.53.0
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/cop_description.rb +32 -8
- 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 -2
- 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/ambiguous_block_association.rb +2 -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/erb_new_arguments.rb +1 -2
- data/lib/rubocop/cop/lint/heredoc_method_call_position.rb +1 -1
- data/lib/rubocop/cop/lint/identity_comparison.rb +0 -1
- data/lib/rubocop/cop/lint/incompatible_io_select_with_fiber_scheduler.rb +1 -2
- data/lib/rubocop/cop/lint/inherit_exception.rb +3 -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/ordered_magic_comments.rb +0 -1
- 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/redundant_safe_navigation.rb +2 -2
- data/lib/rubocop/cop/lint/send_with_mixin_argument.rb +1 -2
- data/lib/rubocop/cop/lint/shadowed_exception.rb +5 -11
- data/lib/rubocop/cop/lint/suppressed_exception.rb +1 -1
- data/lib/rubocop/cop/lint/useless_assignment.rb +2 -1
- data/lib/rubocop/cop/lint/void.rb +1 -1
- data/lib/rubocop/cop/metrics/utils/abc_size_calculator.rb +1 -2
- 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/begin_block.rb +1 -2
- 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/class_equality_comparison.rb +17 -39
- data/lib/rubocop/cop/style/conditional_assignment.rb +3 -1
- data/lib/rubocop/cop/style/dir.rb +1 -1
- data/lib/rubocop/cop/style/dir_empty.rb +8 -14
- data/lib/rubocop/cop/style/document_dynamic_eval_definition.rb +1 -1
- data/lib/rubocop/cop/style/eval_with_location.rb +3 -3
- data/lib/rubocop/cop/style/file_read.rb +2 -2
- data/lib/rubocop/cop/style/hash_transform_keys.rb +2 -2
- data/lib/rubocop/cop/style/hash_transform_values.rb +2 -2
- 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 +86 -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/sole_nested_conditional.rb +3 -1
- data/lib/rubocop/cop/style/special_global_vars.rb +1 -2
- 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/cop/variable_force/assignment.rb +14 -1
- 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 +36 -9
@@ -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
|
@@ -69,6 +69,12 @@ module RuboCop
|
|
69
69
|
meta_assignment_node.type == REST_ASSIGNMENT_TYPE
|
70
70
|
end
|
71
71
|
|
72
|
+
def for_assignment?
|
73
|
+
return false unless meta_assignment_node
|
74
|
+
|
75
|
+
meta_assignment_node.for_type?
|
76
|
+
end
|
77
|
+
|
72
78
|
def operator
|
73
79
|
assignment_node = meta_assignment_node || @node
|
74
80
|
assignment_node.loc.operator.source
|
@@ -78,7 +84,8 @@ module RuboCop
|
|
78
84
|
unless instance_variable_defined?(:@meta_assignment_node)
|
79
85
|
@meta_assignment_node = operator_assignment_node ||
|
80
86
|
multiple_assignment_node ||
|
81
|
-
rest_assignment_node
|
87
|
+
rest_assignment_node ||
|
88
|
+
for_assignment_node
|
82
89
|
end
|
83
90
|
|
84
91
|
@meta_assignment_node
|
@@ -109,6 +116,12 @@ module RuboCop
|
|
109
116
|
|
110
117
|
node.parent
|
111
118
|
end
|
119
|
+
|
120
|
+
def for_assignment_node
|
121
|
+
return nil unless node.parent&.for_type?
|
122
|
+
|
123
|
+
node.parent
|
124
|
+
end
|
112
125
|
end
|
113
126
|
end
|
114
127
|
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
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Lsp
|
5
|
+
# Severity for Language Server Protocol of RuboCop.
|
6
|
+
# @api private
|
7
|
+
class Severity
|
8
|
+
SEVERITIES = {
|
9
|
+
fatal: LanguageServer::Protocol::Constant::DiagnosticSeverity::ERROR,
|
10
|
+
error: LanguageServer::Protocol::Constant::DiagnosticSeverity::ERROR,
|
11
|
+
warning: LanguageServer::Protocol::Constant::DiagnosticSeverity::WARNING,
|
12
|
+
convention: LanguageServer::Protocol::Constant::DiagnosticSeverity::INFORMATION,
|
13
|
+
refactor: LanguageServer::Protocol::Constant::DiagnosticSeverity::HINT,
|
14
|
+
info: LanguageServer::Protocol::Constant::DiagnosticSeverity::HINT
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
def self.find_by(rubocop_severity)
|
18
|
+
if (severity = SEVERITIES[rubocop_severity.to_sym])
|
19
|
+
return severity
|
20
|
+
end
|
21
|
+
|
22
|
+
Logger.log("Unknown severity: #{rubocop_severity}")
|
23
|
+
LanguageServer::Protocol::Constant::DiagnosticSeverity::HINT
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/rubocop/options.rb
CHANGED
@@ -16,7 +16,7 @@ module RuboCop
|
|
16
16
|
'root of the project. RuboCop will use this path to determine which ' \
|
17
17
|
'cops are enabled (via eg. Include/Exclude), and so that certain cops ' \
|
18
18
|
'like Naming/FileName can be checked.'
|
19
|
-
EXITING_OPTIONS = %i[version verbose_version show_cops show_docs_url].freeze
|
19
|
+
EXITING_OPTIONS = %i[version verbose_version show_cops show_docs_url lsp].freeze
|
20
20
|
DEFAULT_MAXIMUM_EXCLUSION_ITEMS = 15
|
21
21
|
|
22
22
|
def initialize
|
@@ -49,12 +49,14 @@ module RuboCop
|
|
49
49
|
|
50
50
|
private
|
51
51
|
|
52
|
+
# rubocop:disable Metrics/AbcSize
|
52
53
|
def define_options
|
53
54
|
OptionParser.new do |opts|
|
54
55
|
opts.banner = rainbow.wrap('Usage: rubocop [options] [file1, file2, ...]').bright
|
55
56
|
|
56
57
|
add_check_options(opts)
|
57
58
|
add_cache_options(opts)
|
59
|
+
add_lsp_option(opts)
|
58
60
|
add_server_options(opts)
|
59
61
|
add_output_options(opts)
|
60
62
|
add_autocorrection_options(opts)
|
@@ -66,6 +68,7 @@ module RuboCop
|
|
66
68
|
add_profile_options(opts) if RUBY_ENGINE == 'ruby' && !Platform.windows?
|
67
69
|
end
|
68
70
|
end
|
71
|
+
# rubocop:enable Metrics/AbcSize
|
69
72
|
|
70
73
|
def add_check_options(opts) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
71
74
|
section(opts, 'Basic Options') do # rubocop:disable Metrics/BlockLength
|
@@ -205,6 +208,12 @@ module RuboCop
|
|
205
208
|
end
|
206
209
|
end
|
207
210
|
|
211
|
+
def add_lsp_option(opts)
|
212
|
+
section(opts, 'LSP Option') do
|
213
|
+
option(opts, '--lsp')
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
208
217
|
def add_server_options(opts)
|
209
218
|
section(opts, 'Server Options') do
|
210
219
|
option(opts, '--[no-]server')
|
@@ -620,6 +629,7 @@ module RuboCop
|
|
620
629
|
stop_server: 'Stop server process.',
|
621
630
|
server_status: 'Show server status.',
|
622
631
|
no_detach: 'Run the server process in the foreground.',
|
632
|
+
lsp: 'Start a language server listening on STDIN.',
|
623
633
|
raise_cop_error: ['Raise cop-related errors with cause and location.',
|
624
634
|
'This is used to prevent cops from failing silently.',
|
625
635
|
'Default is false.'],
|
data/lib/rubocop/version.rb
CHANGED