Skip to content

Commit 96ae9fe

Browse files
authored
Introduce StringLike enum (#9016)
## Summary This PR introduces a new `StringLike` enum which is a narrow type to indicate string-like nodes. These includes the string literals, bytes literals, and the literal parts of f-strings. The main motivation behind this is to avoid repetition of rule calling in the AST checker. We add a new `analyze::string_like` function which takes in the enum and calls all the respective rule functions which expects atleast 2 of the variants of this enum. I'm open to discarding this if others think it's not that useful at this stage as currently only 3 rules require these nodes. As suggested [here](#8835 (comment)) and [here](#8835 (comment)). ## Test Plan `cargo test`
1 parent cdac90e commit 96ae9fe

File tree

8 files changed

+108
-72
lines changed

8 files changed

+108
-72
lines changed

crates/ruff_linter/src/checkers/ast/analyze/expression.rs

Lines changed: 1 addition & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ use ruff_python_literal::cformat::{CFormatError, CFormatErrorType};
44
use ruff_diagnostics::Diagnostic;
55

66
use ruff_python_ast::types::Node;
7-
use ruff_python_ast::AstNode;
87
use ruff_python_semantic::analyze::typing;
98
use ruff_python_semantic::ScopeKind;
109
use ruff_text_size::Ranged;
@@ -1007,30 +1006,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
10071006
pyupgrade::rules::unicode_kind_prefix(checker, string_literal);
10081007
}
10091008
}
1010-
for literal in value.elements().filter_map(|element| element.as_literal()) {
1011-
if checker.enabled(Rule::HardcodedBindAllInterfaces) {
1012-
flake8_bandit::rules::hardcoded_bind_all_interfaces(
1013-
checker,
1014-
&literal.value,
1015-
literal.range,
1016-
);
1017-
}
1018-
if checker.enabled(Rule::HardcodedTempFile) {
1019-
flake8_bandit::rules::hardcoded_tmp_directory(
1020-
checker,
1021-
&literal.value,
1022-
literal.range,
1023-
);
1024-
}
1025-
if checker.source_type.is_stub() {
1026-
if checker.enabled(Rule::StringOrBytesTooLong) {
1027-
flake8_pyi::rules::string_or_bytes_too_long(
1028-
checker,
1029-
literal.as_any_node_ref(),
1030-
);
1031-
}
1032-
}
1033-
}
10341009
}
10351010
Expr::BinOp(ast::ExprBinOp {
10361011
left,
@@ -1295,38 +1270,12 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
12951270
refurb::rules::math_constant(checker, number_literal);
12961271
}
12971272
}
1298-
Expr::BytesLiteral(bytes_literal) => {
1299-
if checker.source_type.is_stub() && checker.enabled(Rule::StringOrBytesTooLong) {
1300-
flake8_pyi::rules::string_or_bytes_too_long(
1301-
checker,
1302-
bytes_literal.as_any_node_ref(),
1303-
);
1304-
}
1305-
}
1306-
Expr::StringLiteral(string_literal @ ast::ExprStringLiteral { value, range }) => {
1307-
if checker.enabled(Rule::HardcodedBindAllInterfaces) {
1308-
flake8_bandit::rules::hardcoded_bind_all_interfaces(
1309-
checker,
1310-
value.to_str(),
1311-
*range,
1312-
);
1313-
}
1314-
if checker.enabled(Rule::HardcodedTempFile) {
1315-
flake8_bandit::rules::hardcoded_tmp_directory(checker, value.to_str(), *range);
1316-
}
1273+
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
13171274
if checker.enabled(Rule::UnicodeKindPrefix) {
13181275
for string_part in value.parts() {
13191276
pyupgrade::rules::unicode_kind_prefix(checker, string_part);
13201277
}
13211278
}
1322-
if checker.source_type.is_stub() {
1323-
if checker.enabled(Rule::StringOrBytesTooLong) {
1324-
flake8_pyi::rules::string_or_bytes_too_long(
1325-
checker,
1326-
string_literal.as_any_node_ref(),
1327-
);
1328-
}
1329-
}
13301279
}
13311280
Expr::IfExp(
13321281
if_exp @ ast::ExprIfExp {

crates/ruff_linter/src/checkers/ast/analyze/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pub(super) use module::module;
1010
pub(super) use parameter::parameter;
1111
pub(super) use parameters::parameters;
1212
pub(super) use statement::statement;
13+
pub(super) use string_like::string_like;
1314
pub(super) use suite::suite;
1415
pub(super) use unresolved_references::unresolved_references;
1516

@@ -25,5 +26,6 @@ mod module;
2526
mod parameter;
2627
mod parameters;
2728
mod statement;
29+
mod string_like;
2830
mod suite;
2931
mod unresolved_references;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use ruff_python_ast::StringLike;
2+
3+
use crate::checkers::ast::Checker;
4+
use crate::codes::Rule;
5+
use crate::rules::{flake8_bandit, flake8_pyi};
6+
7+
/// Run lint rules over a [`StringLike`] syntax nodes.
8+
pub(crate) fn string_like(string_like: StringLike, checker: &mut Checker) {
9+
if checker.enabled(Rule::HardcodedBindAllInterfaces) {
10+
flake8_bandit::rules::hardcoded_bind_all_interfaces(checker, string_like);
11+
}
12+
if checker.enabled(Rule::HardcodedTempFile) {
13+
flake8_bandit::rules::hardcoded_tmp_directory(checker, string_like);
14+
}
15+
if checker.source_type.is_stub() {
16+
if checker.enabled(Rule::StringOrBytesTooLong) {
17+
flake8_pyi::rules::string_or_bytes_too_long(checker, string_like);
18+
}
19+
}
20+
}

crates/ruff_linter/src/checkers/ast/mod.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ use ruff_python_ast::helpers::{
4444
};
4545
use ruff_python_ast::identifier::Identifier;
4646
use ruff_python_ast::str::trailing_quote;
47-
use ruff_python_ast::visitor::{walk_except_handler, walk_pattern, Visitor};
47+
use ruff_python_ast::visitor::{walk_except_handler, walk_f_string_element, walk_pattern, Visitor};
4848
use ruff_python_ast::{helpers, str, visitor, PySourceType};
4949
use ruff_python_codegen::{Generator, Quote, Stylist};
5050
use ruff_python_index::Indexer;
@@ -1267,6 +1267,13 @@ where
12671267

12681268
// Step 4: Analysis
12691269
analyze::expression(expr, self);
1270+
match expr {
1271+
Expr::StringLiteral(string_literal) => {
1272+
analyze::string_like(string_literal.into(), self);
1273+
}
1274+
Expr::BytesLiteral(bytes_literal) => analyze::string_like(bytes_literal.into(), self),
1275+
_ => {}
1276+
}
12701277

12711278
self.semantic.flags = flags_snapshot;
12721279
self.semantic.pop_node();
@@ -1431,6 +1438,16 @@ where
14311438
.push((bound, self.semantic.snapshot()));
14321439
}
14331440
}
1441+
1442+
fn visit_f_string_element(&mut self, f_string_element: &'b ast::FStringElement) {
1443+
// Step 2: Traversal
1444+
walk_f_string_element(self, f_string_element);
1445+
1446+
// Step 4: Analysis
1447+
if let Some(literal) = f_string_element.as_literal() {
1448+
analyze::string_like(literal.into(), self);
1449+
}
1450+
}
14341451
}
14351452

14361453
impl<'a> Checker<'a> {

crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use ruff_diagnostics::{Diagnostic, Violation};
22
use ruff_macros::{derive_message_formats, violation};
3-
use ruff_text_size::TextRange;
3+
use ruff_python_ast::{self as ast, StringLike};
4+
use ruff_text_size::Ranged;
45

56
use crate::checkers::ast::Checker;
67

@@ -36,10 +37,16 @@ impl Violation for HardcodedBindAllInterfaces {
3637
}
3738

3839
/// S104
39-
pub(crate) fn hardcoded_bind_all_interfaces(checker: &mut Checker, value: &str, range: TextRange) {
40-
if value == "0.0.0.0" {
40+
pub(crate) fn hardcoded_bind_all_interfaces(checker: &mut Checker, string: StringLike) {
41+
let is_bind_all_interface = match string {
42+
StringLike::StringLiteral(ast::ExprStringLiteral { value, .. }) => value == "0.0.0.0",
43+
StringLike::FStringLiteral(ast::FStringLiteralElement { value, .. }) => value == "0.0.0.0",
44+
StringLike::BytesLiteral(_) => return,
45+
};
46+
47+
if is_bind_all_interface {
4148
checker
4249
.diagnostics
43-
.push(Diagnostic::new(HardcodedBindAllInterfaces, range));
50+
.push(Diagnostic::new(HardcodedBindAllInterfaces, string.range()));
4451
}
4552
}

crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
use ruff_python_ast::{self as ast, Expr};
2-
use ruff_text_size::TextRange;
1+
use ruff_python_ast::{self as ast, Expr, StringLike};
2+
use ruff_text_size::Ranged;
33

44
use ruff_diagnostics::{Diagnostic, Violation};
55
use ruff_macros::{derive_message_formats, violation};
@@ -52,7 +52,13 @@ impl Violation for HardcodedTempFile {
5252
}
5353

5454
/// S108
55-
pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, value: &str, range: TextRange) {
55+
pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, string: StringLike) {
56+
let value = match string {
57+
StringLike::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.to_str(),
58+
StringLike::FStringLiteral(ast::FStringLiteralElement { value, .. }) => value,
59+
StringLike::BytesLiteral(_) => return,
60+
};
61+
5662
if !checker
5763
.settings
5864
.flake8_bandit
@@ -79,6 +85,6 @@ pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, value: &str, range:
7985
HardcodedTempFile {
8086
string: value.to_string(),
8187
},
82-
range,
88+
string.range(),
8389
));
8490
}

crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
22
use ruff_macros::{derive_message_formats, violation};
33
use ruff_python_ast::helpers::is_docstring_stmt;
4-
use ruff_python_ast::{self as ast, AnyNodeRef};
4+
use ruff_python_ast::{self as ast, StringLike};
55
use ruff_text_size::Ranged;
66

77
use crate::checkers::ast::Checker;
@@ -43,30 +43,27 @@ impl AlwaysFixableViolation for StringOrBytesTooLong {
4343
}
4444

4545
/// PYI053
46-
pub(crate) fn string_or_bytes_too_long(checker: &mut Checker, node: AnyNodeRef) {
46+
pub(crate) fn string_or_bytes_too_long(checker: &mut Checker, string: StringLike) {
4747
// Ignore docstrings.
4848
if is_docstring_stmt(checker.semantic().current_statement()) {
4949
return;
5050
}
5151

52-
let length = match node {
53-
AnyNodeRef::ExprStringLiteral(ast::ExprStringLiteral { value, .. }) => {
52+
let length = match string {
53+
StringLike::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.chars().count(),
54+
StringLike::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => value.len(),
55+
StringLike::FStringLiteral(ast::FStringLiteralElement { value, .. }) => {
5456
value.chars().count()
5557
}
56-
AnyNodeRef::ExprBytesLiteral(ast::ExprBytesLiteral { value, .. }) => value.len(),
57-
AnyNodeRef::FStringLiteralElement(ast::FStringLiteralElement { value, .. }) => {
58-
value.chars().count()
59-
}
60-
_ => return,
6158
};
6259
if length <= 50 {
6360
return;
6461
}
6562

66-
let mut diagnostic = Diagnostic::new(StringOrBytesTooLong, node.range());
63+
let mut diagnostic = Diagnostic::new(StringOrBytesTooLong, string.range());
6764
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
6865
"...".to_string(),
69-
node.range(),
66+
string.range(),
7067
)));
7168
checker.diagnostics.push(diagnostic);
7269
}

crates/ruff_python_ast/src/expression.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,3 +393,41 @@ impl LiteralExpressionRef<'_> {
393393
}
394394
}
395395
}
396+
397+
/// An enum that holds a reference to a string-like literal from the AST.
398+
/// This includes string literals, bytes literals, and the literal parts of
399+
/// f-strings.
400+
#[derive(Copy, Clone, Debug, PartialEq)]
401+
pub enum StringLike<'a> {
402+
StringLiteral(&'a ast::ExprStringLiteral),
403+
BytesLiteral(&'a ast::ExprBytesLiteral),
404+
FStringLiteral(&'a ast::FStringLiteralElement),
405+
}
406+
407+
impl<'a> From<&'a ast::ExprStringLiteral> for StringLike<'a> {
408+
fn from(value: &'a ast::ExprStringLiteral) -> Self {
409+
StringLike::StringLiteral(value)
410+
}
411+
}
412+
413+
impl<'a> From<&'a ast::ExprBytesLiteral> for StringLike<'a> {
414+
fn from(value: &'a ast::ExprBytesLiteral) -> Self {
415+
StringLike::BytesLiteral(value)
416+
}
417+
}
418+
419+
impl<'a> From<&'a ast::FStringLiteralElement> for StringLike<'a> {
420+
fn from(value: &'a ast::FStringLiteralElement) -> Self {
421+
StringLike::FStringLiteral(value)
422+
}
423+
}
424+
425+
impl Ranged for StringLike<'_> {
426+
fn range(&self) -> TextRange {
427+
match self {
428+
StringLike::StringLiteral(literal) => literal.range(),
429+
StringLike::BytesLiteral(literal) => literal.range(),
430+
StringLike::FStringLiteral(literal) => literal.range(),
431+
}
432+
}
433+
}

0 commit comments

Comments
 (0)