[ruff] Extend unnecessary-regular-expression to non-literal strings (RUF055)#14679
[ruff] Extend unnecessary-regular-expression to non-literal strings (RUF055)#14679AlexWaygood merged 22 commits intoastral-sh:mainfrom
ruff] Extend unnecessary-regular-expression to non-literal strings (RUF055)#14679Conversation
everything shifted down after wrapping the docstring
This reverts commit efcc4cf.
|
crates/ruff_linter/src/rules/ruff/rules/unnecessary_regular_expression.rs
Outdated
Show resolved
Hide resolved
crates/ruff_linter/src/rules/ruff/rules/unnecessary_regular_expression.rs
Outdated
Show resolved
Hide resolved
crates/ruff_linter/src/rules/ruff/rules/unnecessary_regular_expression.rs
Outdated
Show resolved
Hide resolved
| // make sure repl can be resolved to a string literal | ||
| resolve_string_literal(repl, semantic)?; |
There was a problem hiding this comment.
If I understand correctly, I think the need here is slightly different to the need on lines 95-96. On lines 95-96, we do need to know the value of the string in order to be able to check it doesn't have any metacharacters in it (so only a string literal will do, or something that we can resolve to a string literal). But here, we just need to know it's a string; any string will do, as long as the user isn't passing in a function.
Is that the case? If so, it might be worth adding back the is_str function you added in efcc4cf and using that here, rather than using resolve_string_literal in both places. The advantage of the is_str technique is that it also understands basic type hints, e.g. it would understand that re.sub() is being passed a string for the repl argument in something like this:
import re
def foo(input_str: str, repl: str):
re.sub("foobar", repl, input_str)There was a problem hiding this comment.
Now that I look at it, maybe we do need to know (and analyze) the value of the repl string for a fully accurate analysis, though. For example, it seems like the initial version of the check that we merged yesterday emits a false-positive diagnostic (and incorrect autofix) for this:
import re
re.sub(r"a", r"\g<0>\g<0>\g<0>", "a")Now, this is a massive edge case -- I had to work quite hard to find it! I believe the only way you get a false positive with the rule's current logic is if there's a \g in the replacement string but no backslashes or metacharacters in the pattenr string, and it's almost impossible to think of a way you could plausibly have a re.sub() call with those characteristics. So maybe we shouldn't worry about this -- I'm interested in your thoughts and @MichaReiser's!
There was a problem hiding this comment.
Oh wow, good catch! I was working on adding is_str back in, but maybe instead I need to check for metacharacters in repl too.
I thought we were safe from backreferences by avoiding ( in the pattern, but I overlooked \g<0>. That exact sequence seems like the only way to trigger this behavior?
There was a problem hiding this comment.
I thought we were safe from backreferences by avoiding
(in the pattern, but I overlooked\g<0>. That exact sequence seems like the only way to trigger this behavior?
I think so, yes! Although we also emit a RUF055 diagnostic on invalid re.sub() calls like this, and maybe we should just ignore them? It feels like it might be outside of this rule's purview to autofix invalid re.sub() calls into valid str.replace() calls. We probably don't really know what the user intended exactly if the re.sub() call is invalid:
>>> import re
>>> re.sub(r"a", r"\1", "a")
Traceback (most recent call last):
File "<python-input-12>", line 1, in <module>
re.sub(r"a", r"\1", "a")
~~~~~~^^^^^^^^^^^^^^^^^^
File "/Users/alexw/.pyenv/versions/3.13.0/lib/python3.13/re/__init__.py", line 208, in sub
return _compile(pattern, flags).sub(repl, string, count)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^
File "/Users/alexw/.pyenv/versions/3.13.0/lib/python3.13/re/__init__.py", line 377, in _compile_template
return _sre.template(pattern, _parser.parse_template(repl, pattern))
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^
File "/Users/alexw/.pyenv/versions/3.13.0/lib/python3.13/re/_parser.py", line 1070, in parse_template
addgroup(int(this[1:]), len(this) - 1)
~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/alexw/.pyenv/versions/3.13.0/lib/python3.13/re/_parser.py", line 1015, in addgroup
raise s.error("invalid group reference %d" % index, pos)
re.PatternError: invalid group reference 1 at position 1There was a problem hiding this comment.
I've added back is_str locally, along with your function argument test case. It's really nice to handle that case, but I'm a bit bothered by this edge case too, so I could go either way. I'm interested to hear which approach you and Micha think is best overall.
There was a problem hiding this comment.
Why don't you push the version with is_str to this PR, and we can see if it results in any more ecosystem hits? That might give us some more data on how useful it is to be able to detect that the repl argument is a string from the function annotation
There was a problem hiding this comment.
Hmm, it doesn't look like it adds any new ecosystem hits :/
I guess in that case, I'd vote for removing is_str again, and fixing the false positives on \g<0> and \1 in repl arguments.
Thanks for putting up with my pernickitiness here!
There was a problem hiding this comment.
No problem, thanks for the thorough review! Should I reuse the other code to reject any metacharacters, or are references to named or numbered capture groups the only problems? I'm picturing checking for \ followed by g or 1 through 9. That seems a bit nicer than rejecting any metacharacter like I did for the patterns but possibly less safe.
There was a problem hiding this comment.
I'm picturing checking for
\followed bygor1through9. That seems a bit nicer than rejecting any metacharacter like I did for the patterns but possibly less safe.
I think actually we could check for \ followed by any ASCII character except one of abfnrtv. Other than 0-9 and g (which both have special behaviour in repl strings, as we've just been discussing!), I believe those are the only ASCII escapes that will be permitted in a repl string by re.sub(), Anything else causes re.PatternError to be raised -- meaning it's probably out of scope for us to emit this diagnostic on it:
>>> re.sub(r"a", r"\d", "a")
Traceback (most recent call last):
File "<python-input-13>", line 1, in <module>
re.sub(r"a", r"\d", "a")
~~~~~~^^^^^^^^^^^^^^^^^^
File "/Users/alexw/.pyenv/versions/3.13.0/lib/python3.13/re/__init__.py", line 208, in sub
return _compile(pattern, flags).sub(repl, string, count)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^
File "/Users/alexw/.pyenv/versions/3.13.0/lib/python3.13/re/__init__.py", line 377, in _compile_template
return _sre.template(pattern, _parser.parse_template(repl, pattern))
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^
File "/Users/alexw/.pyenv/versions/3.13.0/lib/python3.13/re/_parser.py", line 1076, in parse_template
raise s.error('bad escape %s' % this, len(this)) from None
re.PatternError: bad escape \d at position 0There was a problem hiding this comment.
Let me know what you think about this version. If it looks good, it might be nice to reuse this escape check for pattern as well instead of rejecting \ entirely.
crates/ruff_linter/src/rules/ruff/rules/unnecessary_regular_expression.rs
Outdated
Show resolved
Hide resolved
|
This looks good to me and nice find @AlexWaygood |
AlexWaygood
left a comment
There was a problem hiding this comment.
Thanks @ntBre!! I pushed some minor fixes to address some other edge cases identified by @dscorbett in #14757
* main: [`ruff`] Extend unnecessary-regular-expression to non-literal strings (`RUF055`) (#14679) Minor followups to RUF052 (#14755) [red-knot] Property tests (#14178) [red-knot] `is_subtype_of` fix for `KnownInstance` types (#14750) Improve docs for flake8-use-pathlib rules (#14741) [`ruff`] Implemented `used-dummy-variable` (`RUF052`) (#14611) [red-knot] Simplify tuples containing `Never` (#14744) Possible fix for flaky file watching test (#14543) [`flake8-import-conventions`] Improve syntax check for aliases supplied in configuration for `unconventional-import-alias (ICN001)` (#14745) [red-knot] Deeper understanding of `LiteralString` (#14649) red-knot: support narrowing for bool(E) (#14668) [`refurb`] Handle non-finite decimals in `verbose-decimal-constructor (FURB157)` (#14596) [red-knot] Re-enable linter corpus tests (#14736)
Summary
This is a follow-up to #14659 to try to resolve variable bindings for the
patternargument inremethods likesubandmatch. The rule currently only matches string literals, but these changes enable detection of patterns like this:For
subspecifically, it also handles non-literalreplarguments, which also have to be strings for the suggested fix to be valid.Test Plan
cargo testwith a new snapshot test based on the example above.