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,
|
Rule::MultiLineImplicitStringConcatenation,
|
||||||
Path::new("ISC_syntax_error.py")
|
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"))]
|
#[test_case(Rule::ExplicitStringConcatenation, Path::new("ISC.py"))]
|
||||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
||||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
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_index::Indexer;
|
||||||
use ruff_python_parser::{TokenKind, Tokens};
|
use ruff_python_parser::{Token, TokenKind, Tokens};
|
||||||
use ruff_source_file::LineRanges;
|
use ruff_source_file::LineRanges;
|
||||||
use ruff_text_size::{Ranged, TextRange};
|
use ruff_text_size::{Ranged, TextLen, TextRange};
|
||||||
|
|
||||||
use crate::Locator;
|
use crate::Locator;
|
||||||
use crate::checkers::ast::LintContext;
|
use crate::checkers::ast::LintContext;
|
||||||
|
|
@ -169,7 +168,8 @@ pub(crate) fn implicit(
|
||||||
SingleLineImplicitStringConcatenation,
|
SingleLineImplicitStringConcatenation,
|
||||||
TextRange::new(a_range.start(), b_range.end()),
|
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);
|
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> {
|
/// Concatenates two strings
|
||||||
let a_text = locator.slice(a_range);
|
///
|
||||||
let b_text = locator.slice(b_range);
|
/// 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
|
||||||
let a_leading_quote = leading_quote(a_text)?;
|
/// the start token doesn't span the entire token).
|
||||||
let b_leading_quote = leading_quote(b_text)?;
|
fn concatenate_strings(
|
||||||
|
a_token: &Token,
|
||||||
// Require, for now, that the leading quotes are the same.
|
b_token: &Token,
|
||||||
if a_leading_quote != b_leading_quote {
|
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;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let a_trailing_quote = trailing_quote(a_text)?;
|
let a_string_flags = a_token.string_flags()?;
|
||||||
let b_trailing_quote = trailing_quote(b_text)?;
|
let b_string_flags = b_token.string_flags()?;
|
||||||
|
|
||||||
// Require, for now, that the trailing quotes are the same.
|
let a_prefix = a_string_flags.prefix();
|
||||||
if a_trailing_quote != b_trailing_quote {
|
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;
|
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 =
|
let mut a_body =
|
||||||
Cow::Borrowed(&a_text[a_leading_quote.len()..a_text.len() - a_trailing_quote.len()]);
|
Cow::Borrowed(&a_text[TextRange::new(opener_len, a_text.text_len() - closer_len)]);
|
||||||
let b_body = &b_text[b_leading_quote.len()..b_text.len() - b_trailing_quote.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()
|
if !a_string_flags.is_raw_string() && matches!(b_body.bytes().next(), Some(b'0'..=b'7')) {
|
||||||
&& matches!(b_body.bytes().next(), Some(b'0'..=b'7'))
|
|
||||||
{
|
|
||||||
normalize_ending_octal(&mut a_body);
|
normalize_ending_octal(&mut a_body);
|
||||||
}
|
}
|
||||||
|
|
||||||
let concatenation = format!("{a_leading_quote}{a_body}{b_body}{a_trailing_quote}");
|
let concatenation = format!("{a_prefix}{quotes}{a_body}{b_body}{quotes}");
|
||||||
let range = TextRange::new(a_range.start(), b_range.end());
|
let range = TextRange::new(a_string_range.start(), b_string_range.end());
|
||||||
|
|
||||||
Some(Fix::safe_edit(Edit::range_replacement(
|
Some(Fix::safe_edit(Edit::range_replacement(
|
||||||
concatenation,
|
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