standard 0.1.0 → 1.51.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/.github/dependabot.yml +11 -0
- data/.github/workflows/test.yml +24 -0
- data/.github/workflows/update.yml +54 -0
- data/.gitignore +3 -0
- data/.standard.yml +1 -1
- data/CHANGELOG.md +703 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +71 -37
- data/LICENSE.txt +3 -4
- data/README.md +459 -264
- data/Rakefile +5 -4
- data/bin/console +0 -4
- data/bin/rake +27 -0
- data/bin/run +9 -0
- data/config/base.yml +1065 -193
- data/config/default.yml +8 -0
- data/config/ruby-1.8.yml +10 -2
- data/config/ruby-1.9.yml +11 -1
- data/config/ruby-2.0.yml +4 -0
- data/config/ruby-2.1.yml +4 -0
- data/config/ruby-2.2.yml +13 -5
- data/config/ruby-2.3.yml +10 -0
- data/config/ruby-2.4.yml +10 -0
- data/config/ruby-2.5.yml +10 -0
- data/config/ruby-2.6.yml +13 -0
- data/config/ruby-2.7.yml +10 -0
- data/config/ruby-3.0.yml +13 -0
- data/config/ruby-3.1.yml +11 -0
- data/config/ruby-3.2.yml +4 -0
- data/config/ruby-3.3.yml +7 -0
- data/docs/ARCHITECTURE.md +33 -0
- data/docs/RELEASE.md +41 -0
- data/docs/RUBY_VERSIONS.md +51 -0
- data/docs/UPGRADING.md +31 -0
- data/lib/ruby_lsp/standard/addon.rb +58 -0
- data/lib/ruby_lsp/standard/wraps_built_in_lsp_standardizer.rb +44 -0
- data/lib/standard/base/plugin.rb +69 -0
- data/lib/standard/base.rb +8 -0
- data/lib/standard/builds_config.rb +11 -1
- data/lib/standard/cli.rb +1 -7
- data/lib/standard/creates_config_store/assigns_rubocop_yaml.rb +2 -18
- data/lib/standard/creates_config_store/configures_ignored_paths.rb +2 -2
- data/lib/standard/creates_config_store/merges_user_config_extensions.rb +37 -0
- data/lib/standard/creates_config_store/sets_target_ruby_version.rb +21 -8
- data/lib/standard/creates_config_store.rb +5 -0
- data/lib/standard/formatter.rb +92 -37
- data/lib/standard/loads_runner.rb +17 -3
- data/lib/standard/loads_yaml_config.rb +18 -11
- data/lib/standard/lsp/diagnostic.rb +174 -0
- data/lib/standard/lsp/kills_server.rb +10 -0
- data/lib/standard/lsp/logger.rb +21 -0
- data/lib/standard/lsp/routes.rb +175 -0
- data/lib/standard/lsp/server.rb +37 -0
- data/lib/standard/lsp/standardizer.rb +34 -0
- data/lib/standard/lsp/stdin_rubocop_runner.rb +71 -0
- data/lib/standard/merges_settings.rb +22 -11
- data/lib/standard/plugin/combines_plugin_configs.rb +15 -0
- data/lib/standard/plugin/creates_runner_context.rb +15 -0
- data/lib/standard/plugin/determines_class_constant.rb +56 -0
- data/lib/standard/plugin/initializes_plugins.rb +23 -0
- data/lib/standard/plugin/merges_plugins_into_rubocop_config.rb +177 -0
- data/lib/standard/plugin/standardizes_configured_plugins.rb +37 -0
- data/lib/standard/plugin.rb +11 -0
- data/lib/standard/railtie.rb +1 -1
- data/lib/standard/rake.rb +8 -1
- data/lib/standard/{parses_cli_option.rb → resolves_yaml_option.rb} +9 -2
- data/lib/standard/rubocop/ext.rb +17 -0
- data/lib/standard/runners/genignore.rb +44 -0
- data/lib/standard/runners/help.rb +9 -5
- data/lib/standard/runners/lsp.rb +11 -0
- data/lib/standard/runners/rubocop.rb +14 -18
- data/lib/standard/runners/verbose_version.rb +14 -0
- data/lib/standard/version.rb +1 -1
- data/lib/standard.rb +6 -4
- data/standard.gemspec +22 -20
- metadata +72 -73
- data/.circleci/config.yml +0 -35
- data/lib/standard/cop/semantic_blocks.rb +0 -162
- data/lib/standard/detects_fixability.rb +0 -20
@@ -0,0 +1,175 @@
|
|
1
|
+
require_relative "kills_server"
|
2
|
+
|
3
|
+
module Standard
|
4
|
+
module Lsp
|
5
|
+
class Routes
|
6
|
+
def initialize(writer, logger, standardizer)
|
7
|
+
@writer = writer
|
8
|
+
@logger = logger
|
9
|
+
@standardizer = standardizer
|
10
|
+
|
11
|
+
@text_cache = {}
|
12
|
+
@kills_server = KillsServer.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.handle(name, &block)
|
16
|
+
define_method(:"handle_#{name}", &block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def for(name)
|
20
|
+
name = "handle_#{name}"
|
21
|
+
if respond_to?(name)
|
22
|
+
method(name)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
handle "initialize" do |request|
|
27
|
+
@writer.write(id: request[:id], result: Proto::Interface::InitializeResult.new(
|
28
|
+
capabilities: Proto::Interface::ServerCapabilities.new(
|
29
|
+
document_formatting_provider: true,
|
30
|
+
text_document_sync: Proto::Interface::TextDocumentSyncOptions.new(
|
31
|
+
change: Proto::Constant::TextDocumentSyncKind::FULL,
|
32
|
+
open_close: true
|
33
|
+
)
|
34
|
+
)
|
35
|
+
))
|
36
|
+
end
|
37
|
+
|
38
|
+
handle "initialized" do |request|
|
39
|
+
@logger.puts "Standard Ruby v#{Standard::VERSION} LSP server initialized, pid #{Process.pid}"
|
40
|
+
end
|
41
|
+
|
42
|
+
handle "shutdown" do |request|
|
43
|
+
@logger.puts "Client asked to shutdown Standard LSP server."
|
44
|
+
@kills_server.call do
|
45
|
+
@writer.write(id: request[:id], result: nil)
|
46
|
+
@logger.puts "Exiting..."
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
handle "textDocument/didChange" do |request|
|
51
|
+
params = request[:params]
|
52
|
+
result = diagnostic(params[:textDocument][:uri], params[:contentChanges][0][:text])
|
53
|
+
@writer.write(result)
|
54
|
+
end
|
55
|
+
|
56
|
+
handle "textDocument/didOpen" do |request|
|
57
|
+
doc = request[:params][:textDocument]
|
58
|
+
result = diagnostic(doc[:uri], doc[:text])
|
59
|
+
@writer.write(result)
|
60
|
+
end
|
61
|
+
|
62
|
+
handle "textDocument/didClose" do |request|
|
63
|
+
@text_cache.delete(request.dig(:params, :textDocument, :uri))
|
64
|
+
end
|
65
|
+
|
66
|
+
handle "textDocument/formatting" do |request|
|
67
|
+
uri = request[:params][:textDocument][:uri]
|
68
|
+
@writer.write({id: request[:id], result: format_file(uri)})
|
69
|
+
end
|
70
|
+
|
71
|
+
handle "workspace/didChangeConfiguration" do |_request|
|
72
|
+
@logger.puts "Ignoring workspace/didChangeConfiguration"
|
73
|
+
end
|
74
|
+
|
75
|
+
CONFIGURATION_FILE_PATTERNS = [
|
76
|
+
".standard.yml",
|
77
|
+
".standard_todo.yml"
|
78
|
+
].freeze
|
79
|
+
|
80
|
+
handle "workspace/didChangeWatchedFiles" do |request|
|
81
|
+
if request[:params][:changes].any? { |change|
|
82
|
+
CONFIGURATION_FILE_PATTERNS.any? { |path| change[:uri].end_with?(path) }
|
83
|
+
}
|
84
|
+
@logger.puts "Configuration file changed; restart required"
|
85
|
+
@kills_server.call
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
handle "workspace/executeCommand" do |request|
|
90
|
+
if request[:params][:command] == "standardRuby.formatAutoFixes"
|
91
|
+
uri = request[:params][:arguments][0][:uri]
|
92
|
+
@writer.write({
|
93
|
+
id: request[:id],
|
94
|
+
method: "workspace/applyEdit",
|
95
|
+
params: {
|
96
|
+
label: "Format with Standard Ruby auto-fixes",
|
97
|
+
edit: {
|
98
|
+
changes: {
|
99
|
+
uri => format_file(uri)
|
100
|
+
}
|
101
|
+
}
|
102
|
+
}
|
103
|
+
})
|
104
|
+
else
|
105
|
+
handle_unsupported_method(request, request[:params][:command])
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
handle "textDocument/didSave" do |_request|
|
110
|
+
# Nothing to do
|
111
|
+
end
|
112
|
+
|
113
|
+
handle "$/cancelRequest" do |_request|
|
114
|
+
# Can't cancel anything because single-threaded
|
115
|
+
end
|
116
|
+
|
117
|
+
handle "$/setTrace" do |_request|
|
118
|
+
# No-op, we log everything
|
119
|
+
end
|
120
|
+
|
121
|
+
def handle_unsupported_method(request, method = request[:method])
|
122
|
+
@writer.write({id: request[:id], error: Proto::Interface::ResponseError.new(
|
123
|
+
code: Proto::Constant::ErrorCodes::METHOD_NOT_FOUND,
|
124
|
+
message: "Unsupported Method: #{method}"
|
125
|
+
)})
|
126
|
+
@logger.puts "Unsupported Method: #{method}"
|
127
|
+
end
|
128
|
+
|
129
|
+
def handle_method_missing(request)
|
130
|
+
if request.key?(:id)
|
131
|
+
@writer.write({id: request[:id], result: nil})
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def uri_to_path(uri)
|
138
|
+
uri.sub(%r{^file://}, "")
|
139
|
+
end
|
140
|
+
|
141
|
+
def format_file(file_uri)
|
142
|
+
text = @text_cache[file_uri]
|
143
|
+
if text.nil?
|
144
|
+
@logger.puts "Format request arrived before text synchonized; skipping: `#{file_uri}'"
|
145
|
+
[]
|
146
|
+
else
|
147
|
+
new_text = @standardizer.format(uri_to_path(file_uri), text)
|
148
|
+
if new_text == text
|
149
|
+
[]
|
150
|
+
else
|
151
|
+
[{
|
152
|
+
newText: new_text,
|
153
|
+
range: {
|
154
|
+
start: {line: 0, character: 0},
|
155
|
+
end: {line: text.count("\n") + 1, character: 0}
|
156
|
+
}
|
157
|
+
}]
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def diagnostic(file_uri, text)
|
163
|
+
@text_cache[file_uri] = text
|
164
|
+
|
165
|
+
{
|
166
|
+
method: "textDocument/publishDiagnostics",
|
167
|
+
params: {
|
168
|
+
uri: file_uri,
|
169
|
+
diagnostics: @standardizer.offenses(uri_to_path(file_uri), text)
|
170
|
+
}
|
171
|
+
}
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "language_server-protocol"
|
2
|
+
require_relative "standardizer"
|
3
|
+
require_relative "routes"
|
4
|
+
require_relative "logger"
|
5
|
+
|
6
|
+
module Standard
|
7
|
+
module Lsp
|
8
|
+
Proto = LanguageServer::Protocol
|
9
|
+
SEV = Proto::Constant::DiagnosticSeverity
|
10
|
+
|
11
|
+
class Server
|
12
|
+
def initialize(config)
|
13
|
+
@writer = Proto::Transport::Io::Writer.new($stdout)
|
14
|
+
@reader = Proto::Transport::Io::Reader.new($stdin)
|
15
|
+
@logger = Logger.new
|
16
|
+
@standardizer = Standard::Lsp::Standardizer.new(config)
|
17
|
+
@routes = Routes.new(@writer, @logger, @standardizer)
|
18
|
+
end
|
19
|
+
|
20
|
+
def start
|
21
|
+
RuboCop::LSP.enable
|
22
|
+
@reader.read do |request|
|
23
|
+
if !request.key?(:method)
|
24
|
+
@routes.handle_method_missing(request)
|
25
|
+
elsif (route = @routes.for(request[:method]))
|
26
|
+
route.call(request)
|
27
|
+
else
|
28
|
+
@routes.handle_unsupported_method(request)
|
29
|
+
end
|
30
|
+
rescue => e
|
31
|
+
@logger.puts "Error #{e.class} #{e.message[0..100]}"
|
32
|
+
@logger.puts e.backtrace.inspect
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require_relative "stdin_rubocop_runner"
|
2
|
+
require_relative "diagnostic"
|
3
|
+
|
4
|
+
module Standard
|
5
|
+
module Lsp
|
6
|
+
class Standardizer
|
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
|
13
|
+
end
|
14
|
+
|
15
|
+
def format(path, text)
|
16
|
+
@format_runner.run(path, text)
|
17
|
+
@format_runner.formatted_source
|
18
|
+
end
|
19
|
+
|
20
|
+
def offenses(path, text, document_encoding = nil)
|
21
|
+
@diagnostic_runner.run(path, text)
|
22
|
+
|
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)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,71 @@
|
|
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
|
+
formatters: ["RuboCop::Formatter::BaseFormatter"],
|
16
|
+
raise_cop_error: true,
|
17
|
+
todo_file: nil,
|
18
|
+
todo_ignore_files: []
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
def initialize(config)
|
22
|
+
@options = {}
|
23
|
+
@offenses = []
|
24
|
+
@errors = []
|
25
|
+
@warnings = []
|
26
|
+
|
27
|
+
@config_for_working_directory = config.rubocop_config_store.for_pwd
|
28
|
+
|
29
|
+
super(
|
30
|
+
config.rubocop_options.merge(DEFAULT_RUBOCOP_OPTIONS),
|
31
|
+
config.rubocop_config_store
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def run(path, contents)
|
36
|
+
@errors = []
|
37
|
+
@warnings = []
|
38
|
+
@offenses = []
|
39
|
+
@options[:stdin] = contents
|
40
|
+
|
41
|
+
super([path])
|
42
|
+
|
43
|
+
raise Interrupt if aborting?
|
44
|
+
rescue ::RuboCop::Runner::InfiniteCorrectionLoop => error
|
45
|
+
if defined?(::RubyLsp::Requests::Formatting::Error)
|
46
|
+
raise ::RubyLsp::Requests::Formatting::Error, error.message
|
47
|
+
else
|
48
|
+
raise error
|
49
|
+
end
|
50
|
+
rescue ::RuboCop::ValidationError => error
|
51
|
+
raise ConfigurationError, error.message
|
52
|
+
rescue => error
|
53
|
+
if defined?(::RubyLsp::Requests::Formatting::Error)
|
54
|
+
raise ::RubyLsp::Requests::Support::InternalRuboCopError, error
|
55
|
+
else
|
56
|
+
raise error
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def formatted_source
|
61
|
+
@options[:stdin]
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def file_finished(_file, offenses)
|
67
|
+
@offenses = offenses
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -19,21 +19,24 @@ module Standard
|
|
19
19
|
private
|
20
20
|
|
21
21
|
def separate_argv(argv)
|
22
|
-
argv.partition
|
23
|
-
["--fix", "--no-fix", "--version", "-v", "--help", "-h"].include?(flag)
|
24
|
-
|
22
|
+
argv.partition do |flag|
|
23
|
+
["--generate-todo", "--fix", "--fix-unsafely", "--no-fix", "--version", "-v", "--verbose-version", "-V", "--help", "-h", "--lsp"].include?(flag)
|
24
|
+
end
|
25
25
|
end
|
26
26
|
|
27
27
|
def parse_standard_argv(argv)
|
28
|
-
argv.each_with_object({})
|
28
|
+
argv.each_with_object({}) do |arg, cli_flags|
|
29
29
|
if arg == "--fix"
|
30
|
-
cli_flags[:
|
31
|
-
cli_flags[:
|
30
|
+
cli_flags[:autocorrect] = true
|
31
|
+
cli_flags[:safe_autocorrect] = true
|
32
|
+
elsif arg == "--fix-unsafely"
|
33
|
+
cli_flags[:autocorrect] = true
|
34
|
+
cli_flags[:safe_autocorrect] = false
|
32
35
|
elsif arg == "--no-fix"
|
33
|
-
cli_flags[:
|
34
|
-
cli_flags[:
|
36
|
+
cli_flags[:autocorrect] = false
|
37
|
+
cli_flags[:safe_autocorrect] = false
|
35
38
|
end
|
36
|
-
|
39
|
+
end
|
37
40
|
end
|
38
41
|
|
39
42
|
def determine_command(argv)
|
@@ -41,6 +44,12 @@ module Standard
|
|
41
44
|
:help
|
42
45
|
elsif (argv & ["--version", "-v"]).any?
|
43
46
|
:version
|
47
|
+
elsif (argv & ["--verbose-version", "-V"]).any?
|
48
|
+
:verbose_version
|
49
|
+
elsif (argv & ["--generate-todo"]).any?
|
50
|
+
:genignore
|
51
|
+
elsif (argv & ["--lsp"]).any?
|
52
|
+
:lsp
|
44
53
|
else
|
45
54
|
:rubocop
|
46
55
|
end
|
@@ -48,10 +57,12 @@ module Standard
|
|
48
57
|
|
49
58
|
def merge(standard_yaml, standard_cli_flags, rubocop_cli_flags)
|
50
59
|
{
|
51
|
-
|
52
|
-
|
60
|
+
autocorrect: standard_yaml[:fix],
|
61
|
+
safe_autocorrect: true,
|
53
62
|
formatters: [[standard_yaml[:format] || "Standard::Formatter", nil]],
|
54
63
|
parallel: standard_yaml[:parallel],
|
64
|
+
todo_file: standard_yaml[:todo_file],
|
65
|
+
todo_ignore_files: standard_yaml[:todo_ignore_files]
|
55
66
|
}.merge(standard_cli_flags).merge(rubocop_cli_flags)
|
56
67
|
end
|
57
68
|
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Standard
|
2
|
+
module Plugin
|
3
|
+
class CombinesPluginConfigs
|
4
|
+
def initialize
|
5
|
+
@initializes_plugins = InitializesPlugins.new
|
6
|
+
@merges_plugins_into_rubocop_config = MergesPluginsIntoRubocopConfig.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(options_config, standard_config)
|
10
|
+
plugins = @initializes_plugins.call(standard_config[:plugins])
|
11
|
+
@merges_plugins_into_rubocop_config.call(options_config, standard_config, plugins, permit_merging: true)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Standard
|
2
|
+
module Plugin
|
3
|
+
class CreatesRunnerContext
|
4
|
+
def call(standard_config)
|
5
|
+
LintRoller::Context.new(
|
6
|
+
runner: :standard,
|
7
|
+
runner_version: Standard::VERSION,
|
8
|
+
engine: :rubocop,
|
9
|
+
engine_version: RuboCop::Version.version,
|
10
|
+
target_ruby_version: standard_config[:ruby_version]
|
11
|
+
)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Standard
|
2
|
+
module Plugin
|
3
|
+
class DeterminesClassConstant
|
4
|
+
def call(plugin_name, user_config)
|
5
|
+
require_plugin(user_config["require_path"])
|
6
|
+
|
7
|
+
if (constant_name = user_config["plugin_class_name"])
|
8
|
+
begin
|
9
|
+
Kernel.const_get(constant_name)
|
10
|
+
rescue
|
11
|
+
raise "Failed while configuring plugin `#{plugin_name}': no constant with name `#{constant_name}' was found"
|
12
|
+
end
|
13
|
+
else
|
14
|
+
begin
|
15
|
+
Kernel.const_get(Gem.loaded_specs[plugin_name].metadata["default_lint_roller_plugin"])
|
16
|
+
rescue LoadError, StandardError
|
17
|
+
raise <<~MSG
|
18
|
+
Failed loading plugin `#{plugin_name}' because we couldn't determine
|
19
|
+
the corresponding plugin class to instantiate.
|
20
|
+
|
21
|
+
Standard plugin class names must either be:
|
22
|
+
|
23
|
+
- If the plugin is a gem, defined in the gemspec as `default_lint_roller_plugin'
|
24
|
+
|
25
|
+
spec.metadata["default_lint_roller_plugin"] = "MyModule::Plugin"
|
26
|
+
|
27
|
+
- Set in YAML as `plugin_class_name'; example:
|
28
|
+
|
29
|
+
plugins:
|
30
|
+
- incomplete:
|
31
|
+
require_path: my_module/plugin
|
32
|
+
plugin_class_name: "MyModule::Plugin"
|
33
|
+
MSG
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def require_plugin(require_path)
|
42
|
+
return if require_path.nil?
|
43
|
+
|
44
|
+
begin
|
45
|
+
require require_path
|
46
|
+
rescue LoadError
|
47
|
+
# If require_path isn't on the load_path (and it may be hard to get it
|
48
|
+
# on there since standardrb is a cli), let's give folks a break and
|
49
|
+
# also try to load it via the current working directory. This is
|
50
|
+
# tested in test/standardrb_test.rb #test_plugins_options
|
51
|
+
require Pathname.new(Dir.pwd).join(require_path)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Standard
|
2
|
+
module Plugin
|
3
|
+
class InitializesPlugins
|
4
|
+
def initialize
|
5
|
+
@standardizes_configured_plugins = StandardizesConfiguredPlugins.new
|
6
|
+
@determines_class_constants = DeterminesClassConstant.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(plugins)
|
10
|
+
plugin_configs = @standardizes_configured_plugins.call(plugins)
|
11
|
+
plugin_configs.map { |name_or_class, user_config|
|
12
|
+
if user_config["enabled"]
|
13
|
+
if name_or_class.is_a?(String) || name_or_class.is_a?(Symbol)
|
14
|
+
@determines_class_constants.call(name_or_class, user_config).new(user_config)
|
15
|
+
else
|
16
|
+
name_or_class.new(user_config)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
module Standard
|
2
|
+
module Plugin
|
3
|
+
class MergesPluginsIntoRubocopConfig
|
4
|
+
# Blank configuration object to merge plugins into, with only the following spared:
|
5
|
+
# - AllCops keys set to avoid warnings about unknown properties
|
6
|
+
# - Lint/Syntax must be set to avoid a nil error when verifying inherited configs
|
7
|
+
MANDATORY_RUBOCOP_CONFIG_KEYS = ["AllCops", "Lint/Syntax"].freeze
|
8
|
+
|
9
|
+
# AllCops keys that standard does not allow to be set by plugins
|
10
|
+
DISALLOWED_ALLCOPS_KEYS = [
|
11
|
+
"Include",
|
12
|
+
"Exclude",
|
13
|
+
"StyleGuideBaseURL",
|
14
|
+
"StyleGuideCopsOnly",
|
15
|
+
"TargetRubyVersion",
|
16
|
+
"EnabledByDefault",
|
17
|
+
"DisabledByDefault",
|
18
|
+
|
19
|
+
# The AllCops[Enabled] key is an unused artifact of #merge_with_default.
|
20
|
+
# See: https://github.com/rubocop/rubocop/blob/master/lib/rubocop/config_loader_resolver.rb#L81-L85
|
21
|
+
"Enabled"
|
22
|
+
].freeze
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
@creates_runner_context = Standard::Plugin::CreatesRunnerContext.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def call(options_config, standard_config, plugins, permit_merging:)
|
29
|
+
runner_context = @creates_runner_context.call(standard_config)
|
30
|
+
plugin_config = combine_rubocop_configs(options_config, runner_context, plugins, permit_merging: permit_merging).to_h
|
31
|
+
merge_config_into_all_cops!(options_config, plugin_config)
|
32
|
+
merge_config_into_standard!(options_config, plugin_config, permit_merging: permit_merging)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def combine_rubocop_configs(options_config, runner_context, plugins, permit_merging:)
|
38
|
+
all_cop_keys_configured_by_plugins = all_cop_keys_previously_configured_by_plugins(options_config, permit_merging: permit_merging)
|
39
|
+
fake_out_rubocop_default_configuration(options_config) do |fake_config|
|
40
|
+
plugins.reduce(fake_config) do |combined_config, plugin|
|
41
|
+
RuboCop::ConfigLoader.instance_variable_set(:@default_configuration, combined_config)
|
42
|
+
next_config, path = config_for_plugin(plugin, runner_context)
|
43
|
+
|
44
|
+
next_config["AllCops"], all_cop_keys_configured_by_plugins = merge_all_cop_settings(
|
45
|
+
combined_config["AllCops"],
|
46
|
+
next_config["AllCops"],
|
47
|
+
all_cop_keys_configured_by_plugins
|
48
|
+
)
|
49
|
+
delete_already_configured_keys!(combined_config.keys, next_config, dont_delete_keys: ["AllCops"])
|
50
|
+
|
51
|
+
RuboCop::ConfigLoader.merge_with_default(next_config, path, unset_nil: false)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def config_for_plugin(plugin, runner_context)
|
57
|
+
rules = plugin.rules(runner_context)
|
58
|
+
|
59
|
+
if rules.type == :path
|
60
|
+
[RuboCop::ConfigLoader.load_file(rules.value), rules.value]
|
61
|
+
elsif rules.type == :object
|
62
|
+
path = plugin.method(:rules).source_location[0]
|
63
|
+
[RuboCop::Config.create(rules.value, path, check: true), path]
|
64
|
+
elsif rules.type == :error
|
65
|
+
raise "Plugin `#{plugin.about&.name || plugin.inspect}' failed to load with error: #{rules.value.respond_to?(:message) ? rules.value.message : rules.value}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# This is how we ensure "first-in wins": plugins can override AllCops settings that are
|
70
|
+
# set by RuboCop's default configuration, but once a plugin sets an AllCop setting, they
|
71
|
+
# have exclusive first-in-wins rights to that setting.
|
72
|
+
#
|
73
|
+
# The one exception to this are array fields, because we don't want to
|
74
|
+
# overwrite the AllCops defaults but rather munge the arrays (`existing |
|
75
|
+
# new`) to allow plugins to add to the array, for example Include and
|
76
|
+
# Exclude paths and patterns.
|
77
|
+
def merge_all_cop_settings(existing_all_cops, new_all_cops, already_configured_keys)
|
78
|
+
return [existing_all_cops, already_configured_keys] unless new_all_cops.is_a?(Hash)
|
79
|
+
|
80
|
+
combined_all_cops = existing_all_cops.dup
|
81
|
+
combined_configured_keys = already_configured_keys.dup
|
82
|
+
|
83
|
+
new_all_cops.each do |key, value|
|
84
|
+
if combined_all_cops[key].is_a?(Array) && value.is_a?(Array)
|
85
|
+
combined_all_cops[key] |= value
|
86
|
+
combined_configured_keys |= [key]
|
87
|
+
elsif !combined_configured_keys.include?(key)
|
88
|
+
combined_all_cops[key] = value
|
89
|
+
combined_configured_keys << key
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
[combined_all_cops, combined_configured_keys]
|
94
|
+
end
|
95
|
+
|
96
|
+
def delete_already_configured_keys!(configured_keys, next_config, dont_delete_keys: [])
|
97
|
+
duplicate_keys = configured_keys & Array(next_config&.keys)
|
98
|
+
|
99
|
+
(duplicate_keys - dont_delete_keys).each do |key|
|
100
|
+
next_config.delete(key)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def merge_config_into_all_cops!(options_config, plugin_config)
|
105
|
+
options_config["AllCops"].merge!(
|
106
|
+
except(plugin_config["AllCops"], DISALLOWED_ALLCOPS_KEYS)
|
107
|
+
)
|
108
|
+
end
|
109
|
+
|
110
|
+
def merge_config_into_standard!(options_config, plugin_config, permit_merging:)
|
111
|
+
if permit_merging
|
112
|
+
plugin_config.each do |key, value|
|
113
|
+
options_config[key] = if options_config[key].is_a?(Hash)
|
114
|
+
merge(options_config[key], value)
|
115
|
+
else
|
116
|
+
value
|
117
|
+
end
|
118
|
+
end
|
119
|
+
else
|
120
|
+
except(plugin_config, options_config.keys).each do |key, value|
|
121
|
+
options_config[key] = value
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def all_cop_keys_previously_configured_by_plugins(options_config, permit_merging:)
|
127
|
+
if permit_merging
|
128
|
+
[]
|
129
|
+
else
|
130
|
+
Array(options_config["AllCops"]&.keys) - RuboCop::ConfigLoader.default_configuration["AllCops"].keys
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def fake_out_rubocop_default_configuration(options_config)
|
135
|
+
og_default_config = RuboCop::ConfigLoader.default_configuration
|
136
|
+
set_target_rails_version_on_all_cops_because_its_technically_not_allowed!(options_config)
|
137
|
+
result = yield blank_rubocop_config(options_config)
|
138
|
+
RuboCop::ConfigLoader.instance_variable_set(:@default_configuration, og_default_config)
|
139
|
+
result
|
140
|
+
end
|
141
|
+
|
142
|
+
# Avoid a warning that would otherwise be emitted by any plugin that set TargetRailsVersion
|
143
|
+
# because it's not a default AllCops key specified in RuboCop's embedded default config.
|
144
|
+
#
|
145
|
+
# See: https://github.com/rubocop/rubocop/pull/11833
|
146
|
+
def set_target_rails_version_on_all_cops_because_its_technically_not_allowed!(options_config)
|
147
|
+
return if !options_config.key?("AllCops") || options_config["AllCops"].key?("TargetRailsVersion")
|
148
|
+
|
149
|
+
options_config["AllCops"]["TargetRailsVersion"] = nil
|
150
|
+
end
|
151
|
+
|
152
|
+
def blank_rubocop_config(example_config)
|
153
|
+
RuboCop::Config.new(example_config.to_h.slice(*MANDATORY_RUBOCOP_CONFIG_KEYS), "")
|
154
|
+
end
|
155
|
+
|
156
|
+
def except(hash_or_config, keys)
|
157
|
+
hash_or_config.to_h.except(*keys).to_h
|
158
|
+
end
|
159
|
+
|
160
|
+
# Always deletes nil entries, always overwrites arrays
|
161
|
+
# This is a simplified version of rubocop's ConfigLoader#merge:
|
162
|
+
# https://github.com/rubocop/rubocop/blob/v1.48.1/lib/rubocop/config_loader_resolver.rb#L98
|
163
|
+
def merge(old_hash, new_hash)
|
164
|
+
result = old_hash.merge(new_hash)
|
165
|
+
keys_appearing_in_both = old_hash.keys & new_hash.keys
|
166
|
+
keys_appearing_in_both.each do |key|
|
167
|
+
if new_hash[key].nil?
|
168
|
+
result.delete(key)
|
169
|
+
elsif old_hash[key].is_a?(Hash) && new_hash[key].is_a?(Hash)
|
170
|
+
result[key] = merge(old_hash[key], new_hash[key])
|
171
|
+
end
|
172
|
+
end
|
173
|
+
result
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|