Skip to content

Add lint rule for calling chmod with non-octal integers#18541

Merged
MichaReiser merged 6 commits intoastral-sh:mainfrom
RazerM:feature/chmod-non-octal
Jun 19, 2025
Merged

Add lint rule for calling chmod with non-octal integers#18541
MichaReiser merged 6 commits intoastral-sh:mainfrom
RazerM:feature/chmod-non-octal

Conversation

@RazerM
Copy link
Contributor

@RazerM RazerM commented Jun 7, 2025

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 set u=r,go= permissions but actually sets u=rw,g=w,o=. The intent was probably 0o400.

Test Plan

cargo test

@ntBre ntBre self-requested a review June 8, 2025 03:35
@ntBre ntBre added the rule Implementing or modifying a lint rule label Jun 8, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Jun 8, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+1 -0 violations, +0 -0 fixes in 1 projects; 54 projects unchanged)

apache/airflow (+1 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --no-fix --output-format concise --preview --select ALL

+ devel-common/src/tests_common/test_utils/gcp_system_helpers.py:175:32: RUF064 Non-octal mode

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
RUF064 1 1 0 0 0

@MichaReiser
Copy link
Member

Copy link
Member

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you.

This mostly looks good. My main feedback is that I don't think we should offer a fix here.

(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),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do like clippy's naming because it leaves the door open to lint for other cases than just chmod (if there are any)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 105 to 107
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));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}`?")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@MichaReiser
Copy link
Member

@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.

@RazerM
Copy link
Contributor Author

RazerM commented Jun 9, 2025

I also hemmed and hawed about whether to include a fix, and whether it should be limited to certain common cases (e.g. 644 -> 0o644).

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 0o came in 2.6).

E.g. the pytest test suite contains chmod(256) which is equivalent to 0o400. Our suggested fix would be wrong.

Copy link
Member

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
diagnostic.set_fix(Fix::applicable_edit(edit, Applicability::Unsafe));
diagnostic.set_fix(Fix::unsafe_edit(edit));

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

fn message(&self) -> String {
match self.reason {
Reason::Decimal { found, suggested } => {
format!("Non-octal mode `{found}`, did you mean `{suggested:#o}`?")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd remove the suggested as it is also visible in the fix

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@RazerM RazerM force-pushed the feature/chmod-non-octal branch 2 times, most recently from 23dcc33 to 3bebfc1 Compare June 10, 2025 22:10

#[derive_message_formats]
fn message(&self) -> String {
"Non-octal mode".to_string()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 :(

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added to #17203!

@RazerM
Copy link
Contributor Author

RazerM commented Jun 10, 2025

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
    }
}

@MichaReiser
Copy link
Member

I'm tempted to make the fix logic this:

I like it!

@ntBre ntBre mentioned this pull request Apr 4, 2025
7 tasks
@RazerM RazerM force-pushed the feature/chmod-non-octal branch from 3bebfc1 to df60a38 Compare June 18, 2025 20:46
Copy link
Member

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thank you

@MichaReiser MichaReiser merged commit f67ff33 into astral-sh:main Jun 19, 2025
33 of 34 checks passed
dcreager added a commit that referenced this pull request Jun 19, 2025
* 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)
  ...
ntBre added a commit that referenced this pull request Feb 2, 2026
## 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rule Implementing or modifying a lint rule

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[New rule] Octal literal for chmod mode bits

3 participants