Skip to content

Commit

Permalink
Add support for XDG directory specification
Browse files Browse the repository at this point in the history
Users now have an additional place they can have their own config file,
if they prefer to not have dotfiles inside their home directory.

Based on initial work by @tejasbubane in #6682.
  • Loading branch information
Mange authored and bbatsov committed Apr 15, 2019
1 parent 973e0e6 commit 6fe5956
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 64 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Expand Up @@ -19,6 +19,10 @@
* [#6903](https://github.com/rubocop-hq/rubocop/issues/6903): Handle variables prefixed with `_` in `Naming/RescuedExceptionsVariableName` cop. ([@anthony-robin][])
* [#6917](https://github.com/rubocop-hq/rubocop/issues/6917): Bump Bundler dependency to >= 1.15.0. ([@koic][])

### New features

* [#6895](https://github.com/rubocop-hq/rubocop/pull/6895): Add support for XDG config home for user-config. ([@Mange][], [@tejasbubane][])

## 0.67.2 (2019-04-05)

### Bug fixes
Expand Down Expand Up @@ -3943,3 +3947,4 @@
[@XrXr]: https://github.com/XrXr
[@thomthom]: https://github.com/thomthom
[@Blue-Pix]: https://github.com/Blue-Pix
[@Mange]: https://github.com/Mange
36 changes: 34 additions & 2 deletions lib/rubocop/config_loader.rb
Expand Up @@ -15,6 +15,7 @@ class ConfigNotFoundError < Error
# directories are inspected.
class ConfigLoader
DOTFILE = '.rubocop.yml'.freeze
XDG_CONFIG = 'config.yml'.freeze
RUBOCOP_HOME = File.realpath(File.join(File.dirname(__FILE__), '..', '..'))
DEFAULT_FILE = File.join(RUBOCOP_HOME, 'config', 'default.yml')
AUTO_GENERATED_FILE = '.rubocop_todo.yml'.freeze
Expand Down Expand Up @@ -75,7 +76,10 @@ 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_file_upwards(DOTFILE, target_dir, use_home: true) || DEFAULT_FILE
find_project_dotfile(target_dir) ||
find_user_dotfile ||
find_user_xdg_config ||
DEFAULT_FILE
end

def configuration_from_file(config_file)
Expand All @@ -91,7 +95,10 @@ def configuration_from_file(config_file)
end

def add_excludes_from_files(config, config_file)
found_files = find_files_upwards(DOTFILE, config_file, use_home: true)
found_files =
find_files_upwards(DOTFILE, config_file) +
[find_user_dotfile, find_user_xdg_config].compact

return if found_files.empty?
return if PathUtil.relative_path(found_files.last) ==
PathUtil.relative_path(config_file)
Expand Down Expand Up @@ -139,6 +146,31 @@ def add_inheritance_from_auto_generated_file

private

def find_project_dotfile(target_dir)
find_file_upwards(DOTFILE, target_dir)
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 existing_configuration(config_file)
IO.read(config_file, encoding: Encoding::UTF_8)
.sub(%r{^inherit_from: *[.\/\w]+}, '')
Expand Down
15 changes: 5 additions & 10 deletions lib/rubocop/file_finder.rb
Expand Up @@ -13,35 +13,30 @@ def self.root_level?(path)
@root_level == path.to_s
end

def find_file_upwards(filename, start_dir, use_home: false)
traverse_files_upwards(filename, start_dir, use_home) do |file|
def find_file_upwards(filename, start_dir)
traverse_files_upwards(filename, start_dir) do |file|
# minimize iteration for performance
return file if file
end
end

def find_files_upwards(filename, start_dir, use_home: false)
def find_files_upwards(filename, start_dir)
files = []
traverse_files_upwards(filename, start_dir, use_home) do |file|
traverse_files_upwards(filename, start_dir) do |file|
files << file
end
files
end

private

def traverse_files_upwards(filename, start_dir, use_home)
def traverse_files_upwards(filename, start_dir)
Pathname.new(start_dir).expand_path.ascend do |dir|
break if FileFinder.root_level?(dir)

file = dir + filename
yield(file.to_s) if file.exist?
end

return unless use_home && ENV.key?('HOME')

file = File.join(Dir.home, filename)
yield(file) if File.exist?(file)
end
end
end
3 changes: 3 additions & 0 deletions lib/rubocop/rspec/shared_contexts.rb
Expand Up @@ -7,6 +7,7 @@
around do |example|
Dir.mktmpdir do |tmpdir|
original_home = ENV['HOME']
original_xdg_config_home = ENV['XDG_CONFIG_HOME']

# Make sure to expand all symlinks in the path first. Otherwise we may
# get mismatched pathnames when loading config files later on.
Expand All @@ -19,6 +20,7 @@
virtual_home = File.expand_path(File.join(tmpdir, 'home'))
Dir.mkdir(virtual_home)
ENV['HOME'] = virtual_home
ENV.delete('XDG_CONFIG_HOME')

working_dir = File.join(tmpdir, 'work')
Dir.mkdir(working_dir)
Expand All @@ -28,6 +30,7 @@
end
ensure
ENV['HOME'] = original_home
ENV['XDG_CONFIG_HOME'] = original_xdg_config_home

RuboCop::FileFinder.root_level = nil
end
Expand Down
43 changes: 35 additions & 8 deletions manual/configuration.md
Expand Up @@ -4,10 +4,8 @@ The behavior of RuboCop can be controlled via the
[.rubocop.yml](https://github.com/rubocop-hq/rubocop/blob/master/.rubocop.yml)
configuration file. It makes it possible to enable/disable certain cops
(checks) and to alter their behavior if they accept any parameters. The file
can be placed either in your home directory or in some project directory.

RuboCop will start looking for the configuration file in the directory
where the inspected file is and continue its way up to the root directory.
can be placed in your home directory, XDG config directory, or in some project
directory.

The file has the following format:

Expand All @@ -26,6 +24,34 @@ Metrics/LineLength:
Qualifying cop name with its type, e.g., `Style`, is recommended,
but not necessary as long as the cop name is unique across all types.

### Config file locations

RuboCop will start looking for the configuration file in the directory
where the inspected file is and continue its way up to the root directory.

If it cannot be found until reaching the project's root directory, then it will
be searched for in the user's global config locations, which consists of a
dotfile or a config file inside the [XDG Base Directory
specification][xdg-basedir-spec].

* `~/.rubocop.yml`
* `$XDG_HOME/rubocop/config.yml` (expands to `~/.config/rubocop/config.yml` if
`$XDG_CONFIG_HOME` is not set)

If both files exist, the dotfile will be selected.

As an example, if RuboCop is invoked from inside `/path/to/project/lib/utils`,
then RuboCop will use the config as specified inside the first of the following
files:

* `/path/to/project/lib/utils/.rubocop.yml`
* `/path/to/project/lib/.rubocop.yml`
* `/path/to/project/.rubocop.yml`
* `/.rubocop.yml`
* `~/.rubocop.yml`
* `~/.config/rubocop/config.yml`
* [RuboCop's default configuration][1]

### Inheritance

All configuration inherits from [RuboCop's default configuration][1] (See
Expand Down Expand Up @@ -215,9 +241,9 @@ In this example the `Exclude` would only include `bar.rb`.

The file [config/default.yml][1] under the RuboCop home directory contains the
default settings that all configurations inherit from. Project and personal
`.rubocop.yml` files need only make settings that are different from the default
ones. If there is no `.rubocop.yml` file in the project or home directory,
`config/default.yml` will be used.
`.rubocop.yml` files need only make settings that are different from the
default ones. If there is no `.rubocop.yml` file in the project, home or XDG
directories, `config/default.yml` will be used.

### Including/Excluding files

Expand Down Expand Up @@ -424,7 +450,7 @@ Metrics/LineLength:
compromise.
```

These details will only be seen when rubocop is run with the `--extra-details` flag or if `ExtraDetails` is set to true in your global rubocop configuration.
These details will only be seen when rubocop is run with the `--extra-details` flag or if `ExtraDetails` is set to true in your global rubocop configuration.

#### AutoCorrect

Expand Down Expand Up @@ -521,3 +547,4 @@ for x in (0..19) # rubocop:disable Style/For
```

[1]: https://github.com/rubocop-hq/rubocop/blob/master/config/default.yml
[xdg-basedir-spec]: https://specifications.freedesktop.org/basedir-spec/latest/index.html
36 changes: 35 additions & 1 deletion spec/rubocop/config_loader_spec.rb
Expand Up @@ -33,7 +33,41 @@
end
end

context 'and no config file exists in home directory' do
context 'but a config file exists in default XDG config directory' do
before { create_empty_file('~/.config/rubocop/config.yml') }

it 'returns the path to the file in XDG directory' do
expect(configuration_file_for).to end_with(
'home/.config/rubocop/config.yml'
)
end
end

context 'but a config file exists in a custom XDG config directory' do
before do
ENV['XDG_CONFIG_HOME'] = '~/xdg-stuff'
create_empty_file('~/xdg-stuff/rubocop/config.yml')
end

it 'returns the path to the file in XDG directory' do
expect(configuration_file_for).to end_with(
'home/xdg-stuff/rubocop/config.yml'
)
end
end

context 'but a config file exists in both home and XDG directories' do
before do
create_empty_file('~/.config/rubocop/config.yml')
create_empty_file('~/.rubocop.yml')
end

it 'returns the path to the file in home directory' do
expect(configuration_file_for).to end_with('home/.rubocop.yml')
end
end

context 'and no config file exists in home or XDG directory' do
it 'falls back to the provided default file' do
expect(configuration_file_for).to end_with('config/default.yml')
end
Expand Down
43 changes: 0 additions & 43 deletions spec/rubocop/file_finder_spec.rb
Expand Up @@ -21,26 +21,6 @@
it 'returns nil when file is not found' do
expect(finder.find_file_upwards('file2', 'dir')).to be(nil)
end

context 'when given `use_home` option' do
before { create_empty_file(File.join(Dir.home, 'file2')) }

context 'and a file exists in home directory' do
it 'returns the file' do
expect(finder.find_file_upwards('file2', 'dir', use_home: true))
.to eq(File.expand_path('file2', Dir.home))
end
end

context 'but no `HOME` in `ENV`' do
before { ENV.delete('HOME') }

it 'returns nil' do
expect(finder.find_file_upwards('file2', 'dir', use_home: true))
.to be(nil)
end
end
end
end

describe '#find_files_upwards' do
Expand All @@ -53,28 +33,5 @@
it 'returns an empty array when file is not found' do
expect(finder.find_files_upwards('xyz', 'dir')).to eq([])
end

context 'when given `use_home` option' do
before { create_empty_file(File.join(Dir.home, 'file')) }

context 'and a file exists in home directory' do
it 'returns an array including the file' do
expect(finder.find_files_upwards('file', 'dir', use_home: true))
.to eq([File.expand_path('file', 'dir'),
File.expand_path('file'),
File.expand_path('file', Dir.home)])
end
end

context 'but no `HOME` in `ENV`' do
before { ENV.delete('HOME') }

it 'returns an array not including the file' do
expect(finder.find_files_upwards('file', 'dir', use_home: true))
.to eq([File.expand_path('file', 'dir'),
File.expand_path('file')])
end
end
end
end
end

0 comments on commit 6fe5956

Please sign in to comment.