ruff icon indicating copy to clipboard operation
ruff copied to clipboard

bug: child configs that use 'select' or 'extend-select' invalidate the parent config's 'ignore' list.

Open Hnasar opened this issue 1 year ago • 6 comments

Bug/Use-case

As someone who helps maintain a monorepo with many subprojects and child configs, I want to be able to maintain a centralized configuration of ignored rules (which projects may override with tool.ruff.extend). There appears to be a bug where the top-level tool.ruff.lint.ignore configuration isn't honored whenever a child config modifies either tool.ruff.lint.select or tool.ruff.lint.extend-select. This occurs all the time, even if the child config HAS NOT overridden tool.ruff.lint.ignore in any way.

I (think I) understand the semantics of ruff.extend alongside lint.extend-select and lint.select but the semantics of the now-combined lint.ignore create some really confusing behaviors. As a result, I don't have a good way to fulfill my use-case mentioned above.

Repro

Using ruff 0.3.4

$ tree
.
└── parent
    ├── child
    │   ├── foo.py
    │   └── pyproject.toml
    └── pyproject.toml
# child/foo.py
import os, re

"{}".format(1)

Confusing behavior:

0. Parent ignores with child select

# parent/pyproject.toml
[tool.ruff]
lint.select = ["E", "UP"]
lint.ignore = ["E401", "UP032"]
# child/pyproject.toml
[tool.ruff]
extend = "../pyproject.toml"
lint.extend-select = ["UP"]  # also same behavior if we use lint.select ["UP"]

Observed:

$ ruff check .
parent/child/foo.py:3:1: UP032 [*] Use f-string instead of `format` call

Expected:

I expect that the parent ignore list is used in all cases except when the child has its own ignore list.

$ ruff check .
All checks passed!

1. Parent ignores with child select and child ignore

# parent/pyproject.toml
[tool.ruff]
lint.select = ["E", "UP"]
lint.ignore = ["E401", "UP032"]
# child/pyproject.toml
[tool.ruff]
extend = "../pyproject.toml"
lint.extend-select = ["UP"]  # also same behavior if we use lint.select ["UP"]
lint.ignore = ["UP032"]

Observed:

$ ruff check .
All checks passed!

Expected:

I expect that if I provide a child ignore list then it fully invalidates the parent ignore list.

$ ruff check .
parent/child/foo.py:1:1: E401 [*] Multiple imports on one line

Workarounds

Neither of these are ideal, but are some things I tried:

  1. Copy and paste the parent's lint.ignore and keep it in sync in all the child configs. This is cumbersome to keep in sync and easy to miss things.
  2. Instead of using select or extend-select in child configs, I can invert this and ignore all the top-level selects that a project wants to opt-out of. This is confusing, and also is cumbersome and easy to miss.

Examples of fine behavior that makes sense

Click to expand examples

2. No ignores

# parent/pyproject.toml
[tool.ruff]
lint.select = ["E", "UP"]
# child/pyproject.toml
[tool.ruff]
extend = "../pyproject.toml"
$ ruff  check .
parent/child/foo.py:1:1: E401 [*] Multiple imports on one line
parent/child/foo.py:3:1: UP032 [*] Use f-string instead of `format` call

3. Child select

# parent/pyproject.toml
[tool.ruff]
lint.select = ["E", "UP"]
# child/pyproject.toml
[tool.ruff]
extend = "../pyproject.toml"
lint.select = ["UP"]
$ ruff  check .
parent/child/foo.py:3:1: UP032 [*] Use f-string instead of `format` call

4. Child extend-select

# parent/pyproject.toml
[tool.ruff]
lint.select = ["E", "UP"]
# child/pyproject.toml
[tool.ruff]
extend = "../pyproject.toml"
lint.extend-select = ["UP"]
$ ruff  check .
parent/child/foo.py:1:1: E401 [*] Multiple imports on one line
parent/child/foo.py:3:1: UP032 [*] Use f-string instead of `format` call

5. Parent ignores without any child select

# parent/pyproject.toml
[tool.ruff]
lint.select = ["E", "UP"]
lint.ignore = ["E401", "UP032"]
# child/pyproject.toml
[tool.ruff]
extend = "../pyproject.toml"
ruff  check .
All checks passed!

Hnasar avatar Mar 26 '24 21:03 Hnasar

So for "0. Parent ignores with child select", to the best of my recollection, I thought a child select was intended to "reset" the rule selection (which tends to create more intuitive behavior), but an extend-select was not supposed to reset. So your example there should work as you expected. I'll have to test it out myself and see what's up.

charliermarsh avatar Mar 26 '24 22:03 charliermarsh

Here's a ZIP with the described repo. I10622.zip

MichaReiser avatar Mar 27 '24 08:03 MichaReiser

I can take a look at this tonight.

charliermarsh avatar Mar 27 '24 16:03 charliermarsh

On further review this is working as expected but my previous explanation was missing one piece...

When you use extends, we resolve the list of resolves one configuration file at a time. So, we first resolve all of the selectors in parent/pyproject.toml, and come up with a list of rules. Then, we apply the selectors in child/pyproject.toml on top of that list. So if you have lint.extend-select = ["UP"] in your child/pyproject.toml, it will indeed then add all the UP rules and ignore the UP032 ignore from parent/pyproject.toml. However, note that E401 is still disabled.

charliermarsh avatar Mar 30 '24 17:03 charliermarsh

Thanks for the explanation of how it works with the file-by-file resolution, because I was really confused because I thought the 'most specific option wins' was the standard semantics.

So then for my initial user story, is there any supported way to globally ignore some rules when there's a tree of configs? If we had a tool.ruff.lint.ignore that overrode any inherited ignores, and a tool.ruff.lint.extend-ignore which added to any inherited ignores, that would give me the tools I need to fix things, but otherwise I'm not sure.

Hnasar avatar Apr 01 '24 15:04 Hnasar

I think it would be good for us to document this behavior because it wasn't clear to me.

MichaReiser avatar Apr 02 '24 09:04 MichaReiser

This would be quite problematic with shared configs: https://github.com/astral-sh/ruff/issues/12352

Avasam avatar Aug 19 '24 05:08 Avasam