Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.

Commit cc26919

Browse files
committed
Add unnecessary symbol string lint
1 parent 76ccfb4 commit cc26919

File tree

6 files changed

+233
-2
lines changed

6 files changed

+233
-2
lines changed

clippy_lints/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,8 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
526526
&utils::internal_lints::OUTER_EXPN_EXPN_DATA,
527527
#[cfg(feature = "internal-lints")]
528528
&utils::internal_lints::PRODUCE_ICE,
529+
#[cfg(feature = "internal-lints")]
530+
&utils::internal_lints::UNNECESSARY_SYMBOL_STR,
529531
&approx_const::APPROX_CONSTANT,
530532
&arithmetic::FLOAT_ARITHMETIC,
531533
&arithmetic::INTEGER_ARITHMETIC,
@@ -1372,6 +1374,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
13721374
LintId::of(&utils::internal_lints::MATCH_TYPE_ON_DIAGNOSTIC_ITEM),
13731375
LintId::of(&utils::internal_lints::OUTER_EXPN_EXPN_DATA),
13741376
LintId::of(&utils::internal_lints::PRODUCE_ICE),
1377+
LintId::of(&utils::internal_lints::UNNECESSARY_SYMBOL_STR),
13751378
]);
13761379

13771380
store.register_group(true, "clippy::all", Some("clippy"), vec![

clippy_lints/src/utils/internal_lints.rs

Lines changed: 151 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ use rustc_hir::def::{DefKind, Res};
1313
use rustc_hir::def_id::DefId;
1414
use rustc_hir::hir_id::CRATE_HIR_ID;
1515
use rustc_hir::intravisit::{NestedVisitorMap, Visitor};
16-
use rustc_hir::{Crate, Expr, ExprKind, HirId, Item, MutTy, Mutability, Node, Path, StmtKind, Ty, TyKind};
16+
use rustc_hir::{
17+
BinOpKind, Crate, Expr, ExprKind, HirId, Item, MutTy, Mutability, Node, Path, StmtKind, Ty, TyKind, UnOp,
18+
};
1719
use rustc_lint::{EarlyContext, EarlyLintPass, LateContext, LateLintPass};
1820
use rustc_middle::hir::map::Map;
1921
use rustc_middle::mir::interpret::ConstValue;
@@ -273,6 +275,28 @@ declare_clippy_lint! {
273275
"interning a symbol that is pre-interned and defined as a constant"
274276
}
275277

278+
declare_clippy_lint! {
279+
/// **What it does:** Checks for unnecessary conversion from Symbol to a string.
280+
///
281+
/// **Why is this bad?** It's faster use symbols directly intead of strings.
282+
///
283+
/// **Known problems:** None.
284+
///
285+
/// **Example:**
286+
/// Bad:
287+
/// ```rust,ignore
288+
/// symbol.as_str() == "clippy";
289+
/// ```
290+
///
291+
/// Good:
292+
/// ```rust,ignore
293+
/// symbol == sym::clippy;
294+
/// ```
295+
pub UNNECESSARY_SYMBOL_STR,
296+
internal,
297+
"unnecessary conversion between Symbol and string"
298+
}
299+
276300
declare_lint_pass!(ClippyLintsInternal => [CLIPPY_LINTS_INTERNAL]);
277301

278302
impl EarlyLintPass for ClippyLintsInternal {
@@ -873,7 +897,7 @@ pub struct InterningDefinedSymbol {
873897
symbol_map: FxHashMap<u32, DefId>,
874898
}
875899

876-
impl_lint_pass!(InterningDefinedSymbol => [INTERNING_DEFINED_SYMBOL]);
900+
impl_lint_pass!(InterningDefinedSymbol => [INTERNING_DEFINED_SYMBOL, UNNECESSARY_SYMBOL_STR]);
877901

878902
impl<'tcx> LateLintPass<'tcx> for InterningDefinedSymbol {
879903
fn check_crate(&mut self, cx: &LateContext<'_>, _: &Crate<'_>) {
@@ -919,5 +943,130 @@ impl<'tcx> LateLintPass<'tcx> for InterningDefinedSymbol {
919943
);
920944
}
921945
}
946+
if let ExprKind::Binary(op, left, right) = expr.kind {
947+
if matches!(op.node, BinOpKind::Eq | BinOpKind::Ne) {
948+
let data = [
949+
(left, self.symbol_str_expr(left, cx)),
950+
(right, self.symbol_str_expr(right, cx)),
951+
];
952+
match data {
953+
// both operands are a symbol string
954+
[(_, Some(left)), (_, Some(right))] => {
955+
span_lint_and_sugg(
956+
cx,
957+
UNNECESSARY_SYMBOL_STR,
958+
expr.span,
959+
"unnecessary `Symbol` to string conversion",
960+
"try",
961+
format!(
962+
"{} {} {}",
963+
left.as_symbol_snippet(cx),
964+
op.node.as_str(),
965+
right.as_symbol_snippet(cx),
966+
),
967+
Applicability::MachineApplicable,
968+
);
969+
},
970+
// one of the operands is a symbol string
971+
[(expr, Some(symbol)), _] | [_, (expr, Some(symbol))] => {
972+
// creating an owned string for comparison
973+
if matches!(symbol, SymbolStrExpr::Expr { is_to_owned: true, .. }) {
974+
span_lint_and_sugg(
975+
cx,
976+
UNNECESSARY_SYMBOL_STR,
977+
expr.span,
978+
"unnecessary string allocation",
979+
"try",
980+
format!("{}.as_str()", symbol.as_symbol_snippet(cx)),
981+
Applicability::MachineApplicable,
982+
);
983+
}
984+
},
985+
// nothing found
986+
[(_, None), (_, None)] => {},
987+
}
988+
}
989+
}
990+
}
991+
}
992+
993+
impl InterningDefinedSymbol {
994+
fn symbol_str_expr<'tcx>(&self, expr: &'tcx Expr<'tcx>, cx: &LateContext<'tcx>) -> Option<SymbolStrExpr<'tcx>> {
995+
static IDENT_STR_PATHS: &[&[&str]] = &[&paths::IDENT_AS_STR, &paths::TO_STRING_METHOD];
996+
static SYMBOL_STR_PATHS: &[&[&str]] = &[
997+
&paths::SYMBOL_AS_STR,
998+
&paths::SYMBOL_TO_IDENT_STRING,
999+
&paths::TO_STRING_METHOD,
1000+
];
1001+
// SymbolStr might be de-referenced: `&*symbol.as_str()`
1002+
let call = if_chain! {
1003+
if let ExprKind::AddrOf(_, _, e) = expr.kind;
1004+
if let ExprKind::Unary(UnOp::UnDeref, e) = e.kind;
1005+
then { e } else { expr }
1006+
};
1007+
if_chain! {
1008+
// is a method call
1009+
if let ExprKind::MethodCall(_, _, [item], _) = call.kind;
1010+
if let Some(did) = cx.typeck_results().type_dependent_def_id(call.hir_id);
1011+
let ty = cx.typeck_results().expr_ty(item);
1012+
// ...on either an Ident or a Symbol
1013+
if let Some(is_ident) = if match_type(cx, ty, &paths::SYMBOL) {
1014+
Some(false)
1015+
} else if match_type(cx, ty, &paths::IDENT) {
1016+
Some(true)
1017+
} else {
1018+
None
1019+
};
1020+
// ...which converts it to a string
1021+
let paths = if is_ident { IDENT_STR_PATHS } else { SYMBOL_STR_PATHS };
1022+
if let Some(path) = paths.iter().find(|path| match_def_path(cx, did, path));
1023+
then {
1024+
let is_to_owned = path.last().unwrap().ends_with("string");
1025+
return Some(SymbolStrExpr::Expr {
1026+
item,
1027+
is_ident,
1028+
is_to_owned,
1029+
});
1030+
}
1031+
}
1032+
// is a string constant
1033+
if let Some(Constant::Str(s)) = constant_simple(cx, cx.typeck_results(), expr) {
1034+
let value = Symbol::intern(&s).as_u32();
1035+
// ...which matches a symbol constant
1036+
if let Some(&def_id) = self.symbol_map.get(&value) {
1037+
return Some(SymbolStrExpr::Const(def_id));
1038+
}
1039+
}
1040+
None
1041+
}
1042+
}
1043+
1044+
enum SymbolStrExpr<'tcx> {
1045+
/// a string constant with a corresponding symbol constant
1046+
Const(DefId),
1047+
/// a "symbol to string" expression like `symbol.as_str()`
1048+
Expr {
1049+
/// part that evaluates to `Symbol` or `Ident`
1050+
item: &'tcx Expr<'tcx>,
1051+
is_ident: bool,
1052+
/// whether an owned `String` is created like `to_ident_string()`
1053+
is_to_owned: bool,
1054+
},
1055+
}
1056+
1057+
impl<'tcx> SymbolStrExpr<'tcx> {
1058+
/// Returns a snippet that evaluates to a `Symbol` and is const if possible
1059+
fn as_symbol_snippet(&self, cx: &LateContext<'_>) -> Cow<'tcx, str> {
1060+
match *self {
1061+
Self::Const(def_id) => cx.tcx.def_path_str(def_id).into(),
1062+
Self::Expr { item, is_ident, .. } => {
1063+
let mut snip = snippet(cx, item.span.source_callsite(), "..");
1064+
if is_ident {
1065+
// get `Ident.name`
1066+
snip.to_mut().push_str(".name");
1067+
}
1068+
snip
1069+
},
1070+
}
9221071
}
9231072
}

clippy_lints/src/utils/paths.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ pub const HASH: [&str; 3] = ["core", "hash", "Hash"];
5454
pub const HASHMAP: [&str; 5] = ["std", "collections", "hash", "map", "HashMap"];
5555
pub const HASHMAP_ENTRY: [&str; 5] = ["std", "collections", "hash", "map", "Entry"];
5656
pub const HASHSET: [&str; 5] = ["std", "collections", "hash", "set", "HashSet"];
57+
#[cfg(feature = "internal-lints")]
58+
pub const IDENT: [&str; 3] = ["rustc_span", "symbol", "Ident"];
59+
#[cfg(feature = "internal-lints")]
60+
pub const IDENT_AS_STR: [&str; 4] = ["rustc_span", "symbol", "Ident", "as_str"];
5761
pub const INDEX: [&str; 3] = ["core", "ops", "Index"];
5862
pub const INDEX_MUT: [&str; 3] = ["core", "ops", "IndexMut"];
5963
pub const INSERT_STR: [&str; 4] = ["alloc", "string", "String", "insert_str"];
@@ -150,8 +154,12 @@ pub const STR_STARTS_WITH: [&str; 4] = ["core", "str", "<impl str>", "starts_wit
150154
#[cfg(feature = "internal-lints")]
151155
pub const SYMBOL: [&str; 3] = ["rustc_span", "symbol", "Symbol"];
152156
#[cfg(feature = "internal-lints")]
157+
pub const SYMBOL_AS_STR: [&str; 4] = ["rustc_span", "symbol", "Symbol", "as_str"];
158+
#[cfg(feature = "internal-lints")]
153159
pub const SYMBOL_INTERN: [&str; 4] = ["rustc_span", "symbol", "Symbol", "intern"];
154160
#[cfg(feature = "internal-lints")]
161+
pub const SYMBOL_TO_IDENT_STRING: [&str; 4] = ["rustc_span", "symbol", "Symbol", "to_ident_string"];
162+
#[cfg(feature = "internal-lints")]
155163
pub const SYM_MODULE: [&str; 3] = ["rustc_span", "symbol", "sym"];
156164
#[cfg(feature = "internal-lints")]
157165
pub const SYNTAX_CONTEXT: [&str; 3] = ["rustc_span", "hygiene", "SyntaxContext"];
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// run-rustfix
2+
#![feature(rustc_private)]
3+
#![deny(clippy::internal)]
4+
#![allow(clippy::unnecessary_operation, unused_must_use)]
5+
6+
extern crate rustc_span;
7+
8+
use rustc_span::symbol::{Ident, Symbol};
9+
10+
fn main() {
11+
Symbol::intern("foo") == rustc_span::sym::clippy;
12+
Symbol::intern("foo") == rustc_span::symbol::kw::SelfLower;
13+
Symbol::intern("foo") != rustc_span::symbol::kw::SelfUpper;
14+
Ident::invalid().name == rustc_span::sym::clippy;
15+
rustc_span::sym::clippy == Ident::invalid().name;
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// run-rustfix
2+
#![feature(rustc_private)]
3+
#![deny(clippy::internal)]
4+
#![allow(clippy::unnecessary_operation, unused_must_use)]
5+
6+
extern crate rustc_span;
7+
8+
use rustc_span::symbol::{Ident, Symbol};
9+
10+
fn main() {
11+
Symbol::intern("foo").as_str() == "clippy";
12+
Symbol::intern("foo").to_string() == "self";
13+
Symbol::intern("foo").to_ident_string() != "Self";
14+
&*Ident::invalid().as_str() == "clippy";
15+
"clippy" == Ident::invalid().to_string();
16+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
error: unnecessary `Symbol` to string conversion
2+
--> $DIR/unnecessary_symbol_str.rs:11:5
3+
|
4+
LL | Symbol::intern("foo").as_str() == "clippy";
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `Symbol::intern("foo") == rustc_span::sym::clippy`
6+
|
7+
note: the lint level is defined here
8+
--> $DIR/unnecessary_symbol_str.rs:3:9
9+
|
10+
LL | #![deny(clippy::internal)]
11+
| ^^^^^^^^^^^^^^^^
12+
= note: `#[deny(clippy::unnecessary_symbol_str)]` implied by `#[deny(clippy::internal)]`
13+
14+
error: unnecessary `Symbol` to string conversion
15+
--> $DIR/unnecessary_symbol_str.rs:12:5
16+
|
17+
LL | Symbol::intern("foo").to_string() == "self";
18+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `Symbol::intern("foo") == rustc_span::symbol::kw::SelfLower`
19+
20+
error: unnecessary `Symbol` to string conversion
21+
--> $DIR/unnecessary_symbol_str.rs:13:5
22+
|
23+
LL | Symbol::intern("foo").to_ident_string() != "Self";
24+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `Symbol::intern("foo") != rustc_span::symbol::kw::SelfUpper`
25+
26+
error: unnecessary `Symbol` to string conversion
27+
--> $DIR/unnecessary_symbol_str.rs:14:5
28+
|
29+
LL | &*Ident::invalid().as_str() == "clippy";
30+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `Ident::invalid().name == rustc_span::sym::clippy`
31+
32+
error: unnecessary `Symbol` to string conversion
33+
--> $DIR/unnecessary_symbol_str.rs:15:5
34+
|
35+
LL | "clippy" == Ident::invalid().to_string();
36+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `rustc_span::sym::clippy == Ident::invalid().name`
37+
38+
error: aborting due to 5 previous errors
39+

0 commit comments

Comments
 (0)