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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +11 -0
  3. data/.github/workflows/test.yml +24 -0
  4. data/.github/workflows/update.yml +54 -0
  5. data/.gitignore +3 -0
  6. data/.standard.yml +1 -1
  7. data/CHANGELOG.md +703 -0
  8. data/Gemfile +13 -0
  9. data/Gemfile.lock +71 -37
  10. data/LICENSE.txt +3 -4
  11. data/README.md +459 -264
  12. data/Rakefile +5 -4
  13. data/bin/console +0 -4
  14. data/bin/rake +27 -0
  15. data/bin/run +9 -0
  16. data/config/base.yml +1065 -193
  17. data/config/default.yml +8 -0
  18. data/config/ruby-1.8.yml +10 -2
  19. data/config/ruby-1.9.yml +11 -1
  20. data/config/ruby-2.0.yml +4 -0
  21. data/config/ruby-2.1.yml +4 -0
  22. data/config/ruby-2.2.yml +13 -5
  23. data/config/ruby-2.3.yml +10 -0
  24. data/config/ruby-2.4.yml +10 -0
  25. data/config/ruby-2.5.yml +10 -0
  26. data/config/ruby-2.6.yml +13 -0
  27. data/config/ruby-2.7.yml +10 -0
  28. data/config/ruby-3.0.yml +13 -0
  29. data/config/ruby-3.1.yml +11 -0
  30. data/config/ruby-3.2.yml +4 -0
  31. data/config/ruby-3.3.yml +7 -0
  32. data/docs/ARCHITECTURE.md +33 -0
  33. data/docs/RELEASE.md +41 -0
  34. data/docs/RUBY_VERSIONS.md +51 -0
  35. data/docs/UPGRADING.md +31 -0
  36. data/lib/ruby_lsp/standard/addon.rb +58 -0
  37. data/lib/ruby_lsp/standard/wraps_built_in_lsp_standardizer.rb +44 -0
  38. data/lib/standard/base/plugin.rb +69 -0
  39. data/lib/standard/base.rb +8 -0
  40. data/lib/standard/builds_config.rb +11 -1
  41. data/lib/standard/cli.rb +1 -7
  42. data/lib/standard/creates_config_store/assigns_rubocop_yaml.rb +2 -18
  43. data/lib/standard/creates_config_store/configures_ignored_paths.rb +2 -2
  44. data/lib/standard/creates_config_store/merges_user_config_extensions.rb +37 -0
  45. data/lib/standard/creates_config_store/sets_target_ruby_version.rb +21 -8
  46. data/lib/standard/creates_config_store.rb +5 -0
  47. data/lib/standard/formatter.rb +92 -37
  48. data/lib/standard/loads_runner.rb +17 -3
  49. data/lib/standard/loads_yaml_config.rb +18 -11
  50. data/lib/standard/lsp/diagnostic.rb +174 -0
  51. data/lib/standard/lsp/kills_server.rb +10 -0
  52. data/lib/standard/lsp/logger.rb +21 -0
  53. data/lib/standard/lsp/routes.rb +175 -0
  54. data/lib/standard/lsp/server.rb +37 -0
  55. data/lib/standard/lsp/standardizer.rb +34 -0
  56. data/lib/standard/lsp/stdin_rubocop_runner.rb +71 -0
  57. data/lib/standard/merges_settings.rb +22 -11
  58. data/lib/standard/plugin/combines_plugin_configs.rb +15 -0
  59. data/lib/standard/plugin/creates_runner_context.rb +15 -0
  60. data/lib/standard/plugin/determines_class_constant.rb +56 -0
  61. data/lib/standard/plugin/initializes_plugins.rb +23 -0
  62. data/lib/standard/plugin/merges_plugins_into_rubocop_config.rb +177 -0
  63. data/lib/standard/plugin/standardizes_configured_plugins.rb +37 -0
  64. data/lib/standard/plugin.rb +11 -0
  65. data/lib/standard/railtie.rb +1 -1
  66. data/lib/standard/rake.rb +8 -1
  67. data/lib/standard/{parses_cli_option.rb → resolves_yaml_option.rb} +9 -2
  68. data/lib/standard/rubocop/ext.rb +17 -0
  69. data/lib/standard/runners/genignore.rb +44 -0
  70. data/lib/standard/runners/help.rb +9 -5
  71. data/lib/standard/runners/lsp.rb +11 -0
  72. data/lib/standard/runners/rubocop.rb +14 -18
  73. data/lib/standard/runners/verbose_version.rb +14 -0
  74. data/lib/standard/version.rb +1 -1
  75. data/lib/standard.rb +6 -4
  76. data/standard.gemspec +22 -20
  77. metadata +72 -73
  78. data/.circleci/config.yml +0 -35
  79. data/lib/standard/cop/semantic_blocks.rb +0 -162
  80. 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 { |flag|
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({}) { |arg, cli_flags|
28
+ argv.each_with_object({}) do |arg, cli_flags|
29
29
  if arg == "--fix"
30
- cli_flags[:auto_correct] = true
31
- cli_flags[:safe_auto_correct] = true
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[:auto_correct] = false
34
- cli_flags[:safe_auto_correct] = false
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
- auto_correct: standard_yaml[:fix],
52
- safe_auto_correct: standard_yaml[:fix],
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