Skip to content

Commit

Permalink
[Fix rubocop#10842] Make server mode aware of CacheRootDirectory an…
Browse files Browse the repository at this point in the history
…d two env vars (rubocop#10852)

Fixes rubocop#10842 and follow up rubocop#10849.

This PR makes server mode aware of `CacheRootDirectory` config option value,
`RUBOCOP_CACHE_ROOT`, and `XDG_CACHE_HOME` environment variables.

`RuboCop::ConfigLoader` and `RuboCop::ResultCache` classes to get these values has many dependencies.
So, it is not suitable for use in server mode where speed is required.

This solution is to extract `ConfigFinder` class from `ConfigLoader` class and `CacheConfig` class
from `ResultCache` class.
This allows server mode to depend on lightweight `ConfigFinder` and `CacheConfig` classes.
  • Loading branch information
koic authored and WJWH committed Aug 8, 2022
1 parent ba6c8b5 commit 6ede0f9
Show file tree
Hide file tree
Showing 11 changed files with 318 additions and 71 deletions.
@@ -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][])
29 changes: 29 additions & 0 deletions 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
4 changes: 2 additions & 2 deletions lib/rubocop/cli/command/auto_genenerate_config.rb
Expand Up @@ -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'])
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion lib/rubocop/cli/command/init_dotfile.rb
Expand Up @@ -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

Expand Down
68 changes: 68 additions & 0 deletions 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
50 changes: 5 additions & 45 deletions lib/rubocop/config_loader.rb
Expand Up @@ -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.
Expand All @@ -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')

Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
22 changes: 4 additions & 18 deletions lib/rubocop/result_cache.rb
Expand Up @@ -4,6 +4,7 @@
require 'find'
require 'etc'
require 'zlib'
require_relative 'cache_config'

module RuboCop
# Provides functionality for caching RuboCop runs.
Expand Down Expand Up @@ -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)
Expand Down
29 changes: 27 additions & 2 deletions 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.
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions spec/rubocop/config_loader_spec.rb
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion spec/rubocop/cop/generator_spec.rb
Expand Up @@ -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
Expand Down

0 comments on commit 6ede0f9

Please sign in to comment.