Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Be able to run rubocop in a "nested project" without picking up config in parent directories. #11011

Open
timdiggins opened this issue Sep 23, 2022 · 8 comments

Comments

@timdiggins
Copy link

timdiggins commented Sep 23, 2022

I'm not 100% sure if this is a feature request or a bug report (see below). But here's the issue:

I have a project where I'm trying to add rubocop to a nested project (which is a dependency of the other project but can be run independently - they might be better set up as siblings, but for various reasons the "parent" project is in the parent directory). The rubocop in the nested project adds the configuration of the parent directory, including the "require" - but these are not present in the nested project.

I can exclude the nested project from the parent's rubocop (or include it -- either way are fine and controllable) however there's no way to say to rubocop - run it in this directory and don't reference the parent directory's .rubocp.yml.

Feature request or bug?

As was stated in #9879 :

... the configuration system doesn't have the concept of project root, only of ancestor directories

However #8314 seems to imply there is a concept of project root (and in fact that it shouldn't look in parent directories for .rubocop.yml)

I've used the sections from both feature request and bug report to cover all the bases 😀

Describe the solution you'd like

I think rubocop should have some way of differentiating between traversing down to pick up new configs from a directory (which might well be its own project - it's up to the parent to sensibly include or exclude those directories) and traversing up to look for parent project's config.

When you invoke rubocop in a directory that has a .rubocop.yml and that is recognised as a project root (by the same tokens that bundler does as per #8314

There's would then be a difference between

  • running rubocop nested-project and
  • cd nested-project; rubocop`

Expected behavior

I've documented a toy example in https://github.com/timdiggins/rubocop-nested-test

The top level directory has a .rubocop.yml and excludes the nested-project.
It requires 'rspec-performance', and the Gemfile specifies it also.

When you run rubocop in the top directory it works as expected.

The nested-project directory represents a project nested within the top level project (see below for why it might occur).

The nested-project's .rubocop.yml does not require 'rspec-performance', and the Gemfile does not specify it.

Expectation: cd nested-project; rubocop would run without error, using just the configuration from the nested-project/.rubocop.yml

Actual behavior

if you run rubocop in the nested-project directory, it fails because rspec-performance gem can't be found.

output of `bundle exec rubocop --debug` from nested-project
For /rubocop-nested-test/nested-project: configuration from /rubocop-nested-test/nested-project/.rubocop.yml
Default configuration from ...../gems/rubocop-1.36.0/config/default.yml
AllCops/Exclude configuration from /rubocop-nested-test/.rubocop.yml
cannot load such file -- rubocop-performance
...../gems/rubocop-1.36.0/lib/rubocop/feature_loader.rb:46:in `rescue in rescue in load'
...../gems/rubocop-1.36.0/lib/rubocop/feature_loader.rb:39:in `rescue in load'
...../gems/rubocop-1.36.0/lib/rubocop/feature_loader.rb:32:in `load'
...../gems/rubocop-1.36.0/lib/rubocop/feature_loader.rb:21:in `load'
...../gems/rubocop-1.36.0/lib/rubocop/config_loader_resolver.rb:14:in `block (2 levels) in resolve_requires'
...../gems/rubocop-1.36.0/lib/rubocop/config_loader_resolver.rb:13:in `each'
...../gems/rubocop-1.36.0/lib/rubocop/config_loader_resolver.rb:13:in `block in resolve_requires'
...../gems/rubocop-1.36.0/lib/rubocop/config_loader_resolver.rb:12:in `tap'
...../gems/rubocop-1.36.0/lib/rubocop/config_loader_resolver.rb:12:in `resolve_requires'
...../gems/rubocop-1.36.0/lib/rubocop/config_loader.rb:46:in `load_file'
...../gems/rubocop-1.36.0/lib/rubocop/config_loader.rb:130:in `add_excludes_from_files'
...../gems/rubocop-1.36.0/lib/rubocop/config_loader.rb:110:in `configuration_from_file'
...../gems/rubocop-1.36.0/lib/rubocop/config_store.rb:68:in `for_dir'
...../gems/rubocop-1.36.0/lib/rubocop/config_store.rb:47:in `for_pwd'
...../gems/rubocop-1.36.0/lib/rubocop/cli.rb:99:in `parallel_by_default!'
...../gems/rubocop-1.36.0/lib/rubocop/cli.rb:46:in `run'
...../gems/rubocop-1.36.0/exe/rubocop:19:in `block in <top (required)>'
...../lib/ruby/2.7.0/benchmark.rb:308:in `realtime'
...../gems/rubocop-1.36.0/exe/rubocop:19:in `<top (required)>'
...../bin/rubocop:25:in `load'
...../bin/rubocop:25:in `<main>'
...../bin/ruby_executable_hooks:22:in `eval'
...../bin/ruby_executable_hooks:22:in `<main>'

Steps to reproduce the problem

see above or see https://github.com/timdiggins/rubocop-nested-test

RuboCop version

╰─ bundle exec rubocop -V                                                                                              ─╯
1.36.0 (using Parser 3.1.2.1, rubocop-ast 1.21.0, running on ruby 2.7.5) [x86_64-darwin21]
  - rubocop-performance 1.15.0

Workarounds:

The main workaround is to run rubocop only from the parent project, and allow the nested-project to define a .rubocop.yml overriding config as and when appropriate, and never running rubocop when in the nested-project(and probably excluding rubocop from the nested-project's Gemfile).

@tdeo
Copy link
Contributor

tdeo commented Oct 22, 2022

A similar discussion about running rubocop both from a root project and from nested projects happened in #4328, the suggested technique would be to either:

  • run bundle exec rubocop in the nested project directory (which already works as you intend)
  • run bundle exec rubocop --ignore-parent-exclusion nested-project/ from the root project directory, which will make rubocop only pick up the configuration in nested-project instead of recursing to the current directory.

I think that will support your use-case. However, I'm also afraid that you may run into surprising issues when running bundle exec rubocop nested-project/ from the root project directory, since that would be picking up the rubocop version of root's Gemfile instead of nested-project's Gemfile version, which might well be incompatible with the configuration file nested-project/.rubocop.yml

@timdiggins
Copy link
Author

timdiggins commented Oct 22, 2022

Thanks for response @tdeo it led me to the right answer, but it still feels like a workaround.

bundle exec rubocop in the nexted project directory doesn't work on it's own-- I'm still getting a complaint about rubocop-performance not being found in the Gemfile (see below).

However if I do rubocop --ignore-parent-exclusion then it will work.

The problem is that the scan for the parent AllCops/Exclude leads to rubocop trying to require rubocop-performance (in the require array) from the parent directory's config, even though this is unnecessary for the AllCops/Exclude and not supported in the child's (source) Gemfile).

So this works -- but feels like a workaround


I tried using https://github.com/timdiggins/rubocop-nested-test: cd nested-project; bundle exec rubocop
I think this is functionally equivalent to running the binstub bin/rubocop within nested-project

output (with --debug flag)

$ bundle exec rubocop --debug                                                                                                         ─╯
For /Volumes/data/t/rubocop-nested-test/nested-project: configuration from /Volumes/data/t/rubocop-nested-test/nested-project/.rubocop.yml
Default configuration from /Users/t/.rvm/gems/ruby-2.7.5@rubocop/gems/rubocop-1.36.0/config/default.yml
AllCops/Exclude configuration from /Volumes/data/t/rubocop-nested-test/.rubocop.yml
cannot load such file -- rubocop-performance
/Users/t/.rvm/gems/ruby-2.7.5@rubocop/gems/rubocop-1.36.0/lib/rubocop/feature_loader.rb:46:in `rescue in rescue in load'
/Users/t/.rvm/gems/ruby-2.7.5@rubocop/gems/rubocop-1.36.0/lib/rubocop/feature_loader.rb:39:in `rescue in load'
/Users/t/.rvm/gems/ruby-2.7.5@rubocop/gems/rubocop-1.36.0/lib/rubocop/feature_loader.rb:32:in `load'
/Users/t/.rvm/gems/ruby-2.7.5@rubocop/gems/rubocop-1.36.0/lib/rubocop/feature_loader.rb:21:in `load'
/Users/t/.rvm/gems/ruby-2.7.5@rubocop/gems/rubocop-1.36.0/lib/rubocop/config_loader_resolver.rb:14:in `block (2 levels) in resolve_requires'
/Users/t/.rvm/gems/ruby-2.7.5@rubocop/gems/rubocop-1.36.0/lib/rubocop/config_loader_resolver.rb:13:in `each'
/Users/t/.rvm/gems/ruby-2.7.5@rubocop/gems/rubocop-1.36.0/lib/rubocop/config_loader_resolver.rb:13:in `block in resolve_requires'
/Users/t/.rvm/gems/ruby-2.7.5@rubocop/gems/rubocop-1.36.0/lib/rubocop/config_loader_resolver.rb:12:in `tap'
/Users/t/.rvm/gems/ruby-2.7.5@rubocop/gems/rubocop-1.36.0/lib/rubocop/config_loader_resolver.rb:12:in `resolve_requires'
/Users/t/.rvm/gems/ruby-2.7.5@rubocop/gems/rubocop-1.36.0/lib/rubocop/config_loader.rb:46:in `load_file'
/Users/t/.rvm/gems/ruby-2.7.5@rubocop/gems/rubocop-1.36.0/lib/rubocop/config_loader.rb:130:in `add_excludes_from_files'
/Users/t/.rvm/gems/ruby-2.7.5@rubocop/gems/rubocop-1.36.0/lib/rubocop/config_loader.rb:110:in `configuration_from_file'
/Users/t/.rvm/gems/ruby-2.7.5@rubocop/gems/rubocop-1.36.0/lib/rubocop/config_store.rb:68:in `for_dir'
/Users/t/.rvm/gems/ruby-2.7.5@rubocop/gems/rubocop-1.36.0/lib/rubocop/config_store.rb:47:in `for_pwd'
/Users/t/.rvm/gems/ruby-2.7.5@rubocop/gems/rubocop-1.36.0/lib/rubocop/cli.rb:99:in `parallel_by_default!'
/Users/t/.rvm/gems/ruby-2.7.5@rubocop/gems/rubocop-1.36.0/lib/rubocop/cli.rb:46:in `run'
/Users/t/.rvm/gems/ruby-2.7.5@rubocop/gems/rubocop-1.36.0/exe/rubocop:19:in `block in <top (required)>'
/Users/t/.rvm/rubies/ruby-2.7.5/lib/ruby/2.7.0/benchmark.rb:308:in `realtime'
/Users/t/.rvm/gems/ruby-2.7.5@rubocop/gems/rubocop-1.36.0/exe/rubocop:19:in `<top (required)>'
/Users/t/.rvm/gems/ruby-2.7.5@rubocop/bin/rubocop:25:in `load'
/Users/t/.rvm/gems/ruby-2.7.5@rubocop/bin/rubocop:25:in `<main>'
/Users/t/.rvm/gems/ruby-2.7.5@rubocop/bin/ruby_executable_hooks:22:in `eval'
/Users/t/.rvm/gems/ruby-2.7.5@rubocop/bin/ruby_executable_hooks:22:in `<main>'

@tdeo
Copy link
Contributor

tdeo commented Oct 22, 2022

That's indeed surprising, the implementation of the logic for finding the project root (which is the place up to where rubocop includes configuration recursively), as being the parent-most directory containing a Gemfile (or the parent-most folder containing a gems.rb as a fallback)

The commit setting that behavior is e1a2da0e28, but unfortunately lacks a bit of context about the reason behind the change (except excluding the current_user's home directory).

My first thought would rather be that rubocop should consider as project root the first ancestor of pwd which contains a Gemfile or gems.rb, since that would be the project bundle takes into account to run the command.
I believe that actually gives a bit more freedom to users, in case of a single-project with nested rubocop.yml files for specific folders, things would still work the same way as currently, and in case of nested-projects (typically with git submodules), the behavior would become opt-in by explicitly adding an inherit_from: [../.rubocop.yml] into the sub-project configuration.

Maybe @bbatsov can give more context about the original choice and his vision of the topic

@halilim
Copy link

halilim commented Mar 7, 2023

I have a Gemfile with Rubocop and its various extensions, and a .rubocop.yml at ~. I use them for small scripts or one-off code that I write without a project/Gemfile. But in another project/folder with a Gemfile, Rubocop fails unless I have the exact set of extensions both at ~ and in the project.

I think it should not inherit requires if there is a Gemfile between the inherited config and the current folder since that Gemfile might not have the extensions of the higher-level folder.

Ideally, though, I'd like to disable inheriting from the global config (actually, expect it to be off by default). Because my personal configuration may or may not make sense for a particular project.

@brphelps
Copy link

I just ran into this behavior myself. We have a separate project in a subfolder that has its own rubocop configuration and the parent folder has a required rubocop gem we don't want in the subfolder (the gem has no requirement).

I think at the minimum I'd really like the ability to control this behavior in the .rubocop.yml itself so I don't need to manage the different places that might be invoking rubocop, including the vs code extension, with the command line argument --ignore-parent-exclusion.

Does this seem like a reasonable approach? I don't mind getting a PR together, just want to make sure the approach makes sense to you folks before getting started.

@babelfish
Copy link

babelfish commented Aug 31, 2023

I've run into this too. I have a gem I'm working on inside a rails app directory for ease of development with the plan to extract it later. Nothing I have tried will stop rubocop from trying to include the rails app's .rubocop.yml, and the only way I can get the rubocop integration in my neovim setup to work is to rename the rails app's .rubocop.yml so rubocop can't find it. --ignore-parent-exclusion doesn't help, specifying the exact config file with -c doesn't help.

Rubocop automatically including other additional configuration files outside of the working directory (when one already exists there) is extremely unintuitive, and being unable to stop it from doing so is frankly very irritating.

@davebenvenuti
Copy link

davebenvenuti commented Sep 17, 2023

Another workaround that's worked for me if you typically call rubocop as a rake task is the following mod to my Rakefile:

  require "rubocop/rake_task"
+ require "rubocop/config_finder"
+
+ RuboCop::ConfigFinder.project_root = File.expand_path(".")

  RuboCop::RakeTask.new

Solves it for rake rubocop or bundle exec rake rubocop but not rubocop or bundle exec rubocop.

@lain0
Copy link

lain0 commented Apr 27, 2024

rubocop ./spec -c /dev/null

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants