mirror of https://github.com/astral-sh/ruff
[`ISC001`] fix panic when string literals are unclosed (#21034)
Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
parent
17850eee4b
commit
d0aebaa253
7
crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC_syntax_error_2.py
vendored
Normal file
7
crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC_syntax_error_2.py
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Regression test for https://github.com/astral-sh/ruff/issues/21023
|
||||
'' '
|
||||
"" ""
|
||||
'' '' '
|
||||
"" "" "
|
||||
f"" f"
|
||||
f"" f"" f"
|
||||
|
|
@ -23,6 +23,14 @@ mod tests {
|
|||
Rule::MultiLineImplicitStringConcatenation,
|
||||
Path::new("ISC_syntax_error.py")
|
||||
)]
|
||||
#[test_case(
|
||||
Rule::SingleLineImplicitStringConcatenation,
|
||||
Path::new("ISC_syntax_error_2.py")
|
||||
)]
|
||||
#[test_case(
|
||||
Rule::MultiLineImplicitStringConcatenation,
|
||||
Path::new("ISC_syntax_error_2.py")
|
||||
)]
|
||||
#[test_case(Rule::ExplicitStringConcatenation, Path::new("ISC.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::str::{leading_quote, trailing_quote};
|
||||
use ruff_python_ast::StringFlags;
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_parser::{TokenKind, Tokens};
|
||||
use ruff_python_parser::{Token, TokenKind, Tokens};
|
||||
use ruff_source_file::LineRanges;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange};
|
||||
|
||||
use crate::Locator;
|
||||
use crate::checkers::ast::LintContext;
|
||||
|
|
@ -169,7 +168,8 @@ pub(crate) fn implicit(
|
|||
SingleLineImplicitStringConcatenation,
|
||||
TextRange::new(a_range.start(), b_range.end()),
|
||||
) {
|
||||
if let Some(fix) = concatenate_strings(a_range, b_range, locator) {
|
||||
if let Some(fix) = concatenate_strings(a_token, b_token, a_range, b_range, locator)
|
||||
{
|
||||
diagnostic.set_fix(fix);
|
||||
}
|
||||
}
|
||||
|
|
@ -177,38 +177,55 @@ pub(crate) fn implicit(
|
|||
}
|
||||
}
|
||||
|
||||
fn concatenate_strings(a_range: TextRange, b_range: TextRange, locator: &Locator) -> Option<Fix> {
|
||||
let a_text = locator.slice(a_range);
|
||||
let b_text = locator.slice(b_range);
|
||||
|
||||
let a_leading_quote = leading_quote(a_text)?;
|
||||
let b_leading_quote = leading_quote(b_text)?;
|
||||
|
||||
// Require, for now, that the leading quotes are the same.
|
||||
if a_leading_quote != b_leading_quote {
|
||||
/// Concatenates two strings
|
||||
///
|
||||
/// The `a_string_range` and `b_string_range` are the range of the entire string,
|
||||
/// not just of the string token itself (important for interpolated strings where
|
||||
/// the start token doesn't span the entire token).
|
||||
fn concatenate_strings(
|
||||
a_token: &Token,
|
||||
b_token: &Token,
|
||||
a_string_range: TextRange,
|
||||
b_string_range: TextRange,
|
||||
locator: &Locator,
|
||||
) -> Option<Fix> {
|
||||
if a_token.string_flags()?.is_unclosed() || b_token.string_flags()?.is_unclosed() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let a_trailing_quote = trailing_quote(a_text)?;
|
||||
let b_trailing_quote = trailing_quote(b_text)?;
|
||||
let a_string_flags = a_token.string_flags()?;
|
||||
let b_string_flags = b_token.string_flags()?;
|
||||
|
||||
// Require, for now, that the trailing quotes are the same.
|
||||
if a_trailing_quote != b_trailing_quote {
|
||||
let a_prefix = a_string_flags.prefix();
|
||||
let b_prefix = b_string_flags.prefix();
|
||||
|
||||
// Require, for now, that the strings have the same prefix,
|
||||
// quote style, and number of quotes
|
||||
if a_prefix != b_prefix
|
||||
|| a_string_flags.quote_style() != b_string_flags.quote_style()
|
||||
|| a_string_flags.is_triple_quoted() != b_string_flags.is_triple_quoted()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let a_text = locator.slice(a_string_range);
|
||||
let b_text = locator.slice(b_string_range);
|
||||
|
||||
let quotes = a_string_flags.quote_str();
|
||||
|
||||
let opener_len = a_string_flags.opener_len();
|
||||
let closer_len = a_string_flags.closer_len();
|
||||
|
||||
let mut a_body =
|
||||
Cow::Borrowed(&a_text[a_leading_quote.len()..a_text.len() - a_trailing_quote.len()]);
|
||||
let b_body = &b_text[b_leading_quote.len()..b_text.len() - b_trailing_quote.len()];
|
||||
Cow::Borrowed(&a_text[TextRange::new(opener_len, a_text.text_len() - closer_len)]);
|
||||
let b_body = &b_text[TextRange::new(opener_len, b_text.text_len() - closer_len)];
|
||||
|
||||
if a_leading_quote.find(['r', 'R']).is_none()
|
||||
&& matches!(b_body.bytes().next(), Some(b'0'..=b'7'))
|
||||
{
|
||||
if !a_string_flags.is_raw_string() && matches!(b_body.bytes().next(), Some(b'0'..=b'7')) {
|
||||
normalize_ending_octal(&mut a_body);
|
||||
}
|
||||
|
||||
let concatenation = format!("{a_leading_quote}{a_body}{b_body}{a_trailing_quote}");
|
||||
let range = TextRange::new(a_range.start(), b_range.end());
|
||||
let concatenation = format!("{a_prefix}{quotes}{a_body}{b_body}{quotes}");
|
||||
let range = TextRange::new(a_string_range.start(), b_string_range.end());
|
||||
|
||||
Some(Fix::safe_edit(Edit::range_replacement(
|
||||
concatenation,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs
|
||||
---
|
||||
ISC001 Implicitly concatenated string literals on one line
|
||||
--> ISC_syntax_error_2.py:2:1
|
||||
|
|
||||
1 | # Regression test for https://github.com/astral-sh/ruff/issues/21023
|
||||
2 | '' '
|
||||
| ^^^^
|
||||
3 | "" ""
|
||||
4 | '' '' '
|
||||
|
|
||||
help: Combine string literals
|
||||
|
||||
invalid-syntax: missing closing quote in string literal
|
||||
--> ISC_syntax_error_2.py:2:4
|
||||
|
|
||||
1 | # Regression test for https://github.com/astral-sh/ruff/issues/21023
|
||||
2 | '' '
|
||||
| ^
|
||||
3 | "" ""
|
||||
4 | '' '' '
|
||||
|
|
||||
|
||||
ISC001 Implicitly concatenated string literals on one line
|
||||
--> ISC_syntax_error_2.py:3:1
|
||||
|
|
||||
1 | # Regression test for https://github.com/astral-sh/ruff/issues/21023
|
||||
2 | '' '
|
||||
3 | "" ""
|
||||
| ^^^^^
|
||||
4 | '' '' '
|
||||
5 | "" "" "
|
||||
|
|
||||
help: Combine string literals
|
||||
|
||||
ISC001 Implicitly concatenated string literals on one line
|
||||
--> ISC_syntax_error_2.py:4:1
|
||||
|
|
||||
2 | '' '
|
||||
3 | "" ""
|
||||
4 | '' '' '
|
||||
| ^^^^^
|
||||
5 | "" "" "
|
||||
6 | f"" f"
|
||||
|
|
||||
help: Combine string literals
|
||||
|
||||
ISC001 Implicitly concatenated string literals on one line
|
||||
--> ISC_syntax_error_2.py:4:4
|
||||
|
|
||||
2 | '' '
|
||||
3 | "" ""
|
||||
4 | '' '' '
|
||||
| ^^^^
|
||||
5 | "" "" "
|
||||
6 | f"" f"
|
||||
|
|
||||
help: Combine string literals
|
||||
|
||||
invalid-syntax: missing closing quote in string literal
|
||||
--> ISC_syntax_error_2.py:4:7
|
||||
|
|
||||
2 | '' '
|
||||
3 | "" ""
|
||||
4 | '' '' '
|
||||
| ^
|
||||
5 | "" "" "
|
||||
6 | f"" f"
|
||||
|
|
||||
|
||||
ISC001 Implicitly concatenated string literals on one line
|
||||
--> ISC_syntax_error_2.py:5:1
|
||||
|
|
||||
3 | "" ""
|
||||
4 | '' '' '
|
||||
5 | "" "" "
|
||||
| ^^^^^
|
||||
6 | f"" f"
|
||||
7 | f"" f"" f"
|
||||
|
|
||||
help: Combine string literals
|
||||
|
||||
ISC001 Implicitly concatenated string literals on one line
|
||||
--> ISC_syntax_error_2.py:5:4
|
||||
|
|
||||
3 | "" ""
|
||||
4 | '' '' '
|
||||
5 | "" "" "
|
||||
| ^^^^
|
||||
6 | f"" f"
|
||||
7 | f"" f"" f"
|
||||
|
|
||||
help: Combine string literals
|
||||
|
||||
invalid-syntax: missing closing quote in string literal
|
||||
--> ISC_syntax_error_2.py:5:7
|
||||
|
|
||||
3 | "" ""
|
||||
4 | '' '' '
|
||||
5 | "" "" "
|
||||
| ^
|
||||
6 | f"" f"
|
||||
7 | f"" f"" f"
|
||||
|
|
||||
|
||||
invalid-syntax: f-string: unterminated string
|
||||
--> ISC_syntax_error_2.py:6:7
|
||||
|
|
||||
4 | '' '' '
|
||||
5 | "" "" "
|
||||
6 | f"" f"
|
||||
| ^
|
||||
7 | f"" f"" f"
|
||||
|
|
||||
|
||||
ISC001 Implicitly concatenated string literals on one line
|
||||
--> ISC_syntax_error_2.py:7:1
|
||||
|
|
||||
5 | "" "" "
|
||||
6 | f"" f"
|
||||
7 | f"" f"" f"
|
||||
| ^^^^^^^
|
||||
|
|
||||
help: Combine string literals
|
||||
|
||||
invalid-syntax: f-string: unterminated string
|
||||
--> ISC_syntax_error_2.py:7:11
|
||||
|
|
||||
5 | "" "" "
|
||||
6 | f"" f"
|
||||
7 | f"" f"" f"
|
||||
| ^
|
||||
|
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs
|
||||
---
|
||||
invalid-syntax: missing closing quote in string literal
|
||||
--> ISC_syntax_error_2.py:2:4
|
||||
|
|
||||
1 | # Regression test for https://github.com/astral-sh/ruff/issues/21023
|
||||
2 | '' '
|
||||
| ^
|
||||
3 | "" ""
|
||||
4 | '' '' '
|
||||
|
|
||||
|
||||
invalid-syntax: missing closing quote in string literal
|
||||
--> ISC_syntax_error_2.py:4:7
|
||||
|
|
||||
2 | '' '
|
||||
3 | "" ""
|
||||
4 | '' '' '
|
||||
| ^
|
||||
5 | "" "" "
|
||||
6 | f"" f"
|
||||
|
|
||||
|
||||
invalid-syntax: missing closing quote in string literal
|
||||
--> ISC_syntax_error_2.py:5:7
|
||||
|
|
||||
3 | "" ""
|
||||
4 | '' '' '
|
||||
5 | "" "" "
|
||||
| ^
|
||||
6 | f"" f"
|
||||
7 | f"" f"" f"
|
||||
|
|
||||
|
||||
invalid-syntax: f-string: unterminated string
|
||||
--> ISC_syntax_error_2.py:6:7
|
||||
|
|
||||
4 | '' '' '
|
||||
5 | "" "" "
|
||||
6 | f"" f"
|
||||
| ^
|
||||
7 | f"" f"" f"
|
||||
|
|
||||
|
||||
invalid-syntax: f-string: unterminated string
|
||||
--> ISC_syntax_error_2.py:7:11
|
||||
|
|
||||
5 | "" "" "
|
||||
6 | f"" f"
|
||||
7 | f"" f"" f"
|
||||
| ^
|
||||
|
|
||||
Loading…
Reference in New Issue