[ty] Allow overriding rules for specific files#18648
Conversation
4cf280f to
d5f6282
Compare
|
|
d5f6282 to
3cd3310
Compare
92926ae to
e1c3f54
Compare
|
|
||
| impl Rules { | ||
| /// Convert the rules to a `RuleSelection` with diagnostics. | ||
| pub fn to_rule_selection( |
There was a problem hiding this comment.
I only moved this code
| if include_patterns.is_empty() { | ||
| // An override with an empty include `[]` won't match any files. | ||
| let mut diagnostic = OptionDiagnostic::new( | ||
| DiagnosticId::EmptyInclude, | ||
| "Empty include doesn't match any files".to_string(), | ||
| Severity::Warning, | ||
| ) | ||
| .sub(SubDiagnostic::new( | ||
| Severity::Info, | ||
| "Please open an issue on the ty repository and share the pattern that caused the error.", | ||
| ))) | ||
| })?; | ||
|
|
||
| let mut excludes = ExcludeFilterBuilder::new(); | ||
|
|
||
| // Add the default excludes first, so that a user can override them with a negated exclude pattern. | ||
| for pattern in [ | ||
| ".bzr", | ||
| ".direnv", | ||
| ".eggs", | ||
| ".git", | ||
| ".git-rewrite", | ||
| ".hg", | ||
| ".mypy_cache", | ||
| ".nox", | ||
| ".pants.d", | ||
| ".pytype", | ||
| ".ruff_cache", | ||
| ".svn", | ||
| ".tox", | ||
| ".venv", | ||
| "__pypackages__", | ||
| "_build", | ||
| "buck-out", | ||
| "dist", | ||
| "node_modules", | ||
| "venv", | ||
| ] { | ||
| PortableGlobPattern::parse(pattern, true) | ||
| .and_then(|exclude| Ok(excludes.add(&exclude.into_absolute(""))?)) | ||
| .unwrap_or_else(|err| { | ||
| panic!( | ||
| "Expected default exclude to be valid glob but adding it failed with: {err}" | ||
| "Remove the `include` option to match all files or add a pattern to match specific files", | ||
| )); | ||
|
|
||
| // Add source annotation if we have source information | ||
| if let Some(source_file) = include_patterns.source().file() { | ||
| if let Ok(file) = system_path_to_file(db.upcast(), source_file) { | ||
| let annotation = Annotation::primary( | ||
| Span::from(file).with_optional_range(include_patterns.range()), | ||
| ) | ||
| }); | ||
| .message("This `include` option is empty"); | ||
| diagnostic = diagnostic.with_annotation(Some(annotation)); | ||
| } | ||
| } |
BurntSushi
left a comment
There was a problem hiding this comment.
I think this looks great! I feel like the semantics here are very intuitive and also very flexible. I also agree with using two lists to make for a consistent UX.
| /// An `include` glob without any patterns. | ||
| /// | ||
| /// ## Why is this bad? | ||
| /// An `include` glob without any patterns won't match any files. This is probably a mistake and |
There was a problem hiding this comment.
| /// An `include` glob without any patterns won't match any files. This is probably a mistake and | |
| /// An `include` setting without any globs won't match any files. This is probably a mistake and |
crates/ruff_macros/src/rust_doc.rs
Outdated
| .iter() | ||
| .map(|segment| &segment.ident); | ||
|
|
||
| if itertools::equal(path_idents, path) { |
There was a problem hiding this comment.
I think you can just do path_idents.eq(path) here? Via Iterator::eq.
| so e.g. `[0-9]` specifies any character between `0` and `9` inclusive. An unclosed bracket is invalid. | ||
|
|
||
| Unlike `exclude`, all paths are anchored relative to the project root (`src` only | ||
| matches `<project_root>/src` and not `<project_root>/test/src`). |
There was a problem hiding this comment.
I think this part is in contradiction with the third bullet point above.
Also, assuming this part is right and the bullet above is wrong, can you say more about why include and exclude are different in this regard? I feel like they should be treated the same?
There was a problem hiding this comment.
Only explaining why they're different. Anchoring include paths has the advantage that we can skip directories that we know can't match any pattern. However, this only works for as long as there's no pattern starting with a wildcard pattern like **/src.
The same problem doesn't exist with exclude.
Given that most users probably mean ./src when writing source in an include list and the performance optimization that anchoring allows us to do, this seemed the better default.
There was a problem hiding this comment.
Yeah hmmm. I guess that does mean users have to write **/*.pyi for an include rule when they want different rules for, say, stub files. I feel like most users are going to expect that *.pyi should work on its own. But I agree that this semantic implies there is some unavoidable costs imposed by more traversals. Because of that, I think I like what you settled on. But it might be worth highlighting in the docs somewhere that, e.g., *.pyi (or some other example) might not do what you expect.
The other thing I might suggest is if we can make exclude consistent with include? I realize that it isn't needed as an optimization, but maybe a consistent semantic across include and exclude is less confusing? If users use *.pyi to exclude stub files anywhere, then they might expect *.pyi to work in include for stub files anywhere.
There was a problem hiding this comment.
I sort of like it because it allows you to write exclude's in a more concise way. It also is the same behavior as uv's build backend (with the exception that uv doesn't anchor any exclude patterns, whereas we follow the gitignore semantic where any path with a / is anchored)
I do acknowledge that the *.pyi example might be somewhat suprising but not more than src.
There was a problem hiding this comment.
Yeah I guess I just think the consistency here we'll lead to less overall confusion, even at the expense of concision.
But I think reasonable people can disagree here and I'm happy to defer to you. :-)
There was a problem hiding this comment.
Let's hope some more folks chime in :)
There was a problem hiding this comment.
I don't have a need to weigh in on this, but since it was requested, my preference would be for consistency over concision. I think it's confusing if include and exclude work differently, and this will trip people up. I really don't think it's a problem if you have to write **/*.pyi in your exclude pattern instead of just *.pyi -- I think that's better than it working one way in include and a different way in exclude.
There was a problem hiding this comment.
I also agree with Carl and Andrew on this one!
There was a problem hiding this comment.
Alright, I'll change this in a separate PR because this behavior existed before this PR (it only surfaced in this PR because I fixed the docs)
Tests verify configuration parsing works correctly. Override rule resolution logic still needs implementation.
f5c831d to
7e60227
Compare
Summary
This PR adds a new
[[overrides]]configuration section that allows users to toggle rules at a file/path level.A file can match zero or many overrides sections. Later overrides take precedence over earlier overrides. Overrides inherit from the global configuration.
I thought long whether we should allow at most one match (always taking the last) or multiple matches. I did some ecosystem research to see how
per-file-ignoresis used. The most common pattern that I've found is too disable some rules for a sub directory and within that subdirectory, disable more rules for specific files. From airflow:This is why I ultimately decided to allow multiple matches. This complicates the implementation a bit but not too much. The main downside is that it will make it harder to warn if combining two overrides results in incompatible options because the validation happens lazily.
The other downside is that it there's no real way override an existing match other than overriding every single. E.g. when the user configuration has an
overridesthat a project wants to override. I think that this is less common and we could consider introducing an option that would mark anoverridesas the "root", so that ty doesn't apply any more overrides.Closes astral-sh/ty#178
Glob syntax
I decided to use the same
includeandexcludesyntax as we use at thesrclevel. Technically, a singleincludeoption that allows both positive and negative patterns would be enough (and negativeexcludes doesn't really make sense). However, I opted for thesrcsyntax because I think a familiar syntax is important: It woudl be confusing if we allow different glob patterns in differentincludefields.Naming
I went with
overrides, similar to what mypy uses (although mypy filters on modules not paths). I think the name is fine butoverridesare something entirely different in uv.I'm curious to hear more opinions. Alternatives are:
filessub_configurations