use crate::diag::{span_lint_and_help, span_lint_and_sugg};
use crate::utils::*;
use rustc_ast::LitKind;
use rustc_hir::def::{DefKind, Res};
use rustc_hir::def_id::{DefId, LOCAL_CRATE};
use rustc_hir::{BindingMode, Expr, ExprKind, HirId, Node, PatKind, QPath, UnOp};
use rustc_lint::LateContext;
use crate::RAW_SQL_NON_LITERAL;
const SQL_TRAITS: &[&str] = &["SqlClient", "SqlTransaction", "SqlExecutor"];
const SQL_METHODS: &[&str] = &["query", "execute", "query_stream"];
const SQL_CRATE: &str = "id_effect_sql";
pub fn check_raw_sql_non_literal<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
let Some(typeck) = cx.typeck_results else {
return;
};
let (method, sql) = match expr.kind {
ExprKind::MethodCall(_, _, [sql, ..], _) => (typeck.type_dependent_def_id(expr.hir_id), sql),
ExprKind::Call(callee, [_, sql, ..]) => match &callee.kind {
ExprKind::Path(qpath) => (cx.qpath_res(qpath, callee.hir_id).opt_def_id(), sql),
_ => return,
},
_ => return,
};
let Some(method) = method else {
return;
};
if !SQL_METHODS.contains(&cx.tcx.item_name(method).as_str())
|| sql_trait_of(cx, method).is_none()
|| cx.tcx.crate_name(LOCAL_CRATE).as_str() == SQL_CRATE
|| in_sql_trait_impl(cx, expr.hir_id)
|| is_static_sql(cx, sql)
{
return;
}
let msg = "SQL passed to a `SqlClient` / `SqlTransaction` `&str` method is not a literal or a \
`const`";
if let Some(lit) = wrapped_literal(sql)
&& let Some(src) = snippet(cx, lit.span)
&& !sql.span.from_expansion()
{
span_lint_and_sugg(
cx,
RAW_SQL_NON_LITERAL,
sql.span,
msg,
"pass the literal itself",
vec![(sql.span, src)],
);
return;
}
span_lint_and_help(
cx,
RAW_SQL_NON_LITERAL,
sql.span,
msg,
None,
"bind values as parameters (`$1`, `params![…]`) and pass a literal or a `const`; SQL that \
really is built at run time goes through `id_effect_sql::query_raw` / `execute_raw` with a \
`RawSql`, which need the `RawSqlCapability` (ADR 0013 R5)",
);
}
fn sql_trait_of(cx: &LateContext<'_>, method: DefId) -> Option<DefId> {
let tcx = cx.tcx;
let trait_id = tcx.trait_of_assoc(method).or_else(|| {
tcx
.trait_impl_of_assoc(method)
.map(|imp| tcx.impl_trait_id(imp))
})?;
SQL_TRAITS
.contains(&tcx.item_name(trait_id).as_str())
.then_some(trait_id)
}
fn in_sql_trait_impl(cx: &LateContext<'_>, hir_id: HirId) -> bool {
let owner = cx.tcx.hir_get_parent_item(hir_id).to_def_id();
cx.tcx
.trait_impl_of_assoc(owner)
.is_some_and(|imp| SQL_TRAITS.contains(&cx.tcx.item_name(cx.tcx.impl_trait_id(imp)).as_str()))
}
fn peel_refs<'tcx>(mut expr: &'tcx Expr<'tcx>) -> &'tcx Expr<'tcx> {
loop {
expr = match expr.kind {
ExprKind::AddrOf(_, _, inner) | ExprKind::Unary(UnOp::Deref, inner) => inner,
ExprKind::DropTemps(inner) => inner,
_ => return expr,
};
}
}
fn is_static_sql<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> bool {
let expr = peel_refs(expr);
match expr.kind {
ExprKind::Lit(lit) => matches!(lit.node, LitKind::Str(..)),
ExprKind::Path(ref qpath) => match cx.qpath_res(qpath, expr.hir_id) {
Res::Def(DefKind::Const { .. } | DefKind::AssocConst { .. } | DefKind::Static { .. }, _) => {
true
}
Res::Local(id) => {
immutable_local_init(cx, id).is_some_and(|init| is_static_sql(cx, init))
|| is_static_str_param(cx, id)
}
_ => false,
},
ExprKind::MethodCall(seg, recv, [], _) => {
matches!(seg.ident.name.as_str(), "as_str" | "as_ref")
&& expr_ty_is_adt_named(cx, recv, "Sql")
}
_ => false,
}
}
fn immutable_local_init<'tcx>(cx: &LateContext<'tcx>, id: HirId) -> Option<&'tcx Expr<'tcx>> {
let Node::Pat(pat) = cx.tcx.hir_node(id) else {
return None;
};
if !matches!(pat.kind, PatKind::Binding(BindingMode::NONE, _, _, None)) {
return None;
}
match cx.tcx.parent_hir_node(id) {
Node::LetStmt(local) if local.pat.hir_id == id && local.els.is_none() => local.init,
_ => None,
}
}
fn is_static_str_param(cx: &LateContext<'_>, id: HirId) -> bool {
let tcx = cx.tcx;
let owner = tcx.hir_get_parent_item(id);
let (Some(decl), Some(body)) = (
tcx.hir_fn_decl_by_hir_id(tcx.local_def_id_to_hir_id(owner.def_id)),
tcx.hir_maybe_body_owned_by(owner.def_id),
) else {
return false;
};
body.params.iter().zip(decl.inputs).any(|(param, ty)| {
param.pat.hir_id == id
&& matches!(
ty.kind,
rustc_hir::TyKind::Ref(lt, mut_ty)
if lt.is_static() && ty_last_segment_is(mut_ty.ty, "str")
)
})
}
fn wrapped_literal<'tcx>(expr: &'tcx Expr<'tcx>) -> Option<&'tcx Expr<'tcx>> {
let is_str_lit =
|e: &Expr<'_>| matches!(e.kind, ExprKind::Lit(lit) if matches!(lit.node, LitKind::Str(..)));
let inner = match peel_refs(expr).kind {
ExprKind::MethodCall(seg, recv, [], _)
if matches!(seg.ident.name.as_str(), "to_string" | "to_owned") =>
{
recv
}
ExprKind::Call(callee, [arg]) => match &callee.kind {
ExprKind::Path(QPath::TypeRelative(ty, seg))
if seg.ident.name.as_str() == "from" && ty_last_segment_is(ty, "String") =>
{
arg
}
_ => return None,
},
_ => return None,
};
is_str_lit(inner).then_some(inner)
}