Skip to content

Commit 69fa356

Browse files
committed
Lint on c_str!("literal")
1 parent 133254d commit 69fa356

7 files changed

Lines changed: 198 additions & 1 deletion

File tree

src/diagnostic_items/out_of_band.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Out-of-band attributes attached without source code changes.
22
3-
use rustc_hir::def_id::{DefId, LOCAL_CRATE};
3+
use rustc_hir::def::{DefKind, Res};
4+
use rustc_hir::def_id::{CRATE_DEF_ID, DefId, LOCAL_CRATE};
45
use rustc_hir::diagnostic_items::DiagnosticItems;
56
use rustc_middle::middle::exported_symbols::ExportedSymbol;
67
use rustc_middle::ty::TyCtxt;
@@ -11,6 +12,12 @@ pub fn infer_missing_items<'tcx>(tcx: TyCtxt<'tcx>, items: &mut DiagnosticItems)
1112
super::collect_item(tcx, items, crate::symbol::build_error, def_id);
1213
}
1314
}
15+
16+
if !items.name_to_id.contains_key(&crate::symbol::c_str) {
17+
if let Some(def_id) = infer_c_str_diagnostic_item(tcx) {
18+
super::collect_item(tcx, items, crate::symbol::c_str, def_id);
19+
}
20+
}
1421
}
1522

1623
pub fn infer_build_error_diagnostic_item<'tcx>(tcx: TyCtxt<'tcx>) -> Option<DefId> {
@@ -24,3 +31,22 @@ pub fn infer_build_error_diagnostic_item<'tcx>(tcx: TyCtxt<'tcx>) -> Option<DefI
2431

2532
None
2633
}
34+
35+
pub fn infer_c_str_diagnostic_item<'tcx>(tcx: TyCtxt<'tcx>) -> Option<DefId> {
36+
let name = tcx.crate_name(LOCAL_CRATE);
37+
38+
if name != crate::symbol::kernel {
39+
return None;
40+
}
41+
42+
let c_str = tcx
43+
.module_children_local(CRATE_DEF_ID)
44+
.iter()
45+
.find(|c| {
46+
c.ident.name == crate::symbol::c_str && matches!(c.res, Res::Def(DefKind::Macro(_), _))
47+
})?
48+
.res
49+
.def_id();
50+
51+
Some(c_str)
52+
}

src/hir_lints/c_str_literal.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
use rustc_hir::Expr;
2+
use rustc_lint::{LateContext, LateLintPass, LintContext};
3+
use rustc_session::{declare_tool_lint, impl_lint_pass};
4+
use rustc_span::{self, Span, Symbol};
5+
6+
use crate::ctxt::AnalysisCtxt;
7+
8+
declare_tool_lint! {
9+
/// The `c_str_literal` lint detects when the kernel `c_str!` macro is used on a string literal.
10+
pub klint::C_STR_LITERAL,
11+
Warn,
12+
"`c_str!` used on a string literal"
13+
}
14+
15+
pub struct CStrLiteralLint<'tcx> {
16+
pub cx: &'tcx AnalysisCtxt<'tcx>,
17+
}
18+
19+
impl_lint_pass!(CStrLiteralLint<'_> => [C_STR_LITERAL]);
20+
21+
#[derive(Diagnostic)]
22+
#[diag("`{$macro_name}!` is used on a literal")]
23+
struct CStrLiteral {
24+
#[primary_span]
25+
#[suggestion(
26+
"use C-string literals instead",
27+
code = "c{arg}",
28+
applicability = "machine-applicable"
29+
)]
30+
pub span: Span,
31+
pub macro_name: Symbol,
32+
pub arg: String,
33+
}
34+
35+
impl<'tcx> CStrLiteralLint<'tcx> {
36+
fn extract_arg(&self, span: Span) -> Option<String> {
37+
let source = self.cx.sess.source_map().span_to_snippet(span).ok()?;
38+
39+
let arg = source.split_once('!')?.1;
40+
if arg.len() <= 2 {
41+
return None;
42+
}
43+
44+
Some(arg[1..arg.len() - 1].trim().to_owned())
45+
}
46+
}
47+
48+
impl<'tcx> LateLintPass<'tcx> for CStrLiteralLint<'tcx> {
49+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
50+
// If would be ideal if we can check before macro expansion. However, pre_expansion_lint is
51+
// not recommended because the lint level infrastructure if not yet ready, and also it does
52+
// not have information of the name resolution available so we cannot precisely determine if
53+
// the macro is something that we want to lint now.
54+
//
55+
// So, as an alternative strategy, try to backtrace macros and find an expression that is expanded
56+
// from the `c_str!` macro. Once that found, use span information to recover the argument used to
57+
// call the macro.
58+
//
59+
// However, given that `c_str!` have used `concat!()` internally, there is no single span in the
60+
// expanded HIR that maps to the input argument... As a very crude approximation, use source map
61+
// to obtain the source and check based on that. This is also very hacky but at least lint level
62+
// and name resolution works as intended...
63+
64+
// TODO: This check will replicated multiple times for each sub-expression of `c_str!`.
65+
// It might be good if we stop early and stop recursing into sub-expressions, although this is not
66+
// something that can be achieved with `LateLintPass`.
67+
let span = expr.span;
68+
let Some(expn_data) = span.macro_backtrace().next() else {
69+
return;
70+
};
71+
72+
let Some(c_str) = self.cx.get_klint_diagnostic_item(crate::symbol::c_str) else {
73+
return;
74+
};
75+
76+
if expn_data.macro_def_id != Some(c_str) {
77+
return;
78+
}
79+
80+
if let Some(arg) = self.extract_arg(expn_data.call_site) {
81+
if arg.starts_with('"') && arg.ends_with('"') {
82+
cx.emit_diag_lint(
83+
C_STR_LITERAL,
84+
CStrLiteral {
85+
span: expn_data.call_site,
86+
macro_name: self.cx.item_name(c_str),
87+
arg,
88+
},
89+
);
90+
}
91+
}
92+
}
93+
}

src/hir_lints/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
pub(crate) mod c_str_literal;
12
pub(crate) mod not_using_prelude;

src/main.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,16 @@ impl Callbacks for MyCallbacks {
117117
infallible_allocation::INFALLIBLE_ALLOCATION,
118118
atomic_context::ATOMIC_CONTEXT,
119119
binary_analysis::stack_size::STACK_FRAME_TOO_LARGE,
120+
hir_lints::c_str_literal::C_STR_LITERAL,
120121
hir_lints::not_using_prelude::NOT_USING_PRELUDE,
121122
]);
122123

124+
lint_store.register_late_pass(|tcx| {
125+
Box::new(hir_lints::c_str_literal::CStrLiteralLint {
126+
cx: driver::cx::<MyCallbacks>(tcx),
127+
})
128+
});
129+
123130
lint_store.register_late_pass(|tcx| {
124131
Box::new(hir_lints::not_using_prelude::NotUsingPrelude {
125132
cx: driver::cx::<MyCallbacks>(tcx),

src/symbol.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,16 @@ def! {
4444
sort,
4545
quicksort,
4646
partition,
47+
kernel,
4748
diagnostic_item,
4849

4950
any_context,
5051
atomic_context,
5152
atomic_context_only,
5253
process_context,
5354

55+
// Diagnostic items
56+
c_str,
5457
build_error,
5558

5659
CONFIG_FRAME_WARN,

tests/ui/c_str_literal.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#![deny(klint::c_str_literal)]
2+
3+
#[klint::diagnostic_item = "c_str"]
4+
macro_rules! c_str {
5+
($str:expr) => {{
6+
const S: &str = concat!($str, "\0");
7+
const C: &::core::ffi::CStr = match ::core::ffi::CStr::from_bytes_with_nul(S.as_bytes()) {
8+
Ok(v) => v,
9+
Err(_) => panic!("string contains interior NUL"),
10+
};
11+
C
12+
}};
13+
}
14+
15+
macro_rules! forward_c_str {
16+
($str:expr) => {
17+
c_str!($str)
18+
};
19+
}
20+
21+
macro_rules! wrap_c_str {
22+
() => {
23+
c_str!("hello")
24+
};
25+
}
26+
27+
fn main() {
28+
// Should warn.
29+
c_str!("hello");
30+
31+
// Should not warn.
32+
c_str!(concat!("a", "b"));
33+
// Should not warn.
34+
c_str!(stringify!(hello));
35+
36+
// Should not warn.
37+
forward_c_str!("a");
38+
39+
// Should warn. We currently do not have ability to warn directly at `macro_rules!`; warning
40+
// is only generated once the macro is used.
41+
wrap_c_str!();
42+
}

tests/ui/c_str_literal.stderr

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
error: `c_str!` is used on a literal
2+
--> $DIR/c_str_literal.rs:29:5
3+
|
4+
29 | c_str!("hello");
5+
| ^^^^^^^^^^^^^^^ help: use C-string literals instead: `c"hello"`
6+
|
7+
note: the lint level is defined here
8+
--> $DIR/c_str_literal.rs:1:9
9+
|
10+
1 | #![deny(klint::c_str_literal)]
11+
| ^^^^^^^^^^^^^^^^^^^^
12+
13+
error: `c_str!` is used on a literal
14+
--> $DIR/c_str_literal.rs:23:9
15+
|
16+
23 | c_str!("hello")
17+
| ^^^^^^^^^^^^^^^ help: use C-string literals instead: `c"hello"`
18+
...
19+
41 | wrap_c_str!();
20+
| ------------- in this macro invocation
21+
|
22+
= note: this error originates in the macro `wrap_c_str` (in Nightly builds, run with -Z macro-backtrace for more info)
23+
24+
error: aborting due to 2 previous errors
25+

0 commit comments

Comments
 (0)