Skip to content

Commit bda8541

Browse files
committed
add call_missing_target_feature lint
1 parent ebcd6bc commit bda8541

File tree

7 files changed

+304
-0
lines changed

7 files changed

+304
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5281,6 +5281,7 @@ Released 2018-09-13
52815281
[`byte_char_slices`]: https://rust-lang.github.io/rust-clippy/master/index.html#byte_char_slices
52825282
[`bytes_count_to_len`]: https://rust-lang.github.io/rust-clippy/master/index.html#bytes_count_to_len
52835283
[`bytes_nth`]: https://rust-lang.github.io/rust-clippy/master/index.html#bytes_nth
5284+
[`call_missing_target_feature`]: https://rust-lang.github.io/rust-clippy/master/index.html#call_missing_target_feature
52845285
[`cargo_common_metadata`]: https://rust-lang.github.io/rust-clippy/master/index.html#cargo_common_metadata
52855286
[`case_sensitive_file_extension_comparisons`]: https://rust-lang.github.io/rust-clippy/master/index.html#case_sensitive_file_extension_comparisons
52865287
[`cast_abs_to_unsigned`]: https://rust-lang.github.io/rust-clippy/master/index.html#cast_abs_to_unsigned
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
#![allow(clippy::similar_names)]
2+
use clippy_utils::diagnostics::span_lint_and_then;
3+
use rustc_hir as hir;
4+
use rustc_hir::def::Res;
5+
use rustc_hir::def_id::DefId;
6+
use rustc_lint::{LateContext, LateLintPass};
7+
use rustc_middle::lint::in_external_macro;
8+
use rustc_session::declare_lint_pass;
9+
use rustc_span::{sym, Symbol};
10+
11+
declare_clippy_lint! {
12+
/// ### What it does
13+
/// Checks that the caller enables the target features that the callee requires
14+
///
15+
/// ### Why is this bad?
16+
/// Not enabling target features can cause UB and limits optimization opportunities.
17+
///
18+
/// ### Example
19+
/// ```no_run
20+
/// #[target_feature(enable = "avx2")]
21+
/// unsafe fn f() -> u32 {
22+
/// 0
23+
/// }
24+
///
25+
/// fn g() {
26+
/// unsafe { f() };
27+
/// // g does not enable the target features f requires
28+
/// }
29+
/// ```
30+
#[clippy::version = "CURRENT_CLIPPY_VERSION"]
31+
pub CALL_MISSING_TARGET_FEATURE,
32+
suspicious,
33+
"call requires target features that the surrounding function does not enable"
34+
}
35+
36+
declare_lint_pass!(CallMissingTargetFeature => [CALL_MISSING_TARGET_FEATURE]);
37+
38+
impl<'tcx> LateLintPass<'tcx> for CallMissingTargetFeature {
39+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &hir::Expr<'tcx>) {
40+
if !in_external_macro(cx.tcx.sess, expr.span) {
41+
let Some(caller_def_id) = caller_def_id(cx, expr) else {
42+
return;
43+
};
44+
let Some(callee_def_id) = callee_def_id(cx, expr) else {
45+
return;
46+
};
47+
48+
let caller_target_features = def_id_target_features(cx, caller_def_id);
49+
let callee_target_features = def_id_target_features(cx, callee_def_id);
50+
51+
let missing: Vec<_> = callee_target_features
52+
.iter()
53+
.filter(|target_feature| !caller_target_features.contains(target_feature))
54+
.map(Symbol::as_str)
55+
.collect();
56+
57+
if missing.is_empty() {
58+
return;
59+
}
60+
61+
let attr = format!("#[target_feature(enable = \"{}\")]", missing.join(","));
62+
63+
#[expect(clippy::collapsible_span_lint_calls, reason = "rust-clippy#7797")]
64+
span_lint_and_then(
65+
cx,
66+
CALL_MISSING_TARGET_FEATURE,
67+
expr.span,
68+
"this call requires target features that the surrounding function does not enable",
69+
|diag| {
70+
diag.span_label(
71+
expr.span,
72+
"this function call requires target features to be enabled".to_string(),
73+
);
74+
75+
if let Some(caller_item) = caller_item(cx, expr) {
76+
let hir::ItemKind::Fn(fn_sig, _, _) = caller_item.kind else {
77+
unreachable!()
78+
};
79+
80+
let mut suggestions = Vec::with_capacity(2);
81+
82+
let Some(indent) = indentation(cx, caller_item.span) else {
83+
return;
84+
};
85+
86+
let lo_span = caller_item.span.with_hi(caller_item.span.lo());
87+
88+
if let hir::Safety::Safe = fn_sig.header.safety {
89+
if caller_item.vis_span.is_empty() {
90+
suggestions.push((lo_span, format!("{attr}\n{indent}unsafe ")));
91+
} else {
92+
suggestions.push((lo_span, format!("{attr}\n{indent}")));
93+
suggestions.push((caller_item.vis_span.shrink_to_hi(), " unsafe".to_string()));
94+
}
95+
}
96+
97+
diag.multipart_suggestion_verbose(
98+
"add the missing target features to the surrounding function",
99+
suggestions,
100+
rustc_errors::Applicability::MaybeIncorrect,
101+
);
102+
}
103+
},
104+
);
105+
}
106+
}
107+
}
108+
109+
fn callee_def_id(cx: &LateContext<'_>, expr: &hir::Expr<'_>) -> Option<DefId> {
110+
match expr.kind {
111+
hir::ExprKind::Call(path, _) => {
112+
if let hir::ExprKind::Path(ref qpath) = path.kind
113+
&& let Res::Def(_, did) = cx.qpath_res(qpath, path.hir_id)
114+
{
115+
Some(did)
116+
} else {
117+
None
118+
}
119+
},
120+
hir::ExprKind::MethodCall(..) => cx.typeck_results().type_dependent_def_id(expr.hir_id),
121+
_ => None,
122+
}
123+
}
124+
125+
fn caller_def_id<'tcx>(cx: &LateContext<'tcx>, expr: &hir::Expr<'tcx>) -> Option<DefId> {
126+
let item = caller_item(cx, expr)?;
127+
Some(item.owner_id.to_def_id())
128+
}
129+
130+
fn caller_item<'tcx>(cx: &LateContext<'tcx>, expr: &hir::Expr<'tcx>) -> Option<&'tcx hir::Item<'tcx>> {
131+
for (_hir_id, node) in cx.tcx.hir().parent_iter(expr.hir_id) {
132+
if let hir::Node::Item(
133+
item @ hir::Item {
134+
kind: hir::ItemKind::Fn(..),
135+
..
136+
},
137+
) = node
138+
{
139+
return Some(item);
140+
}
141+
}
142+
143+
None
144+
}
145+
146+
// return the target features that the called function depends on
147+
fn def_id_target_features(cx: &LateContext<'_>, did: DefId) -> Vec<Symbol> {
148+
let mut added_target_features = Vec::new();
149+
150+
for attr in cx.tcx.get_attrs(did, sym::target_feature) {
151+
let Some(list) = attr.meta_item_list() else {
152+
return vec![];
153+
};
154+
155+
for item in list {
156+
// Only `enable = ...` is accepted in the meta-item list.
157+
if !item.has_name(sym::enable) {
158+
continue;
159+
}
160+
161+
// Must be of the form `enable = "..."` (a string).
162+
let Some(value) = item.value_str() else {
163+
continue;
164+
};
165+
166+
added_target_features.extend(value.as_str().split(',').map(Symbol::intern));
167+
}
168+
}
169+
170+
added_target_features
171+
}
172+
173+
/// Returns the indentation before `span` if there are nothing but `[ \t]`
174+
/// before it on its line.
175+
fn indentation<T: rustc_lint::LintContext>(cx: &T, span: rustc_span::Span) -> Option<String> {
176+
let lo = cx.sess().source_map().lookup_char_pos(span.lo());
177+
lo.file
178+
.get_line(lo.line - 1 /* line numbers in `Loc` are 1-based */)
179+
.and_then(|line| {
180+
if let Some((pos, _)) = line.char_indices().find(|&(_, c)| c != ' ' && c != '\t') {
181+
// We can mix char and byte positions here because we only consider `[ \t]`.
182+
if lo.col == rustc_span::CharPos(pos) {
183+
Some(line[..pos].into())
184+
} else {
185+
None
186+
}
187+
} else {
188+
None
189+
}
190+
})
191+
}

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
6666
crate::borrow_deref_ref::BORROW_DEREF_REF_INFO,
6767
crate::box_default::BOX_DEFAULT_INFO,
6868
crate::byte_char_slices::BYTE_CHAR_SLICES_INFO,
69+
crate::call_missing_target_feature::CALL_MISSING_TARGET_FEATURE_INFO,
6970
crate::cargo::CARGO_COMMON_METADATA_INFO,
7071
crate::cargo::LINT_GROUPS_PRIORITY_INFO,
7172
crate::cargo::MULTIPLE_CRATE_VERSIONS_INFO,

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ mod booleans;
8989
mod borrow_deref_ref;
9090
mod box_default;
9191
mod byte_char_slices;
92+
mod call_missing_target_feature;
9293
mod cargo;
9394
mod casts;
9495
mod cfg_not_test;
@@ -761,6 +762,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
761762
store.register_late_pass(|_| Box::new(floating_point_arithmetic::FloatingPointArithmetic));
762763
store.register_late_pass(|_| Box::new(as_conversions::AsConversions));
763764
store.register_late_pass(|_| Box::new(let_underscore::LetUnderscore));
765+
store.register_late_pass(|_| Box::new(call_missing_target_feature::CallMissingTargetFeature));
764766
store.register_early_pass(|| Box::<single_component_path_imports::SingleComponentPathImports>::default());
765767
store.register_late_pass(move |_| Box::new(excessive_bools::ExcessiveBools::new(conf)));
766768
store.register_early_pass(|| Box::new(option_env_unwrap::OptionEnvUnwrap));
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//@only-target-x86_64
2+
#![allow(clippy::missing_safety_doc)]
3+
4+
#[target_feature(enable = "avx2")]
5+
pub unsafe fn test_f() {
6+
unsafe { f() };
7+
//~^ ERROR: this call requires target features
8+
}
9+
10+
#[target_feature(enable = "avx2,pclmulqdq")]
11+
pub(crate) unsafe fn test_g() {
12+
unsafe { g() };
13+
//~^ ERROR: this call requires target features
14+
}
15+
16+
#[target_feature(enable = "avx2,pclmulqdq")]
17+
unsafe fn test_h() {
18+
unsafe { h() };
19+
//~^ ERROR: this call requires target features
20+
}
21+
22+
#[target_feature(enable = "avx2")]
23+
unsafe fn f() -> u32 {
24+
0
25+
}
26+
27+
#[target_feature(enable = "avx2,pclmulqdq")]
28+
unsafe fn g() -> u32 {
29+
0
30+
}
31+
32+
#[target_feature(enable = "avx2")]
33+
#[target_feature(enable = "pclmulqdq")]
34+
unsafe fn h() -> u32 {
35+
0
36+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//@only-target-x86_64
2+
#![allow(clippy::missing_safety_doc)]
3+
4+
pub fn test_f() {
5+
unsafe { f() };
6+
//~^ ERROR: this call requires target features
7+
}
8+
9+
pub(crate) fn test_g() {
10+
unsafe { g() };
11+
//~^ ERROR: this call requires target features
12+
}
13+
14+
fn test_h() {
15+
unsafe { h() };
16+
//~^ ERROR: this call requires target features
17+
}
18+
19+
#[target_feature(enable = "avx2")]
20+
unsafe fn f() -> u32 {
21+
0
22+
}
23+
24+
#[target_feature(enable = "avx2,pclmulqdq")]
25+
unsafe fn g() -> u32 {
26+
0
27+
}
28+
29+
#[target_feature(enable = "avx2")]
30+
#[target_feature(enable = "pclmulqdq")]
31+
unsafe fn h() -> u32 {
32+
0
33+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
error: this call requires target features that the surrounding function does not enable
2+
--> tests/ui/call_missing_target_feature.rs:5:14
3+
|
4+
LL | unsafe { f() };
5+
| ^^^ this function call requires target features to be enabled
6+
|
7+
= note: `-D clippy::call-missing-target-feature` implied by `-D warnings`
8+
= help: to override `-D warnings` add `#[allow(clippy::call_missing_target_feature)]`
9+
help: add the missing target features to the surrounding function
10+
|
11+
LL + #[target_feature(enable = "avx2")]
12+
LL ~ pub unsafe fn test_f() {
13+
|
14+
15+
error: this call requires target features that the surrounding function does not enable
16+
--> tests/ui/call_missing_target_feature.rs:10:14
17+
|
18+
LL | unsafe { g() };
19+
| ^^^ this function call requires target features to be enabled
20+
|
21+
help: add the missing target features to the surrounding function
22+
|
23+
LL + #[target_feature(enable = "avx2,pclmulqdq")]
24+
LL ~ pub(crate) unsafe fn test_g() {
25+
|
26+
27+
error: this call requires target features that the surrounding function does not enable
28+
--> tests/ui/call_missing_target_feature.rs:15:14
29+
|
30+
LL | unsafe { h() };
31+
| ^^^ this function call requires target features to be enabled
32+
|
33+
help: add the missing target features to the surrounding function
34+
|
35+
LL + #[target_feature(enable = "avx2,pclmulqdq")]
36+
LL ~ unsafe fn test_h() {
37+
|
38+
39+
error: aborting due to 3 previous errors
40+

0 commit comments

Comments
 (0)