extern crate rustc_ast;
extern crate rustc_span;
use rustc_ast::{
AttrKind, Attribute, ExprKind, Item, ItemKind, MacCall, VisibilityKind, visit, visit::Visitor,
};
use rustc_lint::{EarlyContext, EarlyLintPass, LintContext};
use rustc_session::config::CrateType;
use rustc_span::{FileName, sym};
const FORBIDDEN_MACROS: &[&str] = &["println", "eprintln", "print", "eprint"];
dylint_linting::declare_pre_expansion_lint! {
#[doc = include_str!("../../docs/de13_common_patterns/de1301_no_print_macros/README.md")]
pub DE1301_NO_PRINT_MACROS,
Deny,
"print/debug macros are forbidden in production code (DE1301)"
}
impl EarlyLintPass for De1301NoPrintMacros {
fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) {
let mut v = ForbiddenMacroVisitor {
cx,
in_proc_macro_crate: is_proc_macro_crate(cx),
is_bin_crate: is_bin_crate(cx),
allow_stack: Vec::new(),
};
v.visit_item(item);
}
}
fn is_allowed_location(cx: &EarlyContext<'_>, span: rustc_span::Span) -> bool {
let source_map = cx.sess().source_map();
let file_name = source_map.span_to_filename(span);
let Some(path_str) = (match file_name {
FileName::Real(real_name) => real_name
.local_path()
.map(|p| p.to_string_lossy().to_string()),
_ => None,
}) else {
return false;
};
let effective_path = extract_simulated_path(&path_str).unwrap_or(path_str);
let effective_path = effective_path.replace('\\', "/");
if effective_path.ends_with("/build.rs") {
return true;
}
if effective_path.starts_with("apps/") || effective_path.contains("/apps/") {
return true;
}
false
}
fn is_proc_macro_crate(cx: &EarlyContext<'_>) -> bool {
cx.sess().opts.crate_types.contains(&CrateType::ProcMacro)
}
fn is_bin_crate(cx: &EarlyContext<'_>) -> bool {
cx.sess().opts.crate_types.contains(&CrateType::Executable)
}
fn extract_simulated_path(path_str: &str) -> Option<String> {
let is_temp = path_str.contains("/tmp/")
|| path_str.contains("/var/folders/")
|| path_str.contains("\\Temp\\")
|| path_str.contains(".tmp");
if !is_temp {
return None;
}
let contents = std::fs::read_to_string(std::path::PathBuf::from(path_str)).ok()?;
for line in contents.lines().take(1) {
let trimmed = line.trim();
if trimmed.starts_with("// simulated_dir=") {
return Some(trimmed.trim_start_matches("// simulated_dir=").to_string());
}
if !trimmed.is_empty() && !trimmed.starts_with("//") && !trimmed.starts_with("#!") {
break;
}
}
None
}
struct ForbiddenMacroVisitor<'a, 'cx> {
cx: &'a EarlyContext<'cx>,
in_proc_macro_crate: bool,
is_bin_crate: bool,
allow_stack: Vec<bool>,
}
impl<'a, 'cx> ForbiddenMacroVisitor<'a, 'cx> {
fn lint_mac_call(&self, mac_call: &MacCall) {
let allowed_here = self.allow_stack.last().copied().unwrap_or(false);
if allowed_here {
return;
}
if is_allowed_location(self.cx, mac_call.span()) {
return;
}
let Some(last) = mac_call.path.segments.last() else {
return;
};
let name = last.ident.name.as_str();
if !FORBIDDEN_MACROS.contains(&name) {
return;
}
self.cx
.span_lint(DE1301_NO_PRINT_MACROS, mac_call.span(), |diag| {
diag.primary_message(format!(
"macro `{name}!` is forbidden in production code (DE1301)"
));
diag.help(
"use `tracing`/`log` for observability, or return the value and handle it at the boundary",
);
});
}
}
impl<'ast, 'a, 'cx> visit::Visitor<'ast> for ForbiddenMacroVisitor<'a, 'cx> {
fn visit_item(&mut self, item: &'ast Item) {
let parent_allow = self.allow_stack.last().copied().unwrap_or(false);
match &item.kind {
ItemKind::Fn(_fn_item) => {
let is_binary_entry = self.allow_stack.is_empty() && self.is_bin_crate;
let is_private = matches!(item.vis.kind, VisibilityKind::Inherited);
let allow_here = parent_allow
|| is_binary_entry
|| is_test_item(&item.attrs)
|| (self.in_proc_macro_crate
&& (is_private || has_proc_macro_attr(&item.attrs)));
self.allow_stack.push(allow_here);
visit::walk_item(self, item);
self.allow_stack.pop();
}
ItemKind::Mod(..) => {
let allow_here = parent_allow || is_test_item(&item.attrs);
self.allow_stack.push(allow_here);
visit::walk_item(self, item);
self.allow_stack.pop();
}
_ => {
let allow_here = parent_allow || is_test_item(&item.attrs);
self.allow_stack.push(allow_here);
visit::walk_item(self, item);
self.allow_stack.pop();
}
}
}
fn visit_assoc_item(
&mut self,
assoc_item: &'ast rustc_ast::Item<rustc_ast::AssocItemKind>,
ctxt: visit::AssocCtxt,
) {
let parent_allow = self.allow_stack.last().copied().unwrap_or(false);
let is_private = matches!(assoc_item.vis.kind, VisibilityKind::Inherited);
let allow_here = parent_allow
|| is_test_item(&assoc_item.attrs)
|| (self.in_proc_macro_crate && (is_private || has_proc_macro_attr(&assoc_item.attrs)));
self.allow_stack.push(allow_here);
visit::walk_assoc_item(self, assoc_item, ctxt);
self.allow_stack.pop();
}
fn visit_expr(&mut self, expr: &'ast rustc_ast::Expr) {
if let ExprKind::MacCall(mac_call) = &expr.kind {
self.lint_mac_call(mac_call);
}
visit::walk_expr(self, expr);
}
fn visit_mac_call(&mut self, mac_call: &'ast MacCall) {
self.lint_mac_call(mac_call);
}
}
fn has_proc_macro_attr(attrs: &[rustc_ast::Attribute]) -> bool {
attrs.iter().any(|attr| {
let AttrKind::Normal(normal) = &attr.kind else {
return false;
};
let Some(last) = normal.item.path.segments.last() else {
return false;
};
matches!(
last.ident.name.as_str(),
"proc_macro" | "proc_macro_attribute" | "proc_macro_derive"
)
})
}
fn is_test_item(attrs: &[Attribute]) -> bool {
attrs.iter().any(|attr| {
if attr.has_name(sym::test) {
return true;
}
if let Some(ident) = attr.path().last()
&& *ident == sym::test
{
return true;
}
if attr.has_name(sym::cfg)
&& let Some(list) = attr.meta_item_list()
{
return list.iter().any(|item| item.has_name(sym::test));
}
false
})
}