F507: Fix false negative for non-tuple RHS in %-formatting#24142
F507: Fix false negative for non-tuple RHS in %-formatting#24142MichaReiser merged 2 commits intoastral-sh:mainfrom
F507: Fix false negative for non-tuple RHS in %-formatting#24142Conversation
|
Does this also solve the case where the LHS is an f-string? |
|
No, it doesn't. That's a separate issue. The LHS check happens in if let Expr::StringLiteral(format_string @ ast::ExprStringLiteral { value, .. }) = left.as_ref()So when the LHS is an f-string ( I will make a separate PR for that. |
|
@ntBre Could you please review my PR? |
OK, but perhaps a case with 0 placeholders and 1 item in right hand side should be considered. I'm not sure if this case can be covered without risk of false positive: What happens for this case if banana is an empty tuple? I suppose for this particular case, with a literal string LHS, any use of |
|
Good point! The current implementation already covers that case — For |
F507: Fix false negative for non-tuple RHS in %-formatting
* main: [ty] Avoid eager TypedDict diagnostics in `TypedDict | dict` unions (#24151) `F507`: Fix false negative for non-tuple RHS in `%`-formatting (#24142) [ty] Update `SpecializationBuilder` hook to get both lower/upper bounds (#23848) Fix `%foo?` parsing in IPython assignment expressions (#24152) `E501`/`W505`/formatter: Exclude nested pragma comments from line width calculation (#24071) [ty] Fix Salsa panic propagation (#24141) [ty] Support `type:ignore[ty:code]` suppressions (#24096) [ty] Support narrowing for extended walrus targets (#24129)
##### [\`v0.15.8\`](https://github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#0158) Released on 2026-03-26. ##### Preview features - \[`ruff`] New rule `unnecessary-if` (`RUF050`) ([#24114](astral-sh/ruff#24114)) - \[`ruff`] New rule `useless-finally` (`RUF072`) ([#24165](astral-sh/ruff#24165)) - \[`ruff`] New rule `f-string-percent-format` (`RUF073`): warn when using `%` operator on an f-string ([#24162](astral-sh/ruff#24162)) - \[`pyflakes`] Recognize `frozendict` as a builtin for Python 3.15+ ([#24100](astral-sh/ruff#24100)) ##### Bug fixes - \[`flake8-async`] Use fully-qualified `anyio.lowlevel` import in autofix (`ASYNC115`) ([#24166](astral-sh/ruff#24166)) - \[`flake8-bandit`] Check tuple arguments for partial paths in `S607` ([#24080](astral-sh/ruff#24080)) - \[`pyflakes`] Skip `undefined-name` (`F821`) for conditionally deleted variables ([#24088](astral-sh/ruff#24088)) - `E501`/`W505`/formatter: Exclude nested pragma comments from line width calculation ([#24071](astral-sh/ruff#24071)) - Fix `%foo?` parsing in IPython assignment expressions ([#24152](astral-sh/ruff#24152)) - `analyze graph`: resolve string imports that reference attributes, not just modules ([#24058](astral-sh/ruff#24058)) ##### Rule changes - \[`eradicate`] ignore `ty: ignore` comments in `ERA001` ([#24192](astral-sh/ruff#24192)) - \[`flake8-bandit`] Treat `sys.executable` as trusted input in `S603` ([#24106](astral-sh/ruff#24106)) - \[`flake8-self`] Recognize `Self` annotation and `self` assignment in `SLF001` ([#24144](astral-sh/ruff#24144)) - \[`pyflakes`] `F507`: Fix false negative for non-tuple RHS in `%`-formatting ([#24142](astral-sh/ruff#24142)) - \[`refurb`] Parenthesize generator arguments in `FURB142` fixer ([#24200](astral-sh/ruff#24200)) ##### Performance - Speed up diagnostic rendering ([#24146](astral-sh/ruff#24146)) ##### Server - Warn when Markdown files are skipped due to preview being disabled ([#24150](astral-sh/ruff#24150)) ##### Documentation - Clarify `extend-ignore` and `extend-select` settings documentation ([#24064](astral-sh/ruff#24064)) - Mention AI policy in PR template ([#24198](astral-sh/ruff#24198)) ##### Other changes - Use trusted publishing for NPM packages ([#24171](astral-sh/ruff#24171)) ##### Contributors - [@bitloi](https://github.com/bitloi) - [@Sim-hu](https://github.com/Sim-hu) - [@mvanhorn](https://github.com/mvanhorn) - [@chinar-amrutkar](https://github.com/chinar-amrutkar) - [@markjm](https://github.com/markjm) - [@RenzoMXD](https://github.com/RenzoMXD) - [@vivekkhimani](https://github.com/vivekkhimani) - [@seroperson](https://github.com/seroperson) - [@moktamd](https://github.com/moktamd) - [@charliermarsh](https://github.com/charliermarsh) - [@ntBre](https://github.com/ntBre) - [@zanieb](https://github.com/zanieb) - [@dylwil3](https://github.com/dylwil3) - [@MichaReiser](https://github.com/MichaReiser) Renovate-Branch: renovate/2024.6-ruff-0.15.x Change-Id: Ifd4216a963962ffb24a4df69802bc60fcc29628d Priv-Id: 46d2f61be3a5e65a9fdd2fef998ba41ea3388f12
Summary
Fixes #24031
F507 (
percent-format-positional-count-mismatch) previously only checked tuple right-hand sides in%-format expressions. This meant literal non-tuple values like'%s %s' % 42were silently ignored.This PR extends F507 to also flag non-tuple RHS values when the format string expects a different number of positional arguments, using
ResolvedPythonType::from()to infer the type of the RHS expression.What gets flagged now
42,3.14,"hello",b"hello",True,None,...,f"hello {name}"-1,1 + 2,not x,"a" + "b",1 if True else 2What is intentionally NOT flagged
Variables, attribute accesses, subscripts, and calls are skipped because they could be tuples at runtime:
Why
ResolvedPythonTypePer reviewer suggestion, using
ResolvedPythonType::from()instead of manually matching literal expression types catches more cases (unary ops, binary ops, ternary, etc.) and is more maintainable.Context
The original implementation only checked
Expr::TuplebecauseListandSetwere intentionally removed in #5986 —'%s' % [1, 2, 3]is valid Python (lists aren't unpacked by%). This PR takes the same conservative approach by only flagging cases where we can statically determine the type is not a tuple.Test plan
ResolvedPythonType(unary, binary, boolean, ternary)