Skip to content

Commit 95c1f0b

Browse files
committed
Lint unnecessary safety comments on items
1 parent a599527 commit 95c1f0b

File tree

5 files changed

+189
-63
lines changed

5 files changed

+189
-63
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4449,6 +4449,7 @@ Released 2018-09-13
44494449
[`unnecessary_mut_passed`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_mut_passed
44504450
[`unnecessary_operation`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_operation
44514451
[`unnecessary_owned_empty_strings`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_owned_empty_strings
4452+
[`unnecessary_safety_comment`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_safety_comment
44524453
[`unnecessary_safety_doc`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_safety_doc
44534454
[`unnecessary_self_imports`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_self_imports
44544455
[`unnecessary_sort_by`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_sort_by

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
582582
crate::types::TYPE_COMPLEXITY_INFO,
583583
crate::types::VEC_BOX_INFO,
584584
crate::undocumented_unsafe_blocks::UNDOCUMENTED_UNSAFE_BLOCKS_INFO,
585+
crate::undocumented_unsafe_blocks::UNNECESSARY_SAFETY_COMMENT_INFO,
585586
crate::unicode::INVISIBLE_CHARACTERS_INFO,
586587
crate::unicode::NON_ASCII_LITERAL_INFO,
587588
crate::unicode::UNICODE_NOT_NFC_INFO,

clippy_lints/src/undocumented_unsafe_blocks.rs

Lines changed: 172 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,36 @@ declare_clippy_lint! {
5959
restriction,
6060
"creating an unsafe block without explaining why it is safe"
6161
}
62+
declare_clippy_lint! {
63+
/// ### What it does
64+
/// Checks for `// SAFETY: ` comments on safe code.
65+
///
66+
/// ### Why is this bad?
67+
/// Safe code has no safety requirements, so there is no need to
68+
/// describe safety invariants.
69+
///
70+
/// ### Example
71+
/// ```rust
72+
/// use std::ptr::NonNull;
73+
/// let a = &mut 42;
74+
///
75+
/// // SAFETY: references are guaranteed to be non-null.
76+
/// let ptr = NonNull::new(a).unwrap();
77+
/// ```
78+
/// Use instead:
79+
/// ```rust
80+
/// use std::ptr::NonNull;
81+
/// let a = &mut 42;
82+
///
83+
/// let ptr = NonNull::new(a).unwrap();
84+
/// ```
85+
#[clippy::version = "1.66.0"]
86+
pub UNNECESSARY_SAFETY_COMMENT,
87+
restriction,
88+
"creating an unsafe block without explaining why it is safe"
89+
}
6290

63-
declare_lint_pass!(UndocumentedUnsafeBlocks => [UNDOCUMENTED_UNSAFE_BLOCKS]);
91+
declare_lint_pass!(UndocumentedUnsafeBlocks => [UNDOCUMENTED_UNSAFE_BLOCKS, UNNECESSARY_SAFETY_COMMENT]);
6492

6593
impl LateLintPass<'_> for UndocumentedUnsafeBlocks {
6694
fn check_block(&mut self, cx: &LateContext<'_>, block: &'_ Block<'_>) {
@@ -90,28 +118,95 @@ impl LateLintPass<'_> for UndocumentedUnsafeBlocks {
90118
}
91119

92120
fn check_item(&mut self, cx: &LateContext<'_>, item: &hir::Item<'_>) {
93-
if let hir::ItemKind::Impl(imple) = item.kind
94-
&& imple.unsafety == hir::Unsafety::Unsafe
95-
&& !in_external_macro(cx.tcx.sess, item.span)
96-
&& !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCKS, item.hir_id())
97-
&& !is_unsafe_from_proc_macro(cx, item.span)
98-
&& !item_has_safety_comment(cx, item)
99-
{
121+
if in_external_macro(cx.tcx.sess, item.span) {
122+
return;
123+
}
124+
125+
let mk_spans = |pos: BytePos| {
100126
let source_map = cx.tcx.sess.source_map();
127+
let span = Span::new(pos, pos, SyntaxContext::root(), None);
128+
let help_span = source_map.span_extend_to_next_char(span, '\n', true);
101129
let span = if source_map.is_multiline(item.span) {
102130
source_map.span_until_char(item.span, '\n')
103131
} else {
104132
item.span
105133
};
134+
(span, help_span)
135+
};
106136

107-
span_lint_and_help(
108-
cx,
109-
UNDOCUMENTED_UNSAFE_BLOCKS,
110-
span,
111-
"unsafe impl missing a safety comment",
112-
None,
113-
"consider adding a safety comment on the preceding line",
114-
);
137+
let item_has_safety_comment = item_has_safety_comment(cx, item);
138+
match (&item.kind, item_has_safety_comment) {
139+
(hir::ItemKind::Impl(impl_), Some(None)) if impl_.unsafety == hir::Unsafety::Unsafe => {
140+
if !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCKS, item.hir_id())
141+
&& !is_unsafe_from_proc_macro(cx, item.span)
142+
{
143+
let source_map = cx.tcx.sess.source_map();
144+
let span = if source_map.is_multiline(item.span) {
145+
source_map.span_until_char(item.span, '\n')
146+
} else {
147+
item.span
148+
};
149+
150+
span_lint_and_help(
151+
cx,
152+
UNDOCUMENTED_UNSAFE_BLOCKS,
153+
span,
154+
"unsafe impl missing a safety comment",
155+
None,
156+
"consider adding a safety comment on the preceding line",
157+
);
158+
}
159+
},
160+
(hir::ItemKind::Impl(impl_), Some(Some(pos))) if impl_.unsafety == hir::Unsafety::Normal => {
161+
if !is_lint_allowed(cx, UNNECESSARY_SAFETY_COMMENT, item.hir_id()) {
162+
let (span, help_span) = mk_spans(pos);
163+
164+
span_lint_and_help(
165+
cx,
166+
UNNECESSARY_SAFETY_COMMENT,
167+
span,
168+
"impl has unnecessary safety comment",
169+
Some(help_span),
170+
"consider removing the safety comment",
171+
);
172+
}
173+
},
174+
(hir::ItemKind::Impl(_), _) => {},
175+
(&hir::ItemKind::Const(.., body) | &hir::ItemKind::Static(.., body), Some(Some(pos))) => {
176+
if !is_lint_allowed(cx, UNNECESSARY_SAFETY_COMMENT, body.hir_id) {
177+
let body = cx.tcx.hir().body(body);
178+
if !matches!(
179+
body.value.kind, hir::ExprKind::Block(block, _)
180+
if block.rules == BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided)
181+
) {
182+
let (span, help_span) = mk_spans(pos);
183+
184+
span_lint_and_help(
185+
cx,
186+
UNNECESSARY_SAFETY_COMMENT,
187+
span,
188+
&format!("{} has unnecessary safety comment", item.kind.descr()),
189+
Some(help_span),
190+
"consider removing the safety comment",
191+
);
192+
}
193+
}
194+
},
195+
(_, Some(Some(pos))) => {
196+
if !is_lint_allowed(cx, UNNECESSARY_SAFETY_COMMENT, item.hir_id()) {
197+
let (span, help_span) = mk_spans(pos);
198+
199+
span_lint_and_help(
200+
cx,
201+
UNNECESSARY_SAFETY_COMMENT,
202+
span,
203+
&format!("{} has unnecessary safety comment", item.kind.descr()),
204+
Some(help_span),
205+
"consider removing the safety comment",
206+
);
207+
}
208+
},
209+
_ => (),
115210
}
116211
}
117212
}
@@ -170,28 +265,29 @@ fn block_has_safety_comment(cx: &LateContext<'_>, span: Span) -> bool {
170265
// won't work. This is to avoid dealing with where such a comment should be place relative to
171266
// attributes and doc comments.
172267

173-
span_from_macro_expansion_has_safety_comment(cx, span) || span_in_body_has_safety_comment(cx, span)
268+
matches!(span_from_macro_expansion_has_safety_comment(cx, span), Some(Some(_)))
269+
|| span_in_body_has_safety_comment(cx, span)
174270
}
175271

176272
/// Checks if the lines immediately preceding the item contain a safety comment.
177273
#[allow(clippy::collapsible_match)]
178-
fn item_has_safety_comment(cx: &LateContext<'_>, item: &hir::Item<'_>) -> bool {
179-
if span_from_macro_expansion_has_safety_comment(cx, item.span) {
180-
return true;
274+
fn item_has_safety_comment(cx: &LateContext<'_>, item: &hir::Item<'_>) -> Option<Option<BytePos>> {
275+
if let res @ Some(_) = span_from_macro_expansion_has_safety_comment(cx, item.span) {
276+
return res;
181277
}
182278

183279
if item.span.ctxt() == SyntaxContext::root() {
184280
if let Some(parent_node) = get_parent_node(cx.tcx, item.hir_id()) {
185281
let comment_start = match parent_node {
186282
Node::Crate(parent_mod) => {
187-
comment_start_before_impl_in_mod(cx, parent_mod, parent_mod.spans.inner_span, item)
283+
comment_start_before_item_in_mod(cx, parent_mod, parent_mod.spans.inner_span, item)
188284
},
189285
Node::Item(parent_item) => {
190286
if let ItemKind::Mod(parent_mod) = &parent_item.kind {
191-
comment_start_before_impl_in_mod(cx, parent_mod, parent_item.span, item)
287+
comment_start_before_item_in_mod(cx, parent_mod, parent_item.span, item)
192288
} else {
193289
// Doesn't support impls in this position. Pretend a comment was found.
194-
return true;
290+
return None;
195291
}
196292
},
197293
Node::Stmt(stmt) => {
@@ -200,17 +296,17 @@ fn item_has_safety_comment(cx: &LateContext<'_>, item: &hir::Item<'_>) -> bool {
200296
Node::Block(block) => walk_span_to_context(block.span, SyntaxContext::root()).map(Span::lo),
201297
_ => {
202298
// Doesn't support impls in this position. Pretend a comment was found.
203-
return true;
299+
return None;
204300
},
205301
}
206302
} else {
207303
// Problem getting the parent node. Pretend a comment was found.
208-
return true;
304+
return None;
209305
}
210306
},
211307
_ => {
212308
// Doesn't support impls in this position. Pretend a comment was found.
213-
return true;
309+
return None;
214310
},
215311
};
216312

@@ -221,34 +317,38 @@ fn item_has_safety_comment(cx: &LateContext<'_>, item: &hir::Item<'_>) -> bool {
221317
&& Lrc::ptr_eq(&unsafe_line.sf, &comment_start_line.sf)
222318
&& let Some(src) = unsafe_line.sf.src.as_deref()
223319
{
224-
unsafe_line.sf.lines(|lines| {
225-
comment_start_line.line < unsafe_line.line && text_has_safety_comment(
226-
src,
227-
&lines[comment_start_line.line + 1..=unsafe_line.line],
228-
unsafe_line.sf.start_pos.to_usize(),
229-
)
230-
})
320+
Some(unsafe_line.sf.lines(|lines| {
321+
if comment_start_line.line >= unsafe_line.line {
322+
None
323+
} else {
324+
text_has_safety_comment(
325+
src,
326+
&lines[comment_start_line.line + 1..=unsafe_line.line],
327+
unsafe_line.sf.start_pos.to_usize(),
328+
)
329+
}
330+
}))
231331
} else {
232332
// Problem getting source text. Pretend a comment was found.
233-
true
333+
None
234334
}
235335
} else {
236336
// No parent node. Pretend a comment was found.
237-
true
337+
None
238338
}
239339
} else {
240-
false
340+
Some(None)
241341
}
242342
}
243343

244-
fn comment_start_before_impl_in_mod(
344+
fn comment_start_before_item_in_mod(
245345
cx: &LateContext<'_>,
246346
parent_mod: &hir::Mod<'_>,
247347
parent_mod_span: Span,
248-
imple: &hir::Item<'_>,
348+
item: &hir::Item<'_>,
249349
) -> Option<BytePos> {
250350
parent_mod.item_ids.iter().enumerate().find_map(|(idx, item_id)| {
251-
if *item_id == imple.item_id() {
351+
if *item_id == item.item_id() {
252352
if idx == 0 {
253353
// mod A { /* comment */ unsafe impl T {} ... }
254354
// ^------------------------------------------^ returns the start of this span
@@ -270,11 +370,11 @@ fn comment_start_before_impl_in_mod(
270370
})
271371
}
272372

273-
fn span_from_macro_expansion_has_safety_comment(cx: &LateContext<'_>, span: Span) -> bool {
373+
fn span_from_macro_expansion_has_safety_comment(cx: &LateContext<'_>, span: Span) -> Option<Option<BytePos>> {
274374
let source_map = cx.sess().source_map();
275375
let ctxt = span.ctxt();
276376
if ctxt == SyntaxContext::root() {
277-
false
377+
None
278378
} else {
279379
// From a macro expansion. Get the text from the start of the macro declaration to start of the
280380
// unsafe block.
@@ -285,16 +385,20 @@ fn span_from_macro_expansion_has_safety_comment(cx: &LateContext<'_>, span: Span
285385
&& Lrc::ptr_eq(&unsafe_line.sf, &macro_line.sf)
286386
&& let Some(src) = unsafe_line.sf.src.as_deref()
287387
{
288-
unsafe_line.sf.lines(|lines| {
289-
macro_line.line < unsafe_line.line && text_has_safety_comment(
290-
src,
291-
&lines[macro_line.line + 1..=unsafe_line.line],
292-
unsafe_line.sf.start_pos.to_usize(),
293-
)
294-
})
388+
Some(unsafe_line.sf.lines(|lines| {
389+
if macro_line.line < unsafe_line.line {
390+
text_has_safety_comment(
391+
src,
392+
&lines[macro_line.line + 1..=unsafe_line.line],
393+
unsafe_line.sf.start_pos.to_usize(),
394+
)
395+
} else {
396+
None
397+
}
398+
}))
295399
} else {
296400
// Problem getting source text. Pretend a comment was found.
297-
true
401+
None
298402
}
299403
}
300404
}
@@ -333,7 +437,7 @@ fn span_in_body_has_safety_comment(cx: &LateContext<'_>, span: Span) -> bool {
333437
src,
334438
&lines[body_line.line + 1..=unsafe_line.line],
335439
unsafe_line.sf.start_pos.to_usize(),
336-
)
440+
).is_some()
337441
})
338442
} else {
339443
// Problem getting source text. Pretend a comment was found.
@@ -345,30 +449,34 @@ fn span_in_body_has_safety_comment(cx: &LateContext<'_>, span: Span) -> bool {
345449
}
346450

347451
/// Checks if the given text has a safety comment for the immediately proceeding line.
348-
fn text_has_safety_comment(src: &str, line_starts: &[BytePos], offset: usize) -> bool {
452+
fn text_has_safety_comment(src: &str, line_starts: &[BytePos], offset: usize) -> Option<BytePos> {
349453
let mut lines = line_starts
350454
.array_windows::<2>()
351455
.rev()
352456
.map_while(|[start, end]| {
353457
let start = start.to_usize() - offset;
354458
let end = end.to_usize() - offset;
355-
src.get(start..end).map(|text| (start, text.trim_start()))
459+
let text = src.get(start..end)?;
460+
let trimmed = text.trim_start();
461+
Some((start + (text.len() - trimmed.len()), trimmed))
356462
})
357463
.filter(|(_, text)| !text.is_empty());
358464

359465
let Some((line_start, line)) = lines.next() else {
360-
return false;
466+
return None;
361467
};
362468
// Check for a sequence of line comments.
363469
if line.starts_with("//") {
364-
let mut line = line;
470+
let (mut line, mut line_start) = (line, line_start);
365471
loop {
366472
if line.to_ascii_uppercase().contains("SAFETY:") {
367-
return true;
473+
return Some(BytePos(
474+
u32::try_from(line_start).unwrap() + u32::try_from(offset).unwrap(),
475+
));
368476
}
369477
match lines.next() {
370-
Some((_, x)) if x.starts_with("//") => line = x,
371-
_ => return false,
478+
Some((s, x)) if x.starts_with("//") => (line, line_start) = (x, s),
479+
_ => return None,
372480
}
373481
}
374482
}
@@ -377,16 +485,19 @@ fn text_has_safety_comment(src: &str, line_starts: &[BytePos], offset: usize) ->
377485
let (mut line_start, mut line) = (line_start, line);
378486
loop {
379487
if line.starts_with("/*") {
380-
let src = src[line_start..line_starts.last().unwrap().to_usize() - offset].trim_start();
488+
let src = &src[line_start..line_starts.last().unwrap().to_usize() - offset];
381489
let mut tokens = tokenize(src);
382-
return src[..tokens.next().unwrap().len as usize]
490+
return (src[..tokens.next().unwrap().len as usize]
383491
.to_ascii_uppercase()
384492
.contains("SAFETY:")
385-
&& tokens.all(|t| t.kind == TokenKind::Whitespace);
493+
&& tokens.all(|t| t.kind == TokenKind::Whitespace))
494+
.then_some(BytePos(
495+
u32::try_from(line_start).unwrap() + u32::try_from(offset).unwrap(),
496+
));
386497
}
387498
match lines.next() {
388499
Some(x) => (line_start, line) = x,
389-
None => return false,
500+
None => return None,
390501
}
391502
}
392503
}

0 commit comments

Comments
 (0)