use anyhow::{Context, Result};
use camino::{Utf8Path, Utf8PathBuf};
use proc_macro2::{TokenStream, TokenTree};
use syn::{AttrStyle, Attribute, ImplItem, Item, Macro, Meta, visit::Visit};
#[path = "allow_scan/discovery.rs"]
mod discovery;
#[path = "allow_scan/inclusion.rs"]
mod inclusion;
#[path = "allow_scan/meta.rs"]
mod meta;
#[path = "allow_scan/roots.rs"]
pub mod roots;
#[path = "allow_scan/tokens.rs"]
mod tokens;
pub(crate) const SOURCE_EXTENSION: &str = "rs";
pub use discovery::rust_sources;
use inclusion::foreign_inclusion;
use meta::{render_attribute, render_path, suppressed_by};
use roots::is_sanctioned;
use tokens::{attribute_at, could_cover_a_policy_call, forwarded_attribute_at, macro_arms};
const PROTECTED_LINTS: [&str; 7] = [
"clippy::disallowed_methods",
"clippy::style",
"clippy::all",
"warnings",
"clippy::allow_attributes",
"clippy::allow_attributes_without_reason",
"clippy::restriction",
];
struct Suppression {
inner: bool,
meta: Meta,
item: Option<String>,
}
#[derive(Default)]
struct AttributeCollector {
found: Vec<Suppression>,
scope: Vec<String>,
structural: Vec<String>,
path: Utf8PathBuf,
}
fn item_name(item: &Item) -> Option<String> {
match item {
Item::Const(inner) => Some(inner.ident.to_string()),
Item::Enum(inner) => Some(inner.ident.to_string()),
Item::Fn(inner) => Some(inner.sig.ident.to_string()),
Item::Macro(inner) => inner.ident.as_ref().map(ToString::to_string),
Item::Mod(inner) => Some(inner.ident.to_string()),
Item::Static(inner) => Some(inner.ident.to_string()),
Item::Struct(inner) => Some(inner.ident.to_string()),
Item::Trait(inner) => Some(inner.ident.to_string()),
Item::TraitAlias(inner) => Some(inner.ident.to_string()),
Item::Type(inner) => Some(inner.ident.to_string()),
Item::Union(inner) => Some(inner.ident.to_string()),
_ => None,
}
}
fn impl_item_name(item: &ImplItem) -> Option<String> {
match item {
ImplItem::Const(inner) => Some(inner.ident.to_string()),
ImplItem::Fn(inner) => Some(inner.sig.ident.to_string()),
ImplItem::Type(inner) => Some(inner.ident.to_string()),
_ => None,
}
}
impl AttributeCollector {
fn within(&mut self, named: Option<String>, walk: impl FnOnce(&mut Self)) {
let pushed = named.is_some();
if let Some(name) = named {
self.scope.push(name);
}
walk(self);
if pushed {
self.scope.pop();
}
}
fn collect_from_tokens(&mut self, tokens: TokenStream, reachable: bool) {
let trees: Vec<TokenTree> = tokens.into_iter().collect();
let mut index = 0;
while index < trees.len() {
if let Some(finding) = forwarded_attribute_at(&trees, index, reachable) {
self.structural.push(finding);
index += 1;
continue;
}
if let Some((shape, after)) = attribute_at(&trees, index) {
self.found.push(Suppression {
inner: shape.inner,
meta: shape.meta,
item: self.scope.last().cloned(),
});
index = after;
continue;
}
if let TokenTree::Group(group) = &trees[index] {
self.collect_from_tokens(group.stream(), reachable);
}
index += 1;
}
}
}
impl<'ast> Visit<'ast> for AttributeCollector {
fn visit_attribute(&mut self, attribute: &'ast Attribute) {
self.found.push(Suppression {
inner: matches!(attribute.style, AttrStyle::Inner(_)),
meta: attribute.meta.clone(),
item: self.scope.last().cloned(),
});
}
fn visit_item(&mut self, item: &'ast Item) {
self.within(item_name(item), |collector| {
syn::visit::visit_item(collector, item);
});
}
fn visit_impl_item(&mut self, item: &'ast ImplItem) {
self.within(impl_item_name(item), |collector| {
syn::visit::visit_impl_item(collector, item);
});
}
fn visit_macro(&mut self, mac: &'ast Macro) {
match render_path(&mac.path).rsplit("::").next() {
Some("macro_rules") => {
for (pattern, transcriber) in macro_arms(mac.tokens.clone()) {
let reachable = could_cover_a_policy_call(&pattern, &transcriber);
self.collect_from_tokens(transcriber, reachable);
}
}
Some("include") => {
if let Some(finding) = foreign_inclusion(&mac.tokens, &self.path) {
self.structural.push(finding);
}
}
_ => {}
}
syn::visit::visit_macro(self, mac);
}
}
pub fn suppressed_lints(path: &Utf8Path, contents: &str) -> Result<Vec<String>> {
let parsed = syn::parse_file(contents).context("parse the source as Rust")?;
let mut collector = AttributeCollector {
path: path.to_owned(),
..AttributeCollector::default()
};
collector.visit_file(&parsed);
let mut found = collector.structural.clone();
for suppression in &collector.found {
let sanctioned = is_sanctioned(path, suppression.item.as_deref());
for lint in suppressed_by(&suppression.meta, suppression.inner, sanctioned) {
if PROTECTED_LINTS.contains(&lint.as_str()) {
found.push(describe(suppression, &lint));
}
}
}
Ok(found)
}
fn describe(suppression: &Suppression, lint: &str) -> String {
let rendered = render_attribute(suppression);
if render_path(suppression.meta.path()) == "expect" && !suppression.inner {
let item = suppression.item.as_deref().unwrap_or("the crate root");
return format!(
"expects {lint} at {item} via {rendered}, which is not a documented composition root"
);
}
format!("allows {lint} via {rendered}")
}