mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-14 23:51:03 +00:00
Use ExprFString
for StringLike::FString
variant (#10311)
## Summary This PR updates the `StringLike::FString` variant to use `ExprFString` instead of `FStringLiteralElement`. For context, the reason it used `FStringLiteralElement` is that the node is actually the string part of an f-string ("foo" in `f"foo{x}"`). But, this is inconsistent with other variants where the captured value is the _entire_ string. This is also problematic w.r.t. implicitly concatenated strings. Any rules which work with `StringLike::FString` doesn't account for the string part in an implicitly concatenated f-strings. For example, we don't flag confusable character in the first part of `"𝐁ad" f"𝐁ad string"`, but only the second part (https://play.ruff.rs/16071c4c-a1dd-4920-b56f-e2ce2f69c843). ### Update `PYI053` _This is included in this PR because otherwise it requires a temporary workaround to be compatible with the old logic._ This PR also updates the `PYI053` (`string-or-bytes-too-long`) rule for f-string to consider _all_ the visible characters in a f-string, including the ones which are implicitly concatenated. This is consistent with implicitly concatenated strings and bytes. For example, ```python def foo( # We count all the characters here arg1: str = '51 character ' 'stringgggggggggggggggggggggggggggggggg', # But not here because of the `{x}` replacement field which _breaks_ them up into two chunks arg2: str = f'51 character {x} stringgggggggggggggggggggggggggggggggggggggggggggg', ) -> None: ... ``` This PR fixes it to consider all _visible_ characters inside an f-string which includes expressions as well. fixes: #10310 fixes: #10307 ## Test Plan Add new test cases and update the snapshots. ## Review To facilitate the review process, the change have been split into two commits: one which has the code change while the other has the test cases and updated snapshots.
This commit is contained in:
parent
f7802ad5de
commit
5f40371ffc
16 changed files with 251 additions and 63 deletions
|
@ -18,3 +18,7 @@ func("0.0.0.0")
|
|||
def my_func():
|
||||
x = "0.0.0.0"
|
||||
print(x)
|
||||
|
||||
|
||||
# Implicit string concatenation
|
||||
"0.0.0.0" f"0.0.0.0{expr}0.0.0.0"
|
||||
|
|
|
@ -18,6 +18,13 @@ with open("/dev/shm/unit/test", "w") as f:
|
|||
with open("/foo/bar", "w") as f:
|
||||
f.write("def")
|
||||
|
||||
# Implicit string concatenation
|
||||
with open("/tmp/" "abc", "w") as f:
|
||||
f.write("def")
|
||||
|
||||
with open("/tmp/abc" f"/tmp/abc", "w") as f:
|
||||
f.write("def")
|
||||
|
||||
# Using `tempfile` module should be ok
|
||||
import tempfile
|
||||
from tempfile import TemporaryDirectory
|
||||
|
|
|
@ -64,3 +64,5 @@ def not_warnings_dot_deprecated(
|
|||
"Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!" # Error: PYI053
|
||||
)
|
||||
def not_a_deprecated_function() -> None: ...
|
||||
|
||||
fbaz: str = f"51 character {foo} stringgggggggggggggggggggggggggg" # Error: PYI053
|
||||
|
|
|
@ -53,3 +53,6 @@ class Labware:
|
|||
|
||||
|
||||
assert getattr(Labware(), "µL") == 1.5
|
||||
|
||||
# Implicit string concatenation
|
||||
x = "𝐁ad" f"𝐁ad string"
|
||||
|
|
|
@ -45,7 +45,7 @@ use ruff_python_ast::helpers::{
|
|||
use ruff_python_ast::identifier::Identifier;
|
||||
use ruff_python_ast::name::QualifiedName;
|
||||
use ruff_python_ast::str::Quote;
|
||||
use ruff_python_ast::visitor::{walk_except_handler, walk_f_string_element, walk_pattern, Visitor};
|
||||
use ruff_python_ast::visitor::{walk_except_handler, walk_pattern, Visitor};
|
||||
use ruff_python_ast::{helpers, str, visitor, PySourceType};
|
||||
use ruff_python_codegen::{Generator, Stylist};
|
||||
use ruff_python_index::Indexer;
|
||||
|
@ -1407,6 +1407,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
|||
analyze::string_like(string_literal.into(), self);
|
||||
}
|
||||
Expr::BytesLiteral(bytes_literal) => analyze::string_like(bytes_literal.into(), self),
|
||||
Expr::FString(f_string) => analyze::string_like(f_string.into(), self),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
@ -1573,16 +1574,6 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
|||
.push((bound, self.semantic.snapshot()));
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_f_string_element(&mut self, f_string_element: &'a ast::FStringElement) {
|
||||
// Step 2: Traversal
|
||||
walk_f_string_element(self, f_string_element);
|
||||
|
||||
// Step 4: Analysis
|
||||
if let Some(literal) = f_string_element.as_literal() {
|
||||
analyze::string_like(literal.into(), self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Checker<'a> {
|
||||
|
|
|
@ -38,17 +38,37 @@ impl Violation for HardcodedBindAllInterfaces {
|
|||
|
||||
/// S104
|
||||
pub(crate) fn hardcoded_bind_all_interfaces(checker: &mut Checker, string: StringLike) {
|
||||
let is_bind_all_interface = match string {
|
||||
StringLike::StringLiteral(ast::ExprStringLiteral { value, .. }) => value == "0.0.0.0",
|
||||
StringLike::FStringLiteral(ast::FStringLiteralElement { value, .. }) => {
|
||||
&**value == "0.0.0.0"
|
||||
}
|
||||
StringLike::BytesLiteral(_) => return,
|
||||
};
|
||||
|
||||
if is_bind_all_interface {
|
||||
match string {
|
||||
StringLike::String(ast::ExprStringLiteral { value, .. }) => {
|
||||
if value == "0.0.0.0" {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(HardcodedBindAllInterfaces, string.range()));
|
||||
}
|
||||
}
|
||||
StringLike::FString(ast::ExprFString { value, .. }) => {
|
||||
for part in value {
|
||||
match part {
|
||||
ast::FStringPart::Literal(literal) => {
|
||||
if &**literal == "0.0.0.0" {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(HardcodedBindAllInterfaces, literal.range()));
|
||||
}
|
||||
}
|
||||
ast::FStringPart::FString(f_string) => {
|
||||
for literal in f_string.literals() {
|
||||
if &**literal == "0.0.0.0" {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
HardcodedBindAllInterfaces,
|
||||
literal.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
StringLike::Bytes(_) => (),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use ruff_python_ast::{self as ast, Expr, StringLike};
|
||||
use ruff_text_size::Ranged;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
@ -53,12 +53,29 @@ impl Violation for HardcodedTempFile {
|
|||
|
||||
/// S108
|
||||
pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, string: StringLike) {
|
||||
let value = match string {
|
||||
StringLike::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.to_str(),
|
||||
StringLike::FStringLiteral(ast::FStringLiteralElement { value, .. }) => value,
|
||||
StringLike::BytesLiteral(_) => return,
|
||||
};
|
||||
match string {
|
||||
StringLike::String(ast::ExprStringLiteral { value, .. }) => {
|
||||
check(checker, value.to_str(), string.range());
|
||||
}
|
||||
StringLike::FString(ast::ExprFString { value, .. }) => {
|
||||
for part in value {
|
||||
match part {
|
||||
ast::FStringPart::Literal(literal) => {
|
||||
check(checker, literal, literal.range());
|
||||
}
|
||||
ast::FStringPart::FString(f_string) => {
|
||||
for literal in f_string.literals() {
|
||||
check(checker, literal, literal.range());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
StringLike::Bytes(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn check(checker: &mut Checker, value: &str, range: TextRange) {
|
||||
if !checker
|
||||
.settings
|
||||
.flake8_bandit
|
||||
|
@ -85,6 +102,6 @@ pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, string: StringLike)
|
|||
HardcodedTempFile {
|
||||
string: value.to_string(),
|
||||
},
|
||||
string.range(),
|
||||
range,
|
||||
));
|
||||
}
|
||||
|
|
|
@ -42,4 +42,23 @@ S104.py:19:9: S104 Possible binding to all interfaces
|
|||
20 | print(x)
|
||||
|
|
||||
|
||||
S104.py:24:1: S104 Possible binding to all interfaces
|
||||
|
|
||||
23 | # Implicit string concatenation
|
||||
24 | "0.0.0.0" f"0.0.0.0{expr}0.0.0.0"
|
||||
| ^^^^^^^^^ S104
|
||||
|
|
||||
|
||||
S104.py:24:13: S104 Possible binding to all interfaces
|
||||
|
|
||||
23 | # Implicit string concatenation
|
||||
24 | "0.0.0.0" f"0.0.0.0{expr}0.0.0.0"
|
||||
| ^^^^^^^ S104
|
||||
|
|
||||
|
||||
S104.py:24:26: S104 Possible binding to all interfaces
|
||||
|
|
||||
23 | # Implicit string concatenation
|
||||
24 | "0.0.0.0" f"0.0.0.0{expr}0.0.0.0"
|
||||
| ^^^^^^^ S104
|
||||
|
|
||||
|
|
|
@ -37,4 +37,28 @@ S108.py:14:11: S108 Probable insecure usage of temporary file or directory: "/de
|
|||
15 | f.write("def")
|
||||
|
|
||||
|
||||
S108.py:22:11: S108 Probable insecure usage of temporary file or directory: "/tmp/abc"
|
||||
|
|
||||
21 | # Implicit string concatenation
|
||||
22 | with open("/tmp/" "abc", "w") as f:
|
||||
| ^^^^^^^^^^^^^ S108
|
||||
23 | f.write("def")
|
||||
|
|
||||
|
||||
S108.py:25:11: S108 Probable insecure usage of temporary file or directory: "/tmp/abc"
|
||||
|
|
||||
23 | f.write("def")
|
||||
24 |
|
||||
25 | with open("/tmp/abc" f"/tmp/abc", "w") as f:
|
||||
| ^^^^^^^^^^ S108
|
||||
26 | f.write("def")
|
||||
|
|
||||
|
||||
S108.py:25:24: S108 Probable insecure usage of temporary file or directory: "/tmp/abc"
|
||||
|
|
||||
23 | f.write("def")
|
||||
24 |
|
||||
25 | with open("/tmp/abc" f"/tmp/abc", "w") as f:
|
||||
| ^^^^^^^^ S108
|
||||
26 | f.write("def")
|
||||
|
|
||||
|
|
|
@ -45,4 +45,28 @@ S108.py:18:11: S108 Probable insecure usage of temporary file or directory: "/fo
|
|||
19 | f.write("def")
|
||||
|
|
||||
|
||||
S108.py:22:11: S108 Probable insecure usage of temporary file or directory: "/tmp/abc"
|
||||
|
|
||||
21 | # Implicit string concatenation
|
||||
22 | with open("/tmp/" "abc", "w") as f:
|
||||
| ^^^^^^^^^^^^^ S108
|
||||
23 | f.write("def")
|
||||
|
|
||||
|
||||
S108.py:25:11: S108 Probable insecure usage of temporary file or directory: "/tmp/abc"
|
||||
|
|
||||
23 | f.write("def")
|
||||
24 |
|
||||
25 | with open("/tmp/abc" f"/tmp/abc", "w") as f:
|
||||
| ^^^^^^^^^^ S108
|
||||
26 | f.write("def")
|
||||
|
|
||||
|
||||
S108.py:25:24: S108 Probable insecure usage of temporary file or directory: "/tmp/abc"
|
||||
|
|
||||
23 | f.write("def")
|
||||
24 |
|
||||
25 | with open("/tmp/abc" f"/tmp/abc", "w") as f:
|
||||
| ^^^^^^^^ S108
|
||||
26 | f.write("def")
|
||||
|
|
||||
|
|
|
@ -57,11 +57,9 @@ pub(crate) fn string_or_bytes_too_long(checker: &mut Checker, string: StringLike
|
|||
}
|
||||
|
||||
let length = match string {
|
||||
StringLike::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.chars().count(),
|
||||
StringLike::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => value.len(),
|
||||
StringLike::FStringLiteral(ast::FStringLiteralElement { value, .. }) => {
|
||||
value.chars().count()
|
||||
}
|
||||
StringLike::String(ast::ExprStringLiteral { value, .. }) => value.chars().count(),
|
||||
StringLike::Bytes(ast::ExprBytesLiteral { value, .. }) => value.len(),
|
||||
StringLike::FString(node) => count_f_string_chars(node),
|
||||
};
|
||||
if length <= 50 {
|
||||
return;
|
||||
|
@ -75,6 +73,26 @@ pub(crate) fn string_or_bytes_too_long(checker: &mut Checker, string: StringLike
|
|||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
/// Count the number of visible characters in an f-string. This accounts for
|
||||
/// implicitly concatenated f-strings as well.
|
||||
fn count_f_string_chars(f_string: &ast::ExprFString) -> usize {
|
||||
f_string
|
||||
.value
|
||||
.iter()
|
||||
.map(|part| match part {
|
||||
ast::FStringPart::Literal(string) => string.chars().count(),
|
||||
ast::FStringPart::FString(f_string) => f_string
|
||||
.elements
|
||||
.iter()
|
||||
.map(|element| match element {
|
||||
ast::FStringElement::Literal(string) => string.chars().count(),
|
||||
ast::FStringElement::Expression(expr) => expr.range().len().to_usize(),
|
||||
})
|
||||
.sum(),
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn is_warnings_dot_deprecated(expr: Option<&ast::Expr>, semantic: &SemanticModel) -> bool {
|
||||
// Does `expr` represent a call to `warnings.deprecated` or `typing_extensions.deprecated`?
|
||||
let Some(expr) = expr else {
|
||||
|
|
|
@ -105,12 +105,12 @@ PYI053.pyi:34:14: PYI053 [*] String and bytes literals longer than 50 characters
|
|||
36 36 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
|
||||
37 37 |
|
||||
|
||||
PYI053.pyi:38:15: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
|
||||
PYI053.pyi:38:13: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
|
||||
|
|
||||
36 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
|
||||
37 |
|
||||
38 | fbar: str = f"51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053
|
||||
39 |
|
||||
40 | class Demo:
|
||||
|
|
||||
|
@ -121,7 +121,7 @@ PYI053.pyi:38:15: PYI053 [*] String and bytes literals longer than 50 characters
|
|||
36 36 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
|
||||
37 37 |
|
||||
38 |-fbar: str = f"51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053
|
||||
38 |+fbar: str = f"..." # Error: PYI053
|
||||
38 |+fbar: str = ... # Error: PYI053
|
||||
39 39 |
|
||||
40 40 | class Demo:
|
||||
41 41 | """Docstrings are excluded from this rule. Some padding.""" # OK
|
||||
|
@ -144,5 +144,20 @@ PYI053.pyi:64:5: PYI053 [*] String and bytes literals longer than 50 characters
|
|||
64 |+ ... # Error: PYI053
|
||||
65 65 | )
|
||||
66 66 | def not_a_deprecated_function() -> None: ...
|
||||
67 67 |
|
||||
|
||||
PYI053.pyi:68:13: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
|
||||
|
|
||||
66 | def not_a_deprecated_function() -> None: ...
|
||||
67 |
|
||||
68 | fbaz: str = f"51 character {foo} stringgggggggggggggggggggggggggg" # Error: PYI053
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053
|
||||
|
|
||||
= help: Replace with `...`
|
||||
|
||||
ℹ Safe fix
|
||||
65 65 | )
|
||||
66 66 | def not_a_deprecated_function() -> None: ...
|
||||
67 67 |
|
||||
68 |-fbaz: str = f"51 character {foo} stringgggggggggggggggggggggggggg" # Error: PYI053
|
||||
68 |+fbaz: str = ... # Error: PYI053
|
||||
|
|
|
@ -4,7 +4,7 @@ use bitflags::bitflags;
|
|||
|
||||
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::StringLike;
|
||||
use ruff_python_ast::{self as ast, StringLike};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
|
||||
|
@ -193,29 +193,47 @@ pub(crate) fn ambiguous_unicode_character_string(checker: &mut Checker, string_l
|
|||
};
|
||||
|
||||
match string_like {
|
||||
StringLike::StringLiteral(string_literal) => {
|
||||
for string in &string_literal.value {
|
||||
let text = checker.locator().slice(string);
|
||||
StringLike::String(node) => {
|
||||
for literal in &node.value {
|
||||
let text = checker.locator().slice(literal);
|
||||
ambiguous_unicode_character(
|
||||
&mut checker.diagnostics,
|
||||
text,
|
||||
string.range(),
|
||||
literal.range(),
|
||||
context,
|
||||
checker.settings,
|
||||
);
|
||||
}
|
||||
}
|
||||
StringLike::FStringLiteral(f_string_literal) => {
|
||||
let text = checker.locator().slice(f_string_literal);
|
||||
StringLike::FString(node) => {
|
||||
for part in &node.value {
|
||||
match part {
|
||||
ast::FStringPart::Literal(literal) => {
|
||||
let text = checker.locator().slice(literal);
|
||||
ambiguous_unicode_character(
|
||||
&mut checker.diagnostics,
|
||||
text,
|
||||
f_string_literal.range(),
|
||||
literal.range(),
|
||||
context,
|
||||
checker.settings,
|
||||
);
|
||||
}
|
||||
StringLike::BytesLiteral(_) => (),
|
||||
ast::FStringPart::FString(f_string) => {
|
||||
for literal in f_string.literals() {
|
||||
let text = checker.locator().slice(literal);
|
||||
ambiguous_unicode_character(
|
||||
&mut checker.diagnostics,
|
||||
text,
|
||||
literal.range(),
|
||||
context,
|
||||
checker.settings,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
StringLike::Bytes(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -155,4 +155,16 @@ confusables.py:46:62: RUF003 Comment contains ambiguous `᜵` (PHILIPPINE SINGLE
|
|||
47 | }"
|
||||
|
|
||||
|
||||
confusables.py:58:6: RUF001 String contains ambiguous `𝐁` (MATHEMATICAL BOLD CAPITAL B). Did you mean `B` (LATIN CAPITAL LETTER B)?
|
||||
|
|
||||
57 | # Implicit string concatenation
|
||||
58 | x = "𝐁ad" f"𝐁ad string"
|
||||
| ^ RUF001
|
||||
|
|
||||
|
||||
confusables.py:58:13: RUF001 String contains ambiguous `𝐁` (MATHEMATICAL BOLD CAPITAL B). Did you mean `B` (LATIN CAPITAL LETTER B)?
|
||||
|
|
||||
57 | # Implicit string concatenation
|
||||
58 | x = "𝐁ad" f"𝐁ad string"
|
||||
| ^ RUF001
|
||||
|
|
||||
|
|
|
@ -159,6 +159,20 @@ confusables.py:55:28: RUF001 String contains ambiguous `µ` (MICRO SIGN). Did yo
|
|||
|
|
||||
55 | assert getattr(Labware(), "µL") == 1.5
|
||||
| ^ RUF001
|
||||
56 |
|
||||
57 | # Implicit string concatenation
|
||||
|
|
||||
|
||||
confusables.py:58:6: RUF001 String contains ambiguous `𝐁` (MATHEMATICAL BOLD CAPITAL B). Did you mean `B` (LATIN CAPITAL LETTER B)?
|
||||
|
|
||||
57 | # Implicit string concatenation
|
||||
58 | x = "𝐁ad" f"𝐁ad string"
|
||||
| ^ RUF001
|
||||
|
|
||||
|
||||
confusables.py:58:13: RUF001 String contains ambiguous `𝐁` (MATHEMATICAL BOLD CAPITAL B). Did you mean `B` (LATIN CAPITAL LETTER B)?
|
||||
|
|
||||
57 | # Implicit string concatenation
|
||||
58 | x = "𝐁ad" f"𝐁ad string"
|
||||
| ^ RUF001
|
||||
|
|
||||
|
|
|
@ -399,35 +399,35 @@ impl LiteralExpressionRef<'_> {
|
|||
/// f-strings.
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum StringLike<'a> {
|
||||
StringLiteral(&'a ast::ExprStringLiteral),
|
||||
BytesLiteral(&'a ast::ExprBytesLiteral),
|
||||
FStringLiteral(&'a ast::FStringLiteralElement),
|
||||
String(&'a ast::ExprStringLiteral),
|
||||
Bytes(&'a ast::ExprBytesLiteral),
|
||||
FString(&'a ast::ExprFString),
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::ExprStringLiteral> for StringLike<'a> {
|
||||
fn from(value: &'a ast::ExprStringLiteral) -> Self {
|
||||
StringLike::StringLiteral(value)
|
||||
StringLike::String(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::ExprBytesLiteral> for StringLike<'a> {
|
||||
fn from(value: &'a ast::ExprBytesLiteral) -> Self {
|
||||
StringLike::BytesLiteral(value)
|
||||
StringLike::Bytes(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::FStringLiteralElement> for StringLike<'a> {
|
||||
fn from(value: &'a ast::FStringLiteralElement) -> Self {
|
||||
StringLike::FStringLiteral(value)
|
||||
impl<'a> From<&'a ast::ExprFString> for StringLike<'a> {
|
||||
fn from(value: &'a ast::ExprFString) -> Self {
|
||||
StringLike::FString(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for StringLike<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
match self {
|
||||
StringLike::StringLiteral(literal) => literal.range(),
|
||||
StringLike::BytesLiteral(literal) => literal.range(),
|
||||
StringLike::FStringLiteral(literal) => literal.range(),
|
||||
StringLike::String(literal) => literal.range(),
|
||||
StringLike::Bytes(literal) => literal.range(),
|
||||
StringLike::FString(literal) => literal.range(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue