File size: 9,303 Bytes
17f328f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 | #![feature(rustc_private)]
mod comment_parser;
extern crate rustc_ast;
extern crate rustc_errors;
extern crate rustc_hir;
extern crate rustc_lint;
extern crate rustc_middle;
extern crate rustc_session;
extern crate rustc_span;
use clippy_utils::diagnostics::span_lint_and_help;
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::fn_def_id;
use clippy_utils::is_res_lang_ctor;
use clippy_utils::peel_blocks;
use clippy_utils::source::snippet;
use rustc_ast::LitKind;
use rustc_errors::Applicability;
use rustc_hir::Expr;
use rustc_hir::ExprKind;
use rustc_hir::LangItem;
use rustc_hir::UnOp;
use rustc_hir::def::DefKind;
use rustc_lint::LateContext;
use rustc_lint::LateLintPass;
use rustc_span::BytePos;
use rustc_span::Span;
use crate::comment_parser::parse_argument_comment;
use crate::comment_parser::parse_argument_comment_prefix;
#[cfg(not(feature = "bazel_native"))]
dylint_linting::dylint_library!();
#[unsafe(no_mangle)]
pub fn register_lints(_sess: &rustc_session::Session, lint_store: &mut rustc_lint::LintStore) {
lint_store.register_lints(&[
ARGUMENT_COMMENT_MISMATCH,
UNCOMMENTED_ANONYMOUS_LITERAL_ARGUMENT,
]);
lint_store.register_late_pass(|_| Box::new(ArgumentCommentLint));
}
rustc_session::declare_lint! {
/// ### What it does
///
/// Checks `/*param*/` argument comments and verifies that the comment
/// matches the resolved callee parameter name.
///
/// ### Why is this bad?
///
/// A mismatched comment is worse than no comment because it actively
/// misleads the reader.
///
/// ### Known problems
///
/// This lint only runs when the callee resolves to a concrete function or
/// method with available parameter names.
///
/// ### Example
///
/// ```rust
/// fn create_openai_url(base_url: Option<String>) -> String {
/// String::new()
/// }
///
/// create_openai_url(/*api_base*/ None);
/// ```
///
/// Use instead:
///
/// ```rust
/// fn create_openai_url(base_url: Option<String>) -> String {
/// String::new()
/// }
///
/// create_openai_url(/*base_url*/ None);
/// ```
pub ARGUMENT_COMMENT_MISMATCH,
Warn,
"argument comment does not match the resolved parameter name"
}
rustc_session::declare_lint! {
/// ### What it does
///
/// Requires a `/*param*/` comment before anonymous literal-like
/// arguments such as `None`, booleans, and numeric literals.
/// A method's sole non-self argument is exempt when its name matches the
/// method name.
///
/// ### Why is this bad?
///
/// Bare literal-like arguments make call sites harder to read because the
/// meaning of the value is hidden in the callee signature.
///
/// ### Known problems
///
/// This lint is opinionated, so it is `allow` by default.
///
/// ### Example
///
/// ```rust
/// fn create_openai_url(base_url: Option<String>) -> String {
/// String::new()
/// }
///
/// create_openai_url(None);
/// ```
///
/// Use instead:
///
/// ```rust
/// fn create_openai_url(base_url: Option<String>) -> String {
/// String::new()
/// }
///
/// create_openai_url(/*base_url*/ None);
/// ```
pub UNCOMMENTED_ANONYMOUS_LITERAL_ARGUMENT,
Allow,
"anonymous literal-like argument is missing a `/*param*/` comment"
}
#[derive(Default)]
pub struct ArgumentCommentLint;
enum CallKind {
Function,
Method { name: String },
}
rustc_session::impl_lint_pass!(
ArgumentCommentLint => [ARGUMENT_COMMENT_MISMATCH, UNCOMMENTED_ANONYMOUS_LITERAL_ARGUMENT]
);
impl<'tcx> LateLintPass<'tcx> for ArgumentCommentLint {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
if expr.span.from_expansion() {
return;
}
match expr.kind {
ExprKind::Call(callee, args) => {
self.check_call(cx, expr, callee.span, args, CallKind::Function);
}
ExprKind::MethodCall(method, receiver, args, _) => {
self.check_call(
cx,
expr,
receiver.span,
args,
CallKind::Method {
name: method.ident.name.to_string(),
},
);
}
_ => {}
}
}
}
impl ArgumentCommentLint {
fn check_call<'tcx>(
&self,
cx: &LateContext<'tcx>,
call: &'tcx Expr<'tcx>,
first_gap_anchor: Span,
args: &'tcx [Expr<'tcx>],
call_kind: CallKind,
) {
let Some(def_id) = fn_def_id(cx, call) else {
return;
};
if !def_id.is_local() && !is_workspace_crate_name(cx.tcx.crate_name(def_id.krate).as_str())
{
return;
}
if !matches!(cx.tcx.def_kind(def_id), DefKind::Fn | DefKind::AssocFn) {
return;
}
// Method parameter lists include `self`, which is not present in `args`.
let (parameter_offset, method_name) = match &call_kind {
CallKind::Function => (0, None),
CallKind::Method { name } => (1, Some(name.as_str())),
};
let parameter_names: Vec<_> = cx.tcx.fn_arg_idents(def_id).iter().copied().collect();
for (index, arg) in args.iter().enumerate() {
if arg.span.from_expansion() {
continue;
}
let Some(expected_name) = parameter_names.get(index + parameter_offset) else {
continue;
};
let Some(expected_name) = expected_name else {
continue;
};
let expected_name = expected_name.name.to_string();
if !is_meaningful_parameter_name(&expected_name) {
continue;
}
let boundary_span = if index == 0 {
first_gap_anchor
} else {
args[index - 1].span
};
let gap_span = boundary_span.between(arg.span);
let gap_text = snippet(cx, gap_span, "");
let arg_text = snippet(cx, arg.span, "..");
let lookbehind_start = BytePos(arg.span.lo().0.saturating_sub(64));
let lookbehind_text =
snippet(cx, arg.span.shrink_to_lo().with_lo(lookbehind_start), "");
let argument_comment = parse_argument_comment(gap_text.as_ref())
.or_else(|| parse_argument_comment(lookbehind_text.as_ref()))
.or_else(|| parse_argument_comment_prefix(arg_text.as_ref()));
if let Some(actual_name) = argument_comment {
if actual_name != expected_name {
span_lint_and_help(
cx,
ARGUMENT_COMMENT_MISMATCH,
arg.span,
format!(
"argument comment `/*{actual_name}*/` does not match parameter `{expected_name}`"
),
None,
format!("use `/*{expected_name}*/`"),
);
}
continue;
}
// Don't require a clarifying comment for self-documenting arguments whose names
// match the method.
if args.len() == 1 && method_name == Some(expected_name.as_str()) {
continue;
}
if !is_anonymous_literal_like(cx, arg) {
continue;
}
span_lint_and_sugg(
cx,
UNCOMMENTED_ANONYMOUS_LITERAL_ARGUMENT,
arg.span,
format!("anonymous literal-like argument for parameter `{expected_name}`"),
"prepend the parameter name comment",
format!("/*{expected_name}*/ {arg_text}"),
Applicability::MachineApplicable,
);
}
}
}
fn is_anonymous_literal_like(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
let expr = peel_blocks(expr);
match expr.kind {
ExprKind::Lit(lit) => !matches!(
lit.node,
LitKind::Str(..) | LitKind::ByteStr(..) | LitKind::CStr(..) | LitKind::Char(..)
),
ExprKind::Unary(UnOp::Neg, inner) => matches!(peel_blocks(inner).kind, ExprKind::Lit(_)),
ExprKind::Path(qpath) => {
is_res_lang_ctor(cx, cx.qpath_res(&qpath, expr.hir_id), LangItem::OptionNone)
}
_ => false,
}
}
fn is_meaningful_parameter_name(name: &str) -> bool {
!name.is_empty() && !name.starts_with('_')
}
fn is_workspace_crate_name(name: &str) -> bool {
name.starts_with("codex_") || matches!(name, "app_test_support" | "core_test_support")
}
#[test]
fn ui() {
dylint_testing::ui_test(env!("CARGO_PKG_NAME"), "ui");
}
#[test]
fn workspace_crate_filter_accepts_first_party_names_only() {
assert!(is_workspace_crate_name("codex_core"));
assert!(is_workspace_crate_name("codex_tui"));
assert!(is_workspace_crate_name("core_test_support"));
assert!(!is_workspace_crate_name("std"));
assert!(!is_workspace_crate_name("tokio"));
}
|