standard 1.38.0 → 1.39.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f78e04ed9ba26c056a144f8584e28ade38906bf4f0e5078802654cd5d7b99082
4
- data.tar.gz: 7d6b129ca13e5cbb8dc9ffed5891edcd81975780873501443f125906846d8a3c
3
+ metadata.gz: b15a7085baa8421e82fec821f8562214b001cfe17bd6a02f7a061bc892f047fa
4
+ data.tar.gz: f67e28527faebd95dc452a57f8994523c11f2f3e5be6c28da458d374852c5f9e
5
5
  SHA512:
6
- metadata.gz: 8de4b19e062290cbc42f4b9d3f73cc2b1412b9dc683b6499a0d443b32f598e1a7d48e0f46f1e4343f18a6032ba6a3fb74a3b90a00257e4f3777ecbc73d5fd758
7
- data.tar.gz: 42bf30e5370ca89431b4c8546320fb3c840c1ce7c7256dae9f516ed88f71f8ad1a1b77ea644c417d3ecab3ce98481aecc29211965b6f1c18903d5eccaa9cfc78
6
+ metadata.gz: d6040219e08da54f2ccfff731e787228e9226b20ba0380530513a41b0f29946eecf1503e41cd93e8f6beca3259258a713ad2f6815099325074a11ec2fcaf8915
7
+ data.tar.gz: 6749ee4c00f5cad2c52b774e96048f82769046a9527dccc6515e0ee75ed274987d72f11d6422407c747ceed15c8b9c12a884c94573657af07811d7f013889135
data/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 1.39.0
6
+
7
+ * Add support for LSP Code Actions / Quick Fix under Ruby LSP [#636](https://github.com/standardrb/standard/pull/636)
8
+
5
9
  ## 1.38.0
6
10
 
7
11
  * Update minimum Ruby version to 3.0
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- standard (1.38.0)
4
+ standard (1.39.0)
5
5
  language_server-protocol (~> 3.17.0.2)
6
6
  lint_roller (~> 1.0)
7
7
  rubocop (~> 1.64.0)
@@ -4,20 +4,18 @@ require_relative "wraps_built_in_lsp_standardizer"
4
4
  module RubyLsp
5
5
  module Standard
6
6
  class Addon < ::RubyLsp::Addon
7
- def initializer
8
- @wraps_built_in_lsp_standardizer = nil
9
- end
10
-
11
7
  def name
12
8
  "Standard Ruby"
13
9
  end
14
10
 
15
11
  def activate(global_state, message_queue)
16
- warn "Activating Standard Ruby LSP addon v#{::Standard::VERSION}"
12
+ @logger = ::Standard::Lsp::Logger.new(prefix: "[Standard Ruby]")
13
+ @logger.puts "Activating Standard Ruby LSP addon v#{::Standard::VERSION}"
14
+ RuboCop::LSP.enable
17
15
  @wraps_built_in_lsp_standardizer = WrapsBuiltinLspStandardizer.new
18
16
  global_state.register_formatter("standard", @wraps_built_in_lsp_standardizer)
19
17
  register_additional_file_watchers(global_state, message_queue)
20
- warn "Initialized Standard Ruby LSP addon #{::Standard::VERSION}"
18
+ @logger.puts "Initialized Standard Ruby LSP addon #{::Standard::VERSION}"
21
19
  end
22
20
 
23
21
  def deactivate
@@ -52,7 +50,7 @@ module RubyLsp
52
50
  def workspace_did_change_watched_files(changes)
53
51
  if changes.any? { |change| change[:uri].end_with?(".standard.yml") }
54
52
  @wraps_built_in_lsp_standardizer.init!
55
- warn "Re-initialized Standard Ruby LSP addon #{::Standard::VERSION} due to .standard.yml file change"
53
+ @logger.puts "Re-initialized Standard Ruby LSP addon #{::Standard::VERSION} due to .standard.yml file change"
56
54
  end
57
55
  end
58
56
  end
@@ -7,13 +7,9 @@ module RubyLsp
7
7
  end
8
8
 
9
9
  def init!
10
- @config = ::Standard::BuildsConfig.new.call([])
11
10
  @standardizer = ::Standard::Lsp::Standardizer.new(
12
- @config,
13
- ::Standard::Lsp::Logger.new
11
+ ::Standard::BuildsConfig.new.call([])
14
12
  )
15
- @rubocop_config = @config.rubocop_config_store.for_pwd
16
- @cop_registry = RuboCop::Cop::Registry.global.to_h
17
13
  end
18
14
 
19
15
  def run_formatting(uri, document)
@@ -21,55 +17,7 @@ module RubyLsp
21
17
  end
22
18
 
23
19
  def run_diagnostic(uri, document)
24
- offenses = @standardizer.offenses(uri_to_path(uri), document.source)
25
-
26
- offenses.map { |o|
27
- cop_name = o[:cop_name]
28
-
29
- msg = o[:message].delete_prefix(cop_name)
30
- loc = o[:location]
31
-
32
- severity = case o[:severity]
33
- when "error", "fatal"
34
- RubyLsp::Constant::DiagnosticSeverity::ERROR
35
- when "warning"
36
- RubyLsp::Constant::DiagnosticSeverity::WARNING
37
- when "convention"
38
- RubyLsp::Constant::DiagnosticSeverity::INFORMATION
39
- when "refactor", "info"
40
- RubyLsp::Constant::DiagnosticSeverity::HINT
41
- else # the above cases fully cover what RuboCop sends at this time
42
- logger.puts "Unknown severity: #{severity.inspect}"
43
- RubyLsp::Constant::DiagnosticSeverity::HINT
44
- end
45
-
46
- RubyLsp::Interface::Diagnostic.new(
47
- code: cop_name,
48
- code_description: code_description(cop_name),
49
- message: msg,
50
- source: "Standard Ruby",
51
- severity: severity,
52
- range: RubyLsp::Interface::Range.new(
53
- start: RubyLsp::Interface::Position.new(line: loc[:start_line] - 1, character: loc[:start_column] - 1),
54
- end: RubyLsp::Interface::Position.new(line: loc[:last_line] - 1, character: loc[:last_column])
55
- )
56
- # TODO: We need to do something like to support quickfixes thru code actions
57
- # See: https://github.com/Shopify/ruby-lsp/blob/4c1906172add4d5c39c35d3396aa29c768bfb898/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb#L62
58
- # data: {
59
- # correctable: correctable?(offense),
60
- # code_actions: to_lsp_code_actions
61
- # }
62
- #
63
- # Right now, our offenses are all just JSON parsed from stdout shelling to RuboCop, so
64
- # it seems we don't have the corrector available to us.
65
- #
66
- # Lifted from:
67
- # https://github.com/Shopify/ruby-lsp/blob/8d4c17efce4e8ecc8e7c557ab2981db6b22c0b6d/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb#L201
68
- # def correctable?(offense)
69
- # !offense.corrector.nil?
70
- # end
71
- )
72
- }
20
+ @standardizer.offenses(uri_to_path(uri), document.source, document.encoding)
73
21
  end
74
22
 
75
23
  private
@@ -83,16 +31,6 @@ module RubyLsp
83
31
  uri.to_s.sub(%r{^file://}, "")
84
32
  end
85
33
  end
86
-
87
- # lifted from:
88
- # https://github.com/Shopify/ruby-lsp/blob/4c1906172add4d5c39c35d3396aa29c768bfb898/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb#L84
89
- def code_description(cop_name)
90
- if (cop_class = @cop_registry[cop_name]&.first)
91
- if (doc_url = cop_class.documentation_url(@rubocop_config))
92
- Interface::CodeDescription.new(href: doc_url)
93
- end
94
- end
95
- end
96
34
  end
97
35
  end
98
36
  end
@@ -0,0 +1,168 @@
1
+ module Standard
2
+ module Lsp
3
+ class Diagnostic
4
+ Constant = LanguageServer::Protocol::Constant
5
+ Interface = LanguageServer::Protocol::Interface
6
+
7
+ RUBOCOP_TO_LSP_SEVERITY = {
8
+ info: Constant::DiagnosticSeverity::HINT,
9
+ refactor: Constant::DiagnosticSeverity::INFORMATION,
10
+ convention: Constant::DiagnosticSeverity::INFORMATION,
11
+ warning: Constant::DiagnosticSeverity::WARNING,
12
+ error: Constant::DiagnosticSeverity::ERROR,
13
+ fatal: Constant::DiagnosticSeverity::ERROR
14
+ }.freeze
15
+
16
+ def initialize(document_encoding, offense, uri, cop_class)
17
+ @document_encoding = document_encoding
18
+ @offense = offense
19
+ @uri = uri
20
+ @cop_class = cop_class
21
+ end
22
+
23
+ def to_lsp_code_actions
24
+ code_actions = []
25
+
26
+ code_actions << autocorrect_action if correctable?
27
+ code_actions << disable_line_action
28
+
29
+ code_actions
30
+ end
31
+
32
+ def to_lsp_diagnostic(config)
33
+ highlighted = @offense.highlighted_area
34
+ Interface::Diagnostic.new(
35
+ message: message,
36
+ source: "Standard Ruby",
37
+ code: @offense.cop_name,
38
+ code_description: code_description(config),
39
+ severity: severity,
40
+ range: Interface::Range.new(
41
+ start: Interface::Position.new(
42
+ line: @offense.line - 1,
43
+ character: highlighted.begin_pos
44
+ ),
45
+ end: Interface::Position.new(
46
+ line: @offense.line - 1,
47
+ character: highlighted.end_pos
48
+ )
49
+ ),
50
+ data: {
51
+ correctable: correctable?,
52
+ code_actions: to_lsp_code_actions
53
+ }
54
+ )
55
+ end
56
+
57
+ private
58
+
59
+ def message
60
+ message = @offense.message
61
+ message += "\n\nThis offense is not auto-correctable.\n" unless correctable?
62
+ message
63
+ end
64
+
65
+ def severity
66
+ RUBOCOP_TO_LSP_SEVERITY[@offense.severity.name]
67
+ end
68
+
69
+ def code_description(config)
70
+ return unless @cop_class
71
+
72
+ if (doc_url = @cop_class.documentation_url(config))
73
+ Interface::CodeDescription.new(href: doc_url)
74
+ end
75
+ end
76
+
77
+ def autocorrect_action
78
+ Interface::CodeAction.new(
79
+ title: "Autocorrect #{@offense.cop_name}",
80
+ kind: Constant::CodeActionKind::QUICK_FIX,
81
+ edit: Interface::WorkspaceEdit.new(
82
+ document_changes: [
83
+ Interface::TextDocumentEdit.new(
84
+ text_document: Interface::OptionalVersionedTextDocumentIdentifier.new(
85
+ uri: @uri.to_s,
86
+ version: nil
87
+ ),
88
+ edits: correctable? ? offense_replacements : []
89
+ )
90
+ ]
91
+ ),
92
+ is_preferred: true
93
+ )
94
+ end
95
+
96
+ def offense_replacements
97
+ @offense.corrector.as_replacements.map do |range, replacement|
98
+ Interface::TextEdit.new(
99
+ range: Interface::Range.new(
100
+ start: Interface::Position.new(line: range.line - 1, character: range.column),
101
+ end: Interface::Position.new(line: range.last_line - 1, character: range.last_column)
102
+ ),
103
+ new_text: replacement
104
+ )
105
+ end
106
+ end
107
+
108
+ def disable_line_action
109
+ Interface::CodeAction.new(
110
+ title: "Disable #{@offense.cop_name} for this line",
111
+ kind: Constant::CodeActionKind::QUICK_FIX,
112
+ edit: Interface::WorkspaceEdit.new(
113
+ document_changes: [
114
+ Interface::TextDocumentEdit.new(
115
+ text_document: Interface::OptionalVersionedTextDocumentIdentifier.new(
116
+ uri: @uri.to_s,
117
+ version: nil
118
+ ),
119
+ edits: line_disable_comment
120
+ )
121
+ ]
122
+ )
123
+ )
124
+ end
125
+
126
+ def line_disable_comment
127
+ new_text = if @offense.source_line.include?(" # standard:disable ")
128
+ ",#{@offense.cop_name}"
129
+ else
130
+ " # standard:disable #{@offense.cop_name}"
131
+ end
132
+
133
+ eol = Interface::Position.new(
134
+ line: @offense.line - 1,
135
+ character: length_of_line(@offense.source_line)
136
+ )
137
+
138
+ # TODO: fails for multiline strings - may be preferable to use block
139
+ # comments to disable some offenses
140
+ inline_comment = Interface::TextEdit.new(
141
+ range: Interface::Range.new(start: eol, end: eol),
142
+ new_text: new_text
143
+ )
144
+
145
+ [inline_comment]
146
+ end
147
+
148
+ def length_of_line(line)
149
+ if @document_encoding == Encoding::UTF_16LE
150
+ line_length = 0
151
+ line.codepoints.each do |codepoint|
152
+ line_length += 1
153
+ if codepoint > RubyLsp::Document::Scanner::SURROGATE_PAIR_START
154
+ line_length += 1
155
+ end
156
+ end
157
+ line_length
158
+ else
159
+ line.length
160
+ end
161
+ end
162
+
163
+ def correctable?
164
+ !@offense.corrector.nil?
165
+ end
166
+ end
167
+ end
168
+ end
@@ -1,12 +1,13 @@
1
1
  module Standard
2
2
  module Lsp
3
3
  class Logger
4
- def initialize
4
+ def initialize(prefix: "[server]")
5
+ @prefix = prefix
5
6
  @puts_onces = []
6
7
  end
7
8
 
8
9
  def puts(message)
9
- warn("[server] #{message}")
10
+ warn [@prefix, message].compact.join(" ")
10
11
  end
11
12
 
12
13
  def puts_once(message)
@@ -154,45 +154,12 @@ module Standard
154
154
 
155
155
  def diagnostic(file_uri, text)
156
156
  @text_cache[file_uri] = text
157
- offenses = @standardizer.offenses(uri_to_path(file_uri), text)
158
-
159
- lsp_diagnostics = offenses.map { |o|
160
- code = o[:cop_name]
161
-
162
- msg = o[:message].delete_prefix(code)
163
- loc = o[:location]
164
-
165
- severity = case o[:severity]
166
- when "error", "fatal"
167
- SEV::ERROR
168
- when "warning"
169
- SEV::WARNING
170
- when "convention"
171
- SEV::INFORMATION
172
- when "refactor", "info"
173
- SEV::HINT
174
- else # the above cases fully cover what RuboCop sends at this time
175
- logger.puts "Unknown severity: #{severity.inspect}"
176
- SEV::HINT
177
- end
178
-
179
- {
180
- code: code,
181
- message: msg,
182
- range: {
183
- start: {character: loc[:start_column] - 1, line: loc[:start_line] - 1},
184
- end: {character: loc[:last_column], line: loc[:last_line] - 1}
185
- },
186
- severity: severity,
187
- source: "standard"
188
- }
189
- }
190
157
 
191
158
  {
192
159
  method: "textDocument/publishDiagnostics",
193
160
  params: {
194
161
  uri: file_uri,
195
- diagnostics: lsp_diagnostics
162
+ diagnostics: @standardizer.offenses(uri_to_path(file_uri), text)
196
163
  }
197
164
  }
198
165
  end
@@ -13,11 +13,12 @@ module Standard
13
13
  @writer = Proto::Transport::Io::Writer.new($stdout)
14
14
  @reader = Proto::Transport::Io::Reader.new($stdin)
15
15
  @logger = Logger.new
16
- @standardizer = Standard::Lsp::Standardizer.new(config, @logger)
16
+ @standardizer = Standard::Lsp::Standardizer.new(config)
17
17
  @routes = Routes.new(@writer, @logger, @standardizer)
18
18
  end
19
19
 
20
20
  def start
21
+ RuboCop::LSP.enable
21
22
  @reader.read do |request|
22
23
  if !request.key?(:method)
23
24
  @routes.handle_method_missing(request)
@@ -1,68 +1,33 @@
1
- require_relative "../runners/rubocop"
1
+ require_relative "stdin_rubocop_runner"
2
+ require_relative "diagnostic"
2
3
 
3
4
  module Standard
4
5
  module Lsp
5
6
  class Standardizer
6
- def initialize(config, logger)
7
- @config = config
8
- @logger = logger
9
- @rubocop_runner = Standard::Runners::Rubocop.new
7
+ def initialize(config)
8
+ @diagnostic_runner = ::Standard::Lsp::StdinRubocopRunner.new(config)
9
+ @format_runner = ::Standard::Lsp::StdinRubocopRunner.new(config.dup.tap { |c|
10
+ c.rubocop_options[:autocorrect] = true
11
+ })
12
+ @cop_registry = RuboCop::Cop::Registry.global.to_h
10
13
  end
11
14
 
12
- # This abuses the --stdin option of rubocop and reads the formatted text
13
- # from the options[:stdin] that rubocop mutates. This depends on
14
- # parallel: false as well as the fact that rubocop doesn't otherwise dup
15
- # or reassign that options object. Risky business!
16
- #
17
- # Reassigning options[:stdin] is done here:
18
- # https://github.com/rubocop/rubocop/blob/master/lib/rubocop/cop/team.rb#L131
19
- # Printing options[:stdin]
20
- # https://github.com/rubocop/rubocop/blob/master/lib/rubocop/cli/command/execute_runner.rb#L95
21
- # Setting `parallel: true` would break this here:
22
- # https://github.com/rubocop/rubocop/blob/master/lib/rubocop/runner.rb#L72
23
15
  def format(path, text)
24
- ad_hoc_config = fork_config(path, text, format: true)
25
- capture_rubocop_stdout(ad_hoc_config)
26
- ad_hoc_config.rubocop_options[:stdin]
16
+ @format_runner.run(path, text)
17
+ @format_runner.formatted_source
27
18
  end
28
19
 
29
- def offenses(path, text)
30
- results = JSON.parse(
31
- capture_rubocop_stdout(fork_config(path, text, format: false)),
32
- symbolize_names: true
33
- )
34
- if results[:files].empty?
35
- @logger.puts_once "Ignoring file, per configuration: #{path}"
36
- []
37
- else
38
- results.dig(:files, 0, :offenses)
39
- end
40
- end
41
-
42
- private
20
+ def offenses(path, text, document_encoding = nil)
21
+ @diagnostic_runner.run(path, text)
43
22
 
44
- BASE_OPTIONS = {
45
- force_exclusion: true,
46
- parallel: false,
47
- todo_file: nil,
48
- todo_ignore_files: []
49
- }
50
- def fork_config(path, text, format:)
51
- options = if format
52
- {stdin: text, autocorrect: true, safe_autocorrect: true, formatters: []}
53
- else
54
- {stdin: text, autocorrect: false, safe_autocorrect: false, formatters: [["json"]], format: "json"}
23
+ @diagnostic_runner.offenses.map do |offense|
24
+ Diagnostic.new(
25
+ document_encoding,
26
+ offense,
27
+ path,
28
+ @cop_registry[offense.cop_name]&.first
29
+ ).to_lsp_diagnostic(@diagnostic_runner.config_for_working_directory)
55
30
  end
56
- Standard::Config.new(@config.runner, [path], BASE_OPTIONS.merge(options), @config.rubocop_config_store)
57
- end
58
-
59
- def capture_rubocop_stdout(config)
60
- redir = StringIO.new
61
- $stdout = redir
62
- @rubocop_runner.call(config)
63
- redir.string
64
- ensure
65
- $stdout = STDOUT
66
31
  end
67
32
  end
68
33
  end
@@ -0,0 +1,61 @@
1
+ module Standard
2
+ module Lsp
3
+ # Originally lifted from:
4
+ # https://github.com/Shopify/ruby-lsp/blob/8d4c17efce4e8ecc8e7c557ab2981db6b22c0b6d/lib/ruby_lsp/requests/support/rubocop_runner.rb#L20
5
+ class StdinRubocopRunner < ::RuboCop::Runner
6
+ class ConfigurationError < StandardError; end
7
+
8
+ attr_reader :offenses
9
+
10
+ attr_reader :config_for_working_directory
11
+
12
+ DEFAULT_RUBOCOP_OPTIONS = {
13
+ stderr: true,
14
+ force_exclusion: true,
15
+ format: "RuboCop::Formatter::BaseFormatter",
16
+ raise_cop_error: true
17
+ }.freeze
18
+
19
+ def initialize(config)
20
+ @options = {}
21
+ @offenses = []
22
+ @errors = []
23
+ @warnings = []
24
+
25
+ @config_for_working_directory = config.rubocop_config_store.for_pwd
26
+
27
+ super(
28
+ config.rubocop_options.merge(DEFAULT_RUBOCOP_OPTIONS),
29
+ config.rubocop_config_store
30
+ )
31
+ end
32
+
33
+ def run(path, contents)
34
+ @errors = []
35
+ @warnings = []
36
+ @offenses = []
37
+ @options[:stdin] = contents
38
+
39
+ super([path])
40
+
41
+ raise Interrupt if aborting?
42
+ rescue ::RuboCop::Runner::InfiniteCorrectionLoop => error
43
+ raise RubyLsp::Requests::Formatting::Erro, error.message
44
+ rescue ::RuboCop::ValidationError => error
45
+ raise ConfigurationError, error.message
46
+ rescue => error
47
+ raise ::RubyLsp::Requests::Support::InternalRuboCopError, error
48
+ end
49
+
50
+ def formatted_source
51
+ @options[:stdin]
52
+ end
53
+
54
+ private
55
+
56
+ def file_finished(_file, offenses)
57
+ @offenses = offenses
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,3 +1,3 @@
1
1
  module Standard
2
- VERSION = Gem::Version.new("1.38.0")
2
+ VERSION = Gem::Version.new("1.39.0")
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.38.0
4
+ version: 1.39.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Searls
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-06-23 00:00:00.000000000 Z
11
+ date: 2024-06-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubocop
@@ -139,11 +139,13 @@ files:
139
139
  - lib/standard/formatter.rb
140
140
  - lib/standard/loads_runner.rb
141
141
  - lib/standard/loads_yaml_config.rb
142
+ - lib/standard/lsp/diagnostic.rb
142
143
  - lib/standard/lsp/kills_server.rb
143
144
  - lib/standard/lsp/logger.rb
144
145
  - lib/standard/lsp/routes.rb
145
146
  - lib/standard/lsp/server.rb
146
147
  - lib/standard/lsp/standardizer.rb
148
+ - lib/standard/lsp/stdin_rubocop_runner.rb
147
149
  - lib/standard/merges_settings.rb
148
150
  - lib/standard/plugin.rb
149
151
  - lib/standard/plugin/combines_plugin_configs.rb