standard 1.38.0 → 1.39.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 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