|
| 1 | +use clippy_config::Conf; |
| 2 | +use clippy_utils::diagnostics::span_lint_and_sugg; |
| 3 | +use clippy_utils::source::snippet_opt; |
| 4 | +use clippy_utils::{ReturnType, ReturnVisitor, is_entrypoint_fn, visit_returns}; |
| 5 | +use rustc_errors::Applicability; |
| 6 | +use rustc_hir::def_id::LocalDefId; |
| 7 | +use rustc_hir::{BodyId, FnRetTy, FnSig, ImplItem, ImplItemKind, Item, ItemKind, TyKind}; |
| 8 | +use rustc_lint::{LateContext, LateLintPass}; |
| 9 | +use rustc_middle::ty::TypeckResults; |
| 10 | +use rustc_middle::ty::adjustment::{Adjust, Adjustment}; |
| 11 | +use rustc_session::impl_lint_pass; |
| 12 | + |
| 13 | +declare_clippy_lint! { |
| 14 | + /// ### What it does |
| 15 | + /// |
| 16 | + /// Detects functions that do not return, but do not have `!` as their return type. |
| 17 | + /// |
| 18 | + /// ### Why is this bad? |
| 19 | + /// |
| 20 | + /// Returning `!` is a more accurate API for your callers, and allows for optimisations/further linting. |
| 21 | + /// |
| 22 | + /// ### Example |
| 23 | + /// ```no_run |
| 24 | + /// # fn do_thing() {} |
| 25 | + /// fn run() { |
| 26 | + /// loop { |
| 27 | + /// do_thing(); |
| 28 | + /// } |
| 29 | + /// } |
| 30 | + /// ``` |
| 31 | + /// Use instead: |
| 32 | + /// ```no_run |
| 33 | + /// # fn do_thing() {} |
| 34 | + /// fn run() -> ! { |
| 35 | + /// loop { |
| 36 | + /// do_thing(); |
| 37 | + /// } |
| 38 | + /// } |
| 39 | + /// ``` |
| 40 | + #[clippy::version = "1.83.0"] |
| 41 | + pub NEVER_RETURNS, |
| 42 | + pedantic, |
| 43 | + "functions that never return, but are typed to" |
| 44 | +} |
| 45 | + |
| 46 | +#[derive(Clone, Copy)] |
| 47 | +pub(crate) struct NeverReturns { |
| 48 | + avoid_breaking_exported_api: bool, |
| 49 | +} |
| 50 | + |
| 51 | +impl_lint_pass!(NeverReturns => [NEVER_RETURNS]); |
| 52 | + |
| 53 | +impl NeverReturns { |
| 54 | + pub fn new(conf: &Conf) -> Self { |
| 55 | + Self { |
| 56 | + avoid_breaking_exported_api: conf.avoid_breaking_exported_api, |
| 57 | + } |
| 58 | + } |
| 59 | + |
| 60 | + fn check_item_fn(self, cx: &LateContext<'_>, sig: FnSig<'_>, def_id: LocalDefId, body_id: BodyId) { |
| 61 | + let returns_unit = if let FnRetTy::Return(ret_ty) = sig.decl.output { |
| 62 | + if let TyKind::Never = ret_ty.kind { |
| 63 | + return; |
| 64 | + } |
| 65 | + |
| 66 | + matches!(ret_ty.kind, TyKind::Tup([])) |
| 67 | + } else { |
| 68 | + true |
| 69 | + }; |
| 70 | + |
| 71 | + if self.avoid_breaking_exported_api && cx.effective_visibilities.is_exported(def_id) { |
| 72 | + return; |
| 73 | + } |
| 74 | + |
| 75 | + // We shouldn't try to change the signature of a lang item! |
| 76 | + if cx.tcx.lang_items().from_def_id(def_id.to_def_id()).is_some() { |
| 77 | + return; |
| 78 | + } |
| 79 | + |
| 80 | + let body = cx.tcx.hir().body(body_id); |
| 81 | + let typeck_results = cx.tcx.typeck_body(body_id); |
| 82 | + let mut visitor = NeverReturnVisitor { |
| 83 | + typeck_results, |
| 84 | + returns_unit, |
| 85 | + found_implicit_return: false, |
| 86 | + }; |
| 87 | + |
| 88 | + if visit_returns(&mut visitor, body.value).is_continue() && visitor.found_implicit_return { |
| 89 | + let mut applicability = Applicability::MachineApplicable; |
| 90 | + let (lint_span, mut snippet, sugg) = match sig.decl.output { |
| 91 | + FnRetTy::DefaultReturn(span) => (span, String::new(), " -> !"), |
| 92 | + FnRetTy::Return(ret_ty) => { |
| 93 | + let snippet = if let Some(snippet) = snippet_opt(cx, ret_ty.span) { |
| 94 | + format!(" a `{snippet}`") |
| 95 | + } else { |
| 96 | + applicability = Applicability::HasPlaceholders; |
| 97 | + String::new() |
| 98 | + }; |
| 99 | + |
| 100 | + (ret_ty.span, snippet, "!") |
| 101 | + }, |
| 102 | + }; |
| 103 | + |
| 104 | + snippet.insert_str(0, "function never returns, but is typed to return"); |
| 105 | + span_lint_and_sugg( |
| 106 | + cx, |
| 107 | + NEVER_RETURNS, |
| 108 | + lint_span, |
| 109 | + snippet, |
| 110 | + "replace with", |
| 111 | + sugg.into(), |
| 112 | + applicability, |
| 113 | + ); |
| 114 | + } |
| 115 | + } |
| 116 | +} |
| 117 | + |
| 118 | +impl LateLintPass<'_> for NeverReturns { |
| 119 | + fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) { |
| 120 | + if let ItemKind::Fn(sig, _, body_id) = item.kind { |
| 121 | + let local_def_id = item.owner_id.def_id; |
| 122 | + if is_entrypoint_fn(cx, local_def_id.to_def_id()) { |
| 123 | + return; |
| 124 | + } |
| 125 | + |
| 126 | + self.check_item_fn(cx, sig, local_def_id, body_id); |
| 127 | + } |
| 128 | + } |
| 129 | + |
| 130 | + fn check_impl_item(&mut self, cx: &LateContext<'_>, item: &ImplItem<'_>) { |
| 131 | + if let ImplItemKind::Fn(sig, body_id) = item.kind { |
| 132 | + let local_def_id = item.owner_id.def_id; |
| 133 | + self.check_item_fn(cx, sig, local_def_id, body_id); |
| 134 | + } |
| 135 | + } |
| 136 | +} |
| 137 | + |
| 138 | +struct NeverReturnVisitor<'tcx> { |
| 139 | + typeck_results: &'tcx TypeckResults<'tcx>, |
| 140 | + found_implicit_return: bool, |
| 141 | + returns_unit: bool, |
| 142 | +} |
| 143 | + |
| 144 | +impl ReturnVisitor for &mut NeverReturnVisitor<'_> { |
| 145 | + type Result = std::ops::ControlFlow<()>; |
| 146 | + |
| 147 | + fn visit_return(&mut self, kind: ReturnType<'_>) -> Self::Result { |
| 148 | + let expression = match kind { |
| 149 | + ReturnType::Explicit(expr) => expr, |
| 150 | + ReturnType::UnitReturnExplicit(_) => { |
| 151 | + return Self::Result::Break(()); |
| 152 | + }, |
| 153 | + ReturnType::Implicit(expr) | ReturnType::MissingElseImplicit(expr) => { |
| 154 | + self.found_implicit_return = true; |
| 155 | + expr |
| 156 | + }, |
| 157 | + ReturnType::DivergingImplicit(_) => { |
| 158 | + // If this function returns unit, a diverging implicit may just |
| 159 | + // be an implicit unit return, in which case we should not lint. |
| 160 | + return if self.returns_unit { |
| 161 | + Self::Result::Break(()) |
| 162 | + } else { |
| 163 | + Self::Result::Continue(()) |
| 164 | + }; |
| 165 | + }, |
| 166 | + }; |
| 167 | + |
| 168 | + if expression.span.from_expansion() { |
| 169 | + return Self::Result::Break(()); |
| 170 | + } |
| 171 | + |
| 172 | + let adjustments = self.typeck_results.expr_adjustments(expression); |
| 173 | + if adjustments.iter().any(is_never_to_any) { |
| 174 | + Self::Result::Continue(()) |
| 175 | + } else { |
| 176 | + Self::Result::Break(()) |
| 177 | + } |
| 178 | + } |
| 179 | +} |
| 180 | + |
| 181 | +fn is_never_to_any(adjustment: &Adjustment<'_>) -> bool { |
| 182 | + matches!(adjustment.kind, Adjust::NeverToAny) |
| 183 | +} |
0 commit comments