ruff
ruff copied to clipboard
bug: child configs that use 'select' or 'extend-select' invalidate the parent config's 'ignore' list.
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:
- Copy and paste the parent's
lint.ignoreand keep it in sync in all the child configs. This is cumbersome to keep in sync and easy to miss things. - Instead of using
selectorextend-selectin child configs, I can invert this andignoreall the top-levelselectsthat 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!
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.
Here's a ZIP with the described repo. I10622.zip
I can take a look at this tonight.
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.
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.
I think it would be good for us to document this behavior because it wasn't clear to me.
This would be quite problematic with shared configs: https://github.com/astral-sh/ruff/issues/12352