diff --git a/changelog/fix_make_server_mode_aware_of_cache_root_directory.md b/changelog/fix_make_server_mode_aware_of_cache_root_directory.md new file mode 100644 index 00000000000..cdc93c2cafd --- /dev/null +++ b/changelog/fix_make_server_mode_aware_of_cache_root_directory.md @@ -0,0 +1 @@ +* [#10842](https://github.com/rubocop/rubocop/issues/10842): Make server mode aware of `CacheRootDirectory` config option value, `RUBOCOP_CACHE_ROOT`, and `XDG_CACHE_HOME` environment variables. ([@koic][]) diff --git a/lib/rubocop/cache_config.rb b/lib/rubocop/cache_config.rb new file mode 100644 index 00000000000..504a32e8a19 --- /dev/null +++ b/lib/rubocop/cache_config.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module RuboCop + # This class represents the cache config of the caching RuboCop runs. + # @api private + class CacheConfig + def self.root_dir + root = ENV.fetch('RUBOCOP_CACHE_ROOT', nil) + root ||= yield + root ||= if ENV.key?('XDG_CACHE_HOME') + # Include user ID in the path to make sure the user has write + # access. + File.join(ENV.fetch('XDG_CACHE_HOME'), Process.uid.to_s) + else + # On FreeBSD, the /home path is a symbolic link to /usr/home + # and the $HOME environment variable returns the /home path. + # + # As $HOME is a built-in environment variable, FreeBSD users + # always get a warning message. + # + # To avoid raising warn log messages on FreeBSD, we retrieve + # the real path of the home folder. + File.join(File.realpath(Dir.home), '.cache') + end + + File.join(root, 'rubocop_cache') + end + end +end diff --git a/lib/rubocop/cli/command/auto_genenerate_config.rb b/lib/rubocop/cli/command/auto_genenerate_config.rb index 73e9f34a0d2..420ca7bd99e 100644 --- a/lib/rubocop/cli/command/auto_genenerate_config.rb +++ b/lib/rubocop/cli/command/auto_genenerate_config.rb @@ -98,7 +98,7 @@ def execute_runner def add_inheritance_from_auto_generated_file(config_file) file_string = " #{relative_path_to_todo_from_options_config}" - config_file ||= ConfigLoader::DOTFILE + config_file ||= ConfigFinder::DOTFILE if File.exist?(config_file) files = Array(ConfigLoader.load_yaml_configuration(config_file)['inherit_from']) @@ -113,7 +113,7 @@ def add_inheritance_from_auto_generated_file(config_file) write_config_file(config_file, file_string, rubocop_yml_contents) puts "Added inheritance from `#{relative_path_to_todo_from_options_config}` " \ - "in `#{ConfigLoader::DOTFILE}`." + "in `#{ConfigFinder::DOTFILE}`." end def existing_configuration(config_file) diff --git a/lib/rubocop/cli/command/init_dotfile.rb b/lib/rubocop/cli/command/init_dotfile.rb index 0e85f48b8ae..dab89307fec 100644 --- a/lib/rubocop/cli/command/init_dotfile.rb +++ b/lib/rubocop/cli/command/init_dotfile.rb @@ -6,7 +6,7 @@ module Command # Generate a .rubocop.yml file in the current directory. # @api private class InitDotfile < Base - DOTFILE = ConfigLoader::DOTFILE + DOTFILE = ConfigFinder::DOTFILE self.command_name = :init diff --git a/lib/rubocop/config_finder.rb b/lib/rubocop/config_finder.rb new file mode 100644 index 00000000000..c990969f197 --- /dev/null +++ b/lib/rubocop/config_finder.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require_relative 'file_finder' + +module RuboCop + # This class has methods related to finding configuration path. + # @api private + class ConfigFinder + DOTFILE = '.rubocop.yml' + XDG_CONFIG = 'config.yml' + RUBOCOP_HOME = File.realpath(File.join(File.dirname(__FILE__), '..', '..')) + DEFAULT_FILE = File.join(RUBOCOP_HOME, 'config', 'default.yml') + + class << self + include FileFinder + + attr_writer :project_root + + def find_config_path(target_dir) + find_project_dotfile(target_dir) || find_user_dotfile || find_user_xdg_config || + DEFAULT_FILE + end + + # Returns the path RuboCop inferred as the root of the project. No file + # searches will go past this directory. + def project_root + @project_root ||= find_project_root + end + + private + + def find_project_root + pwd = Dir.pwd + gems_file = find_last_file_upwards('Gemfile', pwd) || find_last_file_upwards('gems.rb', pwd) + return unless gems_file + + File.dirname(gems_file) + end + + def find_project_dotfile(target_dir) + find_file_upwards(DOTFILE, target_dir, project_root) + end + + def find_user_dotfile + return unless ENV.key?('HOME') + + file = File.join(Dir.home, DOTFILE) + + return file if File.exist?(file) + end + + def find_user_xdg_config + xdg_config_home = expand_path(ENV.fetch('XDG_CONFIG_HOME', '~/.config')) + xdg_config = File.join(xdg_config_home, 'rubocop', XDG_CONFIG) + + return xdg_config if File.exist?(xdg_config) + end + + def expand_path(path) + File.expand_path(path) + rescue ArgumentError + # Could happen because HOME or ID could not be determined. Fall back on + # using the path literally in that case. + path + end + end + end +end diff --git a/lib/rubocop/config_loader.rb b/lib/rubocop/config_loader.rb index f65356499f7..6bbcdcf3b16 100644 --- a/lib/rubocop/config_loader.rb +++ b/lib/rubocop/config_loader.rb @@ -3,6 +3,7 @@ require 'erb' require 'yaml' require 'pathname' +require_relative 'config_finder' module RuboCop # Raised when a RuboCop configuration file is not found. @@ -15,8 +16,7 @@ class ConfigNotFoundError < Error # during a run of the rubocop program, if files in several # directories are inspected. class ConfigLoader - DOTFILE = '.rubocop.yml' - XDG_CONFIG = 'config.yml' + DOTFILE = ConfigFinder::DOTFILE RUBOCOP_HOME = File.realpath(File.join(File.dirname(__FILE__), '..', '..')) DEFAULT_FILE = File.join(RUBOCOP_HOME, 'config', 'default.yml') @@ -25,7 +25,7 @@ class << self attr_accessor :debug, :ignore_parent_exclusion, :disable_pending_cops, :enable_pending_cops, :ignore_unrecognized_cops - attr_writer :default_configuration, :project_root + attr_writer :default_configuration attr_reader :loaded_features alias debug? debug @@ -95,8 +95,7 @@ def merge(base_hash, derived_hash) # user's home directory is checked. If there's no .rubocop.yml # there either, the path to the default file is returned. def configuration_file_for(target_dir) - find_project_dotfile(target_dir) || find_user_dotfile || - find_user_xdg_config || DEFAULT_FILE + ConfigFinder.find_config_path(target_dir) end def configuration_from_file(config_file, check: true) @@ -122,7 +121,7 @@ def possible_new_cops?(config) end def add_excludes_from_files(config, config_file) - exclusion_file = find_last_file_upwards(DOTFILE, config_file, project_root) + exclusion_file = find_last_file_upwards(DOTFILE, config_file, ConfigFinder.project_root) return unless exclusion_file return if PathUtil.relative_path(exclusion_file) == PathUtil.relative_path(config_file) @@ -138,12 +137,6 @@ def default_configuration end end - # Returns the path RuboCop inferred as the root of the project. No file - # searches will go past this directory. - def project_root - @project_root ||= find_project_root - end - PENDING_BANNER = <<~BANNER The following cops were added to RuboCop, but are not configured. Please set Enabled to either `true` or `false` in your `.rubocop.yml` file. @@ -187,39 +180,6 @@ def file_path(file) File.absolute_path(file.is_a?(RemoteConfig) ? file.file : file) end - def find_project_dotfile(target_dir) - find_file_upwards(DOTFILE, target_dir, project_root) - end - - def find_project_root - pwd = Dir.pwd - gems_file = find_last_file_upwards('Gemfile', pwd) || find_last_file_upwards('gems.rb', pwd) - return unless gems_file - - File.dirname(gems_file) - end - - def find_user_dotfile - return unless ENV.key?('HOME') - - file = File.join(Dir.home, DOTFILE) - return file if File.exist?(file) - end - - def find_user_xdg_config - xdg_config_home = expand_path(ENV.fetch('XDG_CONFIG_HOME', '~/.config')) - xdg_config = File.join(xdg_config_home, 'rubocop', XDG_CONFIG) - return xdg_config if File.exist?(xdg_config) - end - - def expand_path(path) - File.expand_path(path) - rescue ArgumentError - # Could happen because HOME or ID could not be determined. Fall back on - # using the path literally in that case. - path - end - def resolver @resolver ||= ConfigLoaderResolver.new end diff --git a/lib/rubocop/result_cache.rb b/lib/rubocop/result_cache.rb index 3a876f5d590..36dd86f3651 100644 --- a/lib/rubocop/result_cache.rb +++ b/lib/rubocop/result_cache.rb @@ -4,6 +4,7 @@ require 'find' require 'etc' require 'zlib' +require_relative 'cache_config' module RuboCop # Provides functionality for caching RuboCop runs. @@ -67,24 +68,9 @@ def remove_files(files, dirs, remove_count) end def self.cache_root(config_store) - root = ENV.fetch('RUBOCOP_CACHE_ROOT', nil) - root ||= config_store.for_pwd.for_all_cops['CacheRootDirectory'] - root ||= if ENV.key?('XDG_CACHE_HOME') - # Include user ID in the path to make sure the user has write - # access. - File.join(ENV.fetch('XDG_CACHE_HOME'), Process.uid.to_s) - else - # On FreeBSD, the /home path is a symbolic link to /usr/home - # and the $HOME environment variable returns the /home path. - # - # As $HOME is a built-in environment variable, FreeBSD users - # always get a warning message. - # - # To avoid raising warn log messages on FreeBSD, we retrieve - # the real path of the home folder. - File.join(File.realpath(Dir.home), '.cache') - end - File.join(root, 'rubocop_cache') + CacheConfig.root_dir do + config_store.for_pwd.for_all_cops['CacheRootDirectory'] + end end def self.allow_symlinks_in_cache_location?(config_store) diff --git a/lib/rubocop/server/cache.rb b/lib/rubocop/server/cache.rb index e199b81f7f5..1f707466182 100644 --- a/lib/rubocop/server/cache.rb +++ b/lib/rubocop/server/cache.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true require 'pathname' +require_relative '../cache_config' +require_relative '../config_finder' # # This code is based on https://github.com/fohte/rubocop-daemon. @@ -46,9 +48,32 @@ def dir end def cache_path - cache_root_dir = cache_root_path || File.join(Dir.home, '.cache') + cache_root_dir = if cache_root_path + File.join(cache_root_path, 'rubocop_cache') + else + cache_root_dir_from_config + end - File.expand_path(File.join(cache_root_dir, 'rubocop_cache', 'server')) + File.expand_path(File.join(cache_root_dir, 'server')) + end + + def cache_root_dir_from_config + CacheConfig.root_dir do + # `RuboCop::ConfigStore` has heavy dependencies, this is a lightweight implementation + # so that only the necessary `CacheRootDirectory` can be obtained. + require 'yaml' + + config_path = ConfigFinder.find_config_path(Dir.pwd) + + # Ruby 3.1+ + config_yaml = if Gem::Version.new(Psych::VERSION) >= Gem::Version.new('4.0.0') + YAML.safe_load_file(config_path, permitted_classes: [Regexp, Symbol]) + else + YAML.load_file(config_path) + end + + config_yaml.dig('AllCops', 'CacheRootDirectory') + end end def port_path diff --git a/spec/rubocop/config_loader_spec.rb b/spec/rubocop/config_loader_spec.rb index 3975d4335c3..199a1b34623 100644 --- a/spec/rubocop/config_loader_spec.rb +++ b/spec/rubocop/config_loader_spec.rb @@ -85,14 +85,14 @@ before do # Force reload of project root - described_class.project_root = nil + RuboCop::ConfigFinder.project_root = nil create_empty_file('Gemfile') create_empty_file('../.rubocop.yml') end after do # Don't leak project root change - described_class.project_root = nil + RuboCop::ConfigFinder.project_root = nil end it 'ignores the spurious config and falls back to the provided default file if run from the project' do diff --git a/spec/rubocop/cop/generator_spec.rb b/spec/rubocop/cop/generator_spec.rb index 4d63cba18fc..f485b6f5146 100644 --- a/spec/rubocop/cop/generator_spec.rb +++ b/spec/rubocop/cop/generator_spec.rb @@ -352,7 +352,7 @@ def on_send(node) let(:config) do config = RuboCop::ConfigStore.new - path = File.join(RuboCop::ConfigLoader::RUBOCOP_HOME, RuboCop::ConfigLoader::DOTFILE) + path = File.join(RuboCop::ConfigLoader::RUBOCOP_HOME, RuboCop::ConfigFinder::DOTFILE) config.options_config = path config end diff --git a/spec/rubocop/server/cache_spec.rb b/spec/rubocop/server/cache_spec.rb index f146aeb5544..a10198168e6 100644 --- a/spec/rubocop/server/cache_spec.rb +++ b/spec/rubocop/server/cache_spec.rb @@ -3,6 +3,8 @@ RSpec.describe RuboCop::Server::Cache do subject(:cache_class) { described_class } + include_context 'cli spec behavior' + describe '.cache_path' do context 'when cache root path is not specified as default' do before do @@ -27,5 +29,181 @@ end end end + + context 'when `CacheRootDirectory` configure value is set', :isolated_environment do + context 'when cache root path is not specified path' do + let(:cache_path) { File.join('/tmp/cache-root-directory', 'rubocop_cache', 'server') } + + before do + cache_class.cache_root_path = nil + end + + it 'contains the root from `CacheRootDirectory` configure value' do + create_file('.rubocop.yml', <<~YAML) + AllCops: + CacheRootDirectory: '/tmp/cache-root-directory' + YAML + + if RuboCop::Platform.windows? + expect(cache_class.cache_path).to eq(cache_path.prepend('D:')) + else + expect(cache_class.cache_path).to eq(cache_path) + end + end + end + + context 'when cache root path is not specified path and `XDG_CACHE_HOME` environment variable is spacified' do + let(:cache_path) { File.join('/tmp/cache-root-directory', 'rubocop_cache', 'server') } + + around do |example| + cache_class.cache_root_path = nil + + ENV['XDG_CACHE_HOME'] = '/tmp/cache-from-xdg-env' + begin + example.run + ensure + ENV.delete('XDG_CACHE_HOME') + end + end + + it 'contains the root from `CacheRootDirectory` configure value' do + create_file('.rubocop.yml', <<~YAML) + AllCops: + CacheRootDirectory: '/tmp/cache-root-directory' + YAML + + if RuboCop::Platform.windows? + expect(cache_class.cache_path).to eq(cache_path.prepend('D:')) + else + expect(cache_class.cache_path).to eq(cache_path) + end + end + end + + context 'when cache root path is specified path' do + before do + cache_class.cache_root_path = '/tmp' + end + + it 'contains the root from cache root path' do + create_file('.rubocop.yml', <<~YAML) + AllCops: + CacheRootDirectory: '/tmp/cache-root-directory' + YAML + + if RuboCop::Platform.windows? + expect(cache_class.cache_path).to eq(File.join('D:/tmp', 'rubocop_cache', 'server')) + else + expect(cache_class.cache_path).to eq(File.join('/tmp', 'rubocop_cache', 'server')) + end + end + end + end + + context 'when `RUBOCOP_CACHE_ROOT` environment variable is set' do + around do |example| + ENV['RUBOCOP_CACHE_ROOT'] = '/tmp/rubocop-cache-root-env' + begin + example.run + ensure + ENV.delete('RUBOCOP_CACHE_ROOT') + end + end + + context 'when cache root path is not specified path' do + let(:cache_path) { File.join('/tmp/rubocop-cache-root-env', 'rubocop_cache', 'server') } + + before do + cache_class.cache_root_path = nil + end + + it 'contains the root from `RUBOCOP_CACHE_ROOT`' do + if RuboCop::Platform.windows? + expect(cache_class.cache_path).to eq(cache_path.prepend('D:')) + else + expect(cache_class.cache_path).to eq(cache_path) + end + end + end + + context 'when cache root path is not specified path and `XDG_CACHE_HOME` environment variable is specified' do + let(:cache_path) { File.join('/tmp/rubocop-cache-root-env', 'rubocop_cache', 'server') } + + around do |example| + cache_class.cache_root_path = nil + + ENV['XDG_CACHE_HOME'] = '/tmp/cache-from-xdg-env' + begin + example.run + ensure + ENV.delete('XDG_CACHE_HOME') + end + end + + it 'contains the root from `RUBOCOP_CACHE_ROOT`' do + if RuboCop::Platform.windows? + expect(cache_class.cache_path).to eq(cache_path.prepend('D:')) + else + expect(cache_class.cache_path).to eq(cache_path) + end + end + end + + context 'when cache root path is specified path' do + before do + cache_class.cache_root_path = '/tmp' + end + + it 'contains the root from cache root path' do + if RuboCop::Platform.windows? + expect(cache_class.cache_path).to eq(File.join('D:/tmp', 'rubocop_cache', 'server')) + else + expect(cache_class.cache_path).to eq(File.join('/tmp', 'rubocop_cache', 'server')) + end + end + end + end + + context 'when `XDG_CACHE_HOME` environment variable is set' do + around do |example| + ENV['XDG_CACHE_HOME'] = '/tmp/cache-from-xdg-env' + begin + example.run + ensure + ENV.delete('XDG_CACHE_HOME') + end + end + + context 'when cache root path is not specified path' do + let(:puid) { Process.uid.to_s } + let(:cache_path) { File.join('/tmp/cache-from-xdg-env', puid, 'rubocop_cache', 'server') } + + before do + cache_class.cache_root_path = nil + end + + it 'contains the root from `XDG_CACHE_HOME`' do + if RuboCop::Platform.windows? + expect(cache_class.cache_path).to eq(cache_path.prepend('D:')) + else + expect(cache_class.cache_path).to eq(cache_path) + end + end + end + + context 'when cache root path is specified path' do + before do + cache_class.cache_root_path = '/tmp' + end + + it 'contains the root from cache root path' do + if RuboCop::Platform.windows? + expect(cache_class.cache_path).to eq(File.join('D:/tmp', 'rubocop_cache', 'server')) + else + expect(cache_class.cache_path).to eq(File.join('/tmp', 'rubocop_cache', 'server')) + end + end + end + end end end