#![forbid(unsafe_code)]
#![forbid(warnings)]
extern crate proc_macro2;
extern crate syn;
extern crate walkdir;
use self::walkdir::DirEntry;
use self::walkdir::WalkDir;
use std::error::Error;
use std::fmt;
use std::fs::File;
use std::io;
use std::io::Read;
use std::ops::Add;
use std::path::Path;
use std::path::PathBuf;
use std::string::FromUtf8Error;
use syn::{visit, Expr, ImplItemMethod, ItemFn, ItemImpl, ItemMod, ItemTrait};
#[derive(Debug)]
pub enum ScanFileError {
Io(io::Error, PathBuf),
Utf8(FromUtf8Error, PathBuf),
Syn(syn::Error, PathBuf),
}
impl Error for ScanFileError {}
impl fmt::Display for ScanFileError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Debug::fmt(self, f)
}
}
#[derive(Debug, Default, Clone)]
pub struct Count {
pub safe: u64,
pub unsafe_: u64,
}
impl Count {
fn count(&mut self, is_unsafe: bool) {
if is_unsafe {
self.unsafe_ += 1;
} else {
self.safe += 1;
}
}
}
impl Add for Count {
type Output = Count;
fn add(self, other: Count) -> Count {
Count {
safe: self.safe + other.safe,
unsafe_: self.unsafe_ + other.unsafe_,
}
}
}
#[derive(Debug, Default, Clone)]
pub struct CounterBlock {
pub functions: Count,
pub exprs: Count,
pub item_impls: Count,
pub item_traits: Count,
pub methods: Count,
}
impl CounterBlock {
pub fn has_unsafe(&self) -> bool {
self.functions.unsafe_ > 0
|| self.exprs.unsafe_ > 0
|| self.item_impls.unsafe_ > 0
|| self.item_traits.unsafe_ > 0
|| self.methods.unsafe_ > 0
}
}
impl Add for CounterBlock {
type Output = CounterBlock;
fn add(self, other: CounterBlock) -> CounterBlock {
CounterBlock {
functions: self.functions + other.functions,
exprs: self.exprs + other.exprs,
item_impls: self.item_impls + other.item_impls,
item_traits: self.item_traits + other.item_traits,
methods: self.methods + other.methods,
}
}
}
#[derive(Debug, Default)]
pub struct RsFileMetrics {
pub counters: CounterBlock,
pub forbids_unsafe: bool,
}
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum IncludeTests {
Yes,
No,
}
struct GeigerSynVisitor {
include_tests: IncludeTests,
metrics: RsFileMetrics,
in_unsafe_block: bool,
}
impl GeigerSynVisitor {
fn new(include_tests: IncludeTests) -> Self {
GeigerSynVisitor {
include_tests,
metrics: Default::default(),
in_unsafe_block: false,
}
}
}
fn is_test_mod(i: &ItemMod) -> bool {
use syn::Attribute;
use syn::Meta;
i.attrs
.iter()
.flat_map(Attribute::interpret_meta)
.any(|m| match m {
Meta::List(ml) => meta_list_is_cfg_test(&ml),
_ => false,
})
}
fn meta_list_is_cfg_test(ml: &syn::MetaList) -> bool {
use syn::NestedMeta;
if ml.ident != "cfg" {
return false;
}
ml.nested.iter().any(|n| match n {
NestedMeta::Meta(meta) => meta_is_word_test(meta),
_ => false,
})
}
fn meta_is_word_test(m: &syn::Meta) -> bool {
use syn::Meta;
match m {
Meta::Word(ident) => ident == "test",
_ => false,
}
}
fn is_test_fn(i: &ItemFn) -> bool {
use syn::Attribute;
i.attrs
.iter()
.flat_map(Attribute::interpret_meta)
.any(|m| meta_is_word_test(&m))
}
fn file_forbids_unsafe(f: &syn::File) -> bool {
use proc_macro2::{Ident, Span};
use syn::AttrStyle;
use syn::Meta;
use syn::MetaList;
use syn::NestedMeta;
let forbid_ident = Ident::new("forbid", Span::call_site());
let unsafe_code_ident = Ident::new("unsafe_code", Span::call_site());
f.attrs
.iter()
.filter(|a| match a.style {
AttrStyle::Inner(_) => true,
_ => false,
})
.filter_map(|a| a.parse_meta().ok())
.filter(|meta| match meta {
Meta::List(MetaList {
ident,
paren_token: _paren,
nested,
}) => {
if ident != &forbid_ident {
return false;
}
nested.iter().any(|n| match n {
NestedMeta::Meta(Meta::Word(word)) => {
word == &unsafe_code_ident
}
_ => false,
})
}
_ => false,
})
.count()
> 0
}
impl<'ast> visit::Visit<'ast> for GeigerSynVisitor {
fn visit_file(&mut self, i: &'ast syn::File) {
self.metrics.forbids_unsafe = file_forbids_unsafe(i);
syn::visit::visit_file(self, i);
}
fn visit_item_fn(&mut self, i: &ItemFn) {
if IncludeTests::No == self.include_tests && is_test_fn(i) {
return;
}
self.metrics.counters.functions.count(i.unsafety.is_some());
visit::visit_item_fn(self, i);
}
fn visit_expr(&mut self, i: &Expr) {
match i {
Expr::Unsafe(i) => {
self.in_unsafe_block = true;
visit::visit_expr_unsafe(self, i);
self.in_unsafe_block = false;
}
Expr::Path(_) | Expr::Lit(_) => {
}
other => {
self.metrics.counters.exprs.count(self.in_unsafe_block);
visit::visit_expr(self, other);
}
}
}
fn visit_item_mod(&mut self, i: &ItemMod) {
if IncludeTests::No == self.include_tests && is_test_mod(i) {
return;
}
visit::visit_item_mod(self, i);
}
fn visit_item_impl(&mut self, i: &ItemImpl) {
self.metrics.counters.item_impls.count(i.unsafety.is_some());
visit::visit_item_impl(self, i);
}
fn visit_item_trait(&mut self, i: &ItemTrait) {
self.metrics
.counters
.item_traits
.count(i.unsafety.is_some());
visit::visit_item_trait(self, i);
}
fn visit_impl_item_method(&mut self, i: &ImplItemMethod) {
self.metrics
.counters
.methods
.count(i.sig.unsafety.is_some());
visit::visit_impl_item_method(self, i);
}
}
fn is_file_with_ext(entry: &DirEntry, file_ext: &str) -> bool {
if !entry.file_type().is_file() {
return false;
}
let p = entry.path();
let ext = match p.extension() {
Some(e) => e,
None => return false,
};
ext.to_string_lossy() == file_ext
}
pub fn find_rs_files_in_dir(dir: &Path) -> impl Iterator<Item = PathBuf> {
let walker = WalkDir::new(dir).into_iter();
walker.filter_map(|entry| {
let entry = entry.expect("walkdir error.");
if !is_file_with_ext(&entry, "rs") {
return None;
}
Some(
entry
.path()
.canonicalize()
.expect("Error converting to canonical path"),
)
})
}
pub fn find_unsafe_in_string(
src: &str,
include_tests: IncludeTests,
) -> Result<RsFileMetrics, syn::Error> {
use syn::visit::Visit;
let syntax = syn::parse_file(&src)?;
let mut vis = GeigerSynVisitor::new(include_tests);
vis.visit_file(&syntax);
Ok(vis.metrics)
}
pub fn find_unsafe_in_file(
p: &Path,
include_tests: IncludeTests,
) -> Result<RsFileMetrics, ScanFileError> {
let mut file =
File::open(p).map_err(|e| ScanFileError::Io(e, p.to_path_buf()))?;
let mut src = vec![];
file.read_to_end(&mut src)
.map_err(|e| ScanFileError::Io(e, p.to_path_buf()))?;
let src = String::from_utf8(src)
.map_err(|e| ScanFileError::Utf8(e, p.to_path_buf()))?;
find_unsafe_in_string(&src, include_tests)
.map_err(|e| ScanFileError::Syn(e, p.to_path_buf()))
}