Add lint rule for calling chmod with non-octal integers#18541
Add lint rule for calling chmod with non-octal integers#18541MichaReiser merged 6 commits intoastral-sh:mainfrom
Conversation
|
| code | total | + violation | - violation | + fix | - fix |
|---|---|---|---|---|---|
| RUF064 | 1 | 1 | 0 | 0 | 0 |
MichaReiser
left a comment
There was a problem hiding this comment.
Thank you.
This mostly looks good. My main feedback is that I don't think we should offer a fix here.
crates/ruff_linter/src/codes.rs
Outdated
| (Ruff, "058") => (RuleGroup::Preview, rules::ruff::rules::StarmapZip), | ||
| (Ruff, "059") => (RuleGroup::Preview, rules::ruff::rules::UnusedUnpackedVariable), | ||
| (Ruff, "060") => (RuleGroup::Preview, rules::ruff::rules::InEmptyCollection), | ||
| (Ruff, "064") => (RuleGroup::Preview, rules::ruff::rules::ChmodNonOctal), |
There was a problem hiding this comment.
I do like clippy's naming because it leaves the door open to lint for other cases than just chmod (if there are any)
There was a problem hiding this comment.
I'm definitely open to naming suggestions. My only gripe with the phrase "unix permissions" is that this API is available on Windows in Python (albeit only the 0o400 and 0o200 flags do something)
There was a problem hiding this comment.
I think NonOctalPermissions would be fine (just dropping the unix part from clippy's name). As Micha said, it looks like many of the os methods take a mode argument, so we could reuse a more generally-named rule for those too.
| if let Reason::Decimal { suggested, .. } = reason { | ||
| let edit = Edit::range_replacement(format!("{suggested:#o}"), mode_arg.range()); | ||
| diagnostic.set_fix(Fix::applicable_edit(edit, Applicability::Unsafe)); |
There was a problem hiding this comment.
I don't think we should offer a fix here. The main promise of the rule is that the code is probably incorrect and just rewriting the code from decimal to octal doesn't fix the root problem -> That the code is probably just incorrect.
I'd be okay if we allowlisted some common chmods and offered rewriting them but I don't think we should offer rewriting in general.
| fn message(&self) -> String { | ||
| match self.reason { | ||
| Reason::Decimal { found, suggested } => { | ||
| format!("Non-octal mode `{found}`, did you mean `{suggested:#o}`?") |
There was a problem hiding this comment.
See my comment below. I don't think we should offer a suggestion here because the promise of the rule is that we believe that the used decimal permissions are actually wrong and simply rewriting to octal doesn't fix that.
|
@ntBre reached out to me on Discord and I think I could be persuaded that we should offer a fix (but I would remove the suggestion from the lint message, given that the fix will show the mode). I mainly focused on existing (working) code. The fix would be incorrect in those cases and is probably going to break the user code. This seems fine, given that the fix is marked as unsafe. What I think is the main promise of the rule is that it catches an incorrect permission when you're writing new code. Providing a fix seems very useful in those cases. And I think that should work well because we do show unsafe fixes in the IDE as code actions that users can apply manually. |
|
I also hemmed and hawed about whether to include a fix, and whether it should be limited to certain common cases (e.g. A public code search is interesting. There definitely are cases where a correct decimal integer is used, and often but not always there is a comment of the equivalent octal. This may stem back to code bases that supported Python 2.5 and Python 3 (octal syntax E.g. the pytest test suite contains |
MichaReiser
left a comment
There was a problem hiding this comment.
Okay, I think we can go with a fix and see whether users find it useful.
I've a few smaller remarks.
| /// | ||
| /// ## Fix safety | ||
| /// | ||
| /// This rule's fix is marked as unsafe because it changes runtime behavior. |
There was a problem hiding this comment.
It would be great if we could be more concrete here. I think the airflow example that you mentioned would be a great example
| checker.report_diagnostic(NonOctalPermissions { reason }, mode_arg.range()); | ||
| if let Reason::Decimal { suggested, .. } = reason { | ||
| let edit = Edit::range_replacement(format!("{suggested:#o}"), mode_arg.range()); | ||
| diagnostic.set_fix(Fix::applicable_edit(edit, Applicability::Unsafe)); |
There was a problem hiding this comment.
| diagnostic.set_fix(Fix::applicable_edit(edit, Applicability::Unsafe)); | |
| diagnostic.set_fix(Fix::unsafe_edit(edit)); |
| fn message(&self) -> String { | ||
| match self.reason { | ||
| Reason::Decimal { found, suggested } => { | ||
| format!("Non-octal mode `{found}`, did you mean `{suggested:#o}`?") |
There was a problem hiding this comment.
I'd remove the suggested as it is also visible in the fix
23dcc33 to
3bebfc1
Compare
|
|
||
| #[derive_message_formats] | ||
| fn message(&self) -> String { | ||
| "Non-octal mode".to_string() |
There was a problem hiding this comment.
How would you feel about showing a representation of the mode as written e.g.
t.py:3:17: RUF064 Non-octal mode (u=rw,g=w,o=)
|
1 | import os
2 |
3 | os.chmod("foo", 400)
| ^^^ RUF064
4 | os.chmod("foo", 256)
|
= help: Replace with octal literal
t.py:4:17: RUF064 Non-octal mode (u=r,go=)
|
3 | os.chmod("foo", 400)
4 | os.chmod("foo", 256)
| ^^^ RUF064
|
= help: Replace with octal literal
There was a problem hiding this comment.
I like the idea but I think it's confusing in the fix message because it gives the impression that u=rw, ... was the non octal mode, which it wasn't.
I think this is a case where sub diagnostics (@ntBre is working on this) would be very useful. You could then add two hints (needs better wording but roughly):
info: 0oxxx (256) is u=rw, g=w, o=
info: 0o256 is u=....
But our diagnostics don't support this today :(
|
I'm tempted to make the fix logic this: fn suggest_fix(mode: u16) -> Option<u16> {
// These suggestions are in the form of
// <missing `0o` prefix> | <mode as decimal> => <octal>
// If <as decimal> could theoretically be a valid octal literal, the
// comment explains why it's deemed unlikely to be intentional.
match mode {
400 | 256 => Some(0o400), // -w-r-xrw-, group/other > user unlikely
440 | 288 => Some(0o440),
444 | 292 => Some(0o444),
600 | 384 => Some(0o600),
640 | 416 => Some(0o640), // r----xrw-, other > user unlikely
644 | 420 => Some(0o644), // r---w----, group write but not read unlikely
660 | 432 => Some(0o660), // r---wx-w-, write but not read unlikely
664 | 436 => Some(0o664), // r---wxrw-, other > user unlikely
666 | 438 => Some(0o666),
700 | 448 => Some(0o700),
744 | 484 => Some(0o744),
750 | 488 => Some(0o750),
755 | 493 => Some(0o755),
770 | 504 => Some(0o770), // r-x---r--, other > group unlikely
775 | 509 => Some(0o775),
776 | 510 => Some(0o776), // r-x--x--x, seems unlikely
777 | 511 => Some(0o777), // r-x--x--x, seems unlikely
_ => None
}
} |
I like it! |
3bebfc1 to
df60a38
Compare
* main: (68 commits) Unify `OldDiagnostic` and `Message` (#18391) [`pylint`] Detect more exotic NaN literals in `PLW0177` (#18630) [`flake8-async`] Mark autofix for `ASYNC115` as unsafe if the call expression contains comments (#18753) [`flake8-bugbear`] Mark autofix for `B004` as unsafe if the `hasattr` call expr contains comments (#18755) Enforce `pytest` import for decorators (#18779) [`flake8-comprehension`] Mark autofix for `C420` as unsafe if there's comments inside the dict comprehension (#18768) [flake8-async] fix detection for large integer sleep durations in `ASYNC116` rule (#18767) Update dependency ruff to v0.12.0 (#18790) Update taiki-e/install-action action to v2.53.2 (#18789) Add lint rule for calling chmod with non-octal integers (#18541) Mark `RET501` fix unsafe if comments are inside (#18780) Use `LintContext::report_diagnostic_if_enabled` in `check_tokens` (#18769) [UP008]: use `super()`, not `__super__` in error messages (#18743) Use Depot Windows runners for `cargo test` (#18754) Run ty benchmarks when `ruff_benchmark` changes (#18758) Disallow newlines in format specifiers of single quoted f- or t-strings (#18708) [ty] Add more benchmarks (#18714) [ty] Anchor all exclude patterns (#18685) Include changelog reference for other major versions (#18745) Use updated pre-commit id (#18718) ...
## Summary <!-- What's the purpose of the change? What does it do, and why? --> Part of #17203, comment: #18541 (comment) ## Test Plan <!-- How was it tested? --> updated snapshot --------- Signed-off-by: Bhuminjay <bhuminjaysoni@gmail.com> Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
Resolves #18464
Summary
Adds a new lint rule to find cases where chmod was called with a decimal integer, e.g.
os.chmod(400)looks like it would setu=r,go=permissions but actually setsu=rw,g=w,o=. The intent was probably0o400.Test Plan
cargo test