diff options
author | Chih-Hung Hsieh <chh@google.com> | 2020-03-17 13:20:31 -0700 |
---|---|---|
committer | Chih-Hung Hsieh <chh@google.com> | 2020-03-19 11:34:20 -0700 |
commit | 5846f731f46c552ac3a2dc8b38dea64df8aca81a (patch) | |
tree | 1b9f1909282fc1970ffd41958cc0188f03db2636 /src | |
parent | 3095d1536defaeecd3bda0e10483589a2ebc1428 (diff) | |
download | remain-5846f731f46c552ac3a2dc8b38dea64df8aca81a.tar.gz |
Remove old 0.1.3; used only by old crosvm.
* 0.2.1 becomes the default
Test: make
Bug: 151628085
Change-Id: If095bf4f0fa9e33269df62fc346d5b4d75b0e5aa
Diffstat (limited to 'src')
-rw-r--r-- | src/atom.rs | 141 | ||||
-rw-r--r-- | src/check.rs | 136 | ||||
-rw-r--r-- | src/compare.rs | 68 | ||||
-rw-r--r-- | src/emit.rs | 39 | ||||
-rw-r--r-- | src/format.rs | 27 | ||||
-rw-r--r-- | src/lib.rs | 180 | ||||
-rw-r--r-- | src/parse.rs | 91 | ||||
-rw-r--r-- | src/visit.rs | 79 |
8 files changed, 761 insertions, 0 deletions
diff --git a/src/atom.rs b/src/atom.rs new file mode 100644 index 0000000..bd1ef86 --- /dev/null +++ b/src/atom.rs @@ -0,0 +1,141 @@ +use std::cmp::{Ord, Ordering, PartialOrd}; +use std::str; + +#[derive(Copy, Clone, Eq, PartialEq)] +pub enum Atom<'a> { + /// A sequence of underscores. + Underscore(usize), + /// A sequence of digits. + Number(&'a str), + /// A sequence of characters. + Chars(&'a str), +} + +impl Atom<'_> { + pub fn underscores(&self) -> usize { + match *self { + Atom::Underscore(n) => n, + _ => 0, + } + } +} + +impl PartialOrd for Atom<'_> { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl Ord for Atom<'_> { + fn cmp(&self, other: &Self) -> Ordering { + use self::Atom::*; + + match (self, other) { + (Underscore(l), Underscore(r)) => l.cmp(r), + (Underscore(_), _) => Ordering::Less, + (_, Underscore(_)) => Ordering::Greater, + (Number(l), Number(r)) => cmp_numeric(l, r), + (Number(_), Chars(_)) => Ordering::Less, + (Chars(_), Number(_)) => Ordering::Greater, + (Chars(l), Chars(r)) => cmp_ignore_case(l, r), + } + } +} + +fn cmp_numeric(l: &str, r: &str) -> Ordering { + // Trim leading zeros. + let l = l.trim_start_matches('0'); + let r = r.trim_start_matches('0'); + + match l.len().cmp(&r.len()) { + Ordering::Equal => l.cmp(r), + non_eq => non_eq, + } +} + +fn cmp_ignore_case(l: &str, r: &str) -> Ordering { + for (a, b) in l.bytes().zip(r.bytes()) { + match a.to_ascii_lowercase().cmp(&b.to_ascii_lowercase()) { + Ordering::Equal => match a.cmp(&b) { + Ordering::Equal => {} + non_eq => return non_eq, + }, + non_eq => return non_eq, + } + } + + l.len().cmp(&r.len()) +} + +pub fn iter_atoms(string: &str) -> AtomIter { + AtomIter { + bytes: string.as_bytes(), + offset: 0, + } +} + +pub struct AtomIter<'a> { + bytes: &'a [u8], + offset: usize, +} + +impl<'a> Iterator for AtomIter<'a> { + type Item = Atom<'a>; + + fn next(&mut self) -> Option<Atom<'a>> { + if self.offset >= self.bytes.len() { + return None; + } + + let x = self.bytes[self.offset]; + + match x { + b'_' => { + self.offset += 1; + + let mut n = 1; + while self.offset < self.bytes.len() { + match self.bytes[self.offset] { + b'_' => { + self.offset += 1; + n += 1; + } + _ => break, + } + } + + Some(Atom::Underscore(n)) + } + b'0'..=b'9' => { + let start = self.offset; + + self.offset += 1; + while self.offset < self.bytes.len() { + match self.bytes[self.offset] { + b'0'..=b'9' => self.offset += 1, + _ => break, + } + } + + let bytes = &self.bytes[start..self.offset]; + let number = str::from_utf8(bytes).expect("valid utf8"); + Some(Atom::Number(number)) + } + _ => { + let start = self.offset; + + self.offset += 1; + while self.offset < self.bytes.len() { + match self.bytes[self.offset] { + b'_' | b'0'..=b'9' => break, + _ => self.offset += 1, + } + } + + let bytes = &self.bytes[start..self.offset]; + let chars = str::from_utf8(bytes).expect("valid utf8"); + Some(Atom::Chars(chars)) + } + } + } +} diff --git a/src/check.rs b/src/check.rs new file mode 100644 index 0000000..0643f5f --- /dev/null +++ b/src/check.rs @@ -0,0 +1,136 @@ +use quote::quote; +use std::cmp::Ordering; +use syn::{Arm, Attribute, Ident, Result, Variant}; +use syn::{Error, Field, Pat, PatIdent}; + +use crate::compare::{cmp, Path, UnderscoreOrder}; +use crate::format; +use crate::parse::Input::{self, *}; + +pub fn sorted(input: &mut Input) -> Result<()> { + let paths = match input { + Enum(item) => collect_paths(&mut item.variants)?, + Struct(item) => collect_paths(&mut item.fields)?, + Match(expr) | Let(expr) => collect_paths(&mut expr.arms)?, + }; + + let mode = UnderscoreOrder::First; + if find_misordered(&paths, mode).is_none() { + return Ok(()); + } + + let mode = UnderscoreOrder::Last; + let wrong = match find_misordered(&paths, mode) { + Some(wrong) => wrong, + None => return Ok(()), + }; + + let lesser = &paths[wrong]; + let correct_pos = match paths[..wrong - 1].binary_search_by(|probe| cmp(probe, lesser, mode)) { + Err(correct_pos) => correct_pos, + Ok(equal_to) => equal_to + 1, + }; + let greater = &paths[correct_pos]; + Err(format::error(lesser, greater)) +} + +fn find_misordered(paths: &[Path], mode: UnderscoreOrder) -> Option<usize> { + for i in 1..paths.len() { + if cmp(&paths[i], &paths[i - 1], mode) == Ordering::Less { + return Some(i); + } + } + + None +} + +fn collect_paths<'a, I, P>(iter: I) -> Result<Vec<Path>> +where + I: IntoIterator<Item = &'a mut P>, + P: Sortable + 'a, +{ + iter.into_iter() + .filter_map(|item| { + if remove_unsorted_attr(item.attrs()) { + None + } else { + Some(item.to_path()) + } + }) + .collect() +} + +fn remove_unsorted_attr(attrs: &mut Vec<Attribute>) -> bool { + for i in 0..attrs.len() { + let path = &attrs[i].path; + let path = quote!(#path).to_string(); + if path == "unsorted" || path == "remain :: unsorted" { + attrs.remove(i); + return true; + } + } + + false +} + +trait Sortable { + fn to_path(&self) -> Result<Path>; + fn attrs(&mut self) -> &mut Vec<Attribute>; +} + +impl Sortable for Variant { + fn to_path(&self) -> Result<Path> { + Ok(Path { + segments: vec![self.ident.clone()], + }) + } + fn attrs(&mut self) -> &mut Vec<Attribute> { + &mut self.attrs + } +} + +impl Sortable for Field { + fn to_path(&self) -> Result<Path> { + Ok(Path { + segments: vec![self.ident.clone().expect("must be named field")], + }) + } + fn attrs(&mut self) -> &mut Vec<Attribute> { + &mut self.attrs + } +} + +impl Sortable for Arm { + fn to_path(&self) -> Result<Path> { + // Sort by just the first pat. + let pat = match &self.pat { + Pat::Or(pat) => pat.cases.iter().next().expect("at least one pat"), + _ => &self.pat, + }; + + let segments = match pat { + Pat::Ident(pat) if is_just_ident(&pat) => vec![pat.ident.clone()], + Pat::Path(pat) => idents_of_path(&pat.path), + Pat::Struct(pat) => idents_of_path(&pat.path), + Pat::TupleStruct(pat) => idents_of_path(&pat.path), + Pat::Wild(pat) => vec![Ident::from(pat.underscore_token)], + other => { + let msg = "unsupported by #[remain::sorted]"; + return Err(Error::new_spanned(other, msg)); + } + }; + + Ok(Path { segments }) + } + fn attrs(&mut self) -> &mut Vec<Attribute> { + &mut self.attrs + } +} + +fn idents_of_path(path: &syn::Path) -> Vec<Ident> { + path.segments.iter().map(|seg| seg.ident.clone()).collect() +} + +fn is_just_ident(pat: &PatIdent) -> bool { + pat.by_ref.is_none() && pat.mutability.is_none() && pat.subpat.is_none() +} diff --git a/src/compare.rs b/src/compare.rs new file mode 100644 index 0000000..3fa4198 --- /dev/null +++ b/src/compare.rs @@ -0,0 +1,68 @@ +use proc_macro2::Ident; +use std::cmp::Ordering; + +use crate::atom::iter_atoms; + +#[derive(Copy, Clone, PartialEq)] +pub enum UnderscoreOrder { + First, + Last, +} + +pub struct Path { + pub segments: Vec<Ident>, +} + +pub fn cmp(lhs: &Path, rhs: &Path, mode: UnderscoreOrder) -> Ordering { + // Lexicographic ordering across path segments. + for (lhs, rhs) in lhs.segments.iter().zip(&rhs.segments) { + match cmp_segment(&lhs.to_string(), &rhs.to_string(), mode) { + Ordering::Equal => {} + non_eq => return non_eq, + } + } + + lhs.segments.len().cmp(&rhs.segments.len()) +} + +fn cmp_segment(lhs: &str, rhs: &str, mode: UnderscoreOrder) -> Ordering { + // Sort `_` last. + match (lhs, rhs) { + ("_", "_") => return Ordering::Equal, + ("_", _) => return Ordering::Greater, + (_, "_") => return Ordering::Less, + (_, _) => {} + } + + let mut lhs_atoms = iter_atoms(lhs); + let mut rhs_atoms = iter_atoms(rhs); + + // Path segments can't be empty. + let mut left = lhs_atoms.next().unwrap(); + let mut right = rhs_atoms.next().unwrap(); + + if mode == UnderscoreOrder::Last { + // Compare leading underscores. + match left.underscores().cmp(&right.underscores()) { + Ordering::Equal => {} + non_eq => return non_eq, + } + } + + loop { + match left.cmp(&right) { + Ordering::Equal => {} + non_eq => return non_eq, + } + + match (lhs_atoms.next(), rhs_atoms.next()) { + (None, None) => return Ordering::Equal, + (None, Some(_)) => return Ordering::Less, + (Some(_), None) => return Ordering::Greater, + (Some(nextl), Some(nextr)) => { + left = nextl; + right = nextr; + } + } + } +} diff --git a/src/emit.rs b/src/emit.rs new file mode 100644 index 0000000..4a051ba --- /dev/null +++ b/src/emit.rs @@ -0,0 +1,39 @@ +use proc_macro::TokenStream; +use proc_macro2::Span; +use quote::quote; +use syn::Error; + +#[derive(Copy, Clone)] +pub enum Kind { + Enum, + Match, + Struct, + Let, +} + +pub fn emit(err: Error, kind: Kind, output: TokenStream) -> TokenStream { + let mut err = err; + if !probably_has_spans(kind) { + // Otherwise the error is printed without any line number. + err = Error::new(Span::call_site(), &err.to_string()); + } + + let err = err.to_compile_error(); + let output = proc_macro2::TokenStream::from(output); + + let expanded = match kind { + Kind::Enum | Kind::Let | Kind::Struct => quote!(#err #output), + Kind::Match => quote!({ #err #output }), + }; + + TokenStream::from(expanded) +} + +// Rustc is so bad at spans. +// https://github.com/rust-lang/rust/issues/43081 +fn probably_has_spans(kind: Kind) -> bool { + match kind { + Kind::Enum | Kind::Struct => true, + Kind::Match | Kind::Let => false, + } +} diff --git a/src/format.rs b/src/format.rs new file mode 100644 index 0000000..5832643 --- /dev/null +++ b/src/format.rs @@ -0,0 +1,27 @@ +use proc_macro2::TokenStream; +use quote::TokenStreamExt; +use std::fmt::{self, Display}; +use syn::Error; + +use crate::compare::Path; + +impl Display for Path { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + for (i, segment) in self.segments.iter().enumerate() { + if i > 0 { + formatter.write_str("::")?; + } + segment.fmt(formatter)?; + } + Ok(()) + } +} + +pub fn error(lesser: &Path, greater: &Path) -> Error { + let mut spans = TokenStream::new(); + spans.append_all(&lesser.segments); + + let msg = format!("{} should sort before {}", lesser, greater); + + Error::new_spanned(spans, msg) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..5648e28 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,180 @@ +//! This crate provides an attribute macro to check at compile time that the +//! variants of an enum or the arms of a match expression are written in sorted +//! order. +//! +//! # Syntax +//! +//! Place a `#[remain::sorted]` attribute on enums, structs, match-expressions, +//! or let-statements whose value is a match-expression. +//! +//! Alternatively, import as `use remain::sorted;` and use `#[sorted]` as the +//! attribute. +//! +//! ``` +//! # use std::error::Error as StdError; +//! # use std::fmt::{self, Display}; +//! # use std::io; +//! # +//! #[remain::sorted] +//! #[derive(Debug)] +//! pub enum Error { +//! BlockSignal(signal::Error), +//! CreateCrasClient(libcras::Error), +//! CreateEventFd(sys_util::Error), +//! CreateSignalFd(sys_util::SignalFdError), +//! CreateSocket(io::Error), +//! DetectImageType(qcow::Error), +//! DeviceJail(io_jail::Error), +//! NetDeviceNew(virtio::NetError), +//! SpawnVcpu(io::Error), +//! } +//! +//! impl Display for Error { +//! # #[remain::check] +//! fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { +//! use self::Error::*; +//! +//! #[remain::sorted] +//! match self { +//! BlockSignal(e) => write!(f, "failed to block signal: {}", e), +//! CreateCrasClient(e) => write!(f, "failed to create cras client: {}", e), +//! CreateEventFd(e) => write!(f, "failed to create eventfd: {}", e), +//! CreateSignalFd(e) => write!(f, "failed to create signalfd: {}", e), +//! CreateSocket(e) => write!(f, "failed to create socket: {}", e), +//! DetectImageType(e) => write!(f, "failed to detect disk image type: {}", e), +//! DeviceJail(e) => write!(f, "failed to jail device: {}", e), +//! NetDeviceNew(e) => write!(f, "failed to set up virtio networking: {}", e), +//! SpawnVcpu(e) => write!(f, "failed to spawn VCPU thread: {}", e), +//! } +//! } +//! } +//! # +//! # mod signal { +//! # pub use std::io::Error; +//! # } +//! # +//! # mod libcras { +//! # pub use std::io::Error; +//! # } +//! # +//! # mod sys_util { +//! # pub use std::io::{Error, Error as SignalFdError}; +//! # } +//! # +//! # mod qcow { +//! # pub use std::io::Error; +//! # } +//! # +//! # mod io_jail { +//! # pub use std::io::Error; +//! # } +//! # +//! # mod virtio { +//! # pub use std::io::Error as NetError; +//! # } +//! # +//! # fn main() {} +//! ``` +//! +//! If an enum variant, struct field, or match arm is inserted out of order,\ +//! +//! ```diff +//! NetDeviceNew(virtio::NetError), +//! SpawnVcpu(io::Error), +//! + AaaUhOh(Box<dyn StdError>), +//! } +//! ``` +//! +//! then the macro produces a compile error. +//! +//! ```console +//! error: AaaUhOh should sort before BlockSignal +//! --> tests/stable.rs:49:5 +//! | +//! 49 | AaaUhOh(Box<dyn StdError>), +//! | ^^^^^^^ +//! ``` +//! +//! # Compiler support +//! +//! The attribute on enums is supported on any rustc version 1.31+. +//! +//! Rust does not yet have stable support for user-defined attributes within a +//! function body, so the attribute on match-expressions and let-statements +//! requires a nightly compiler and the following two features enabled: +//! +//! ``` +//! # const IGNORE: &str = stringify! { +//! #![feature(proc_macro_hygiene, stmt_expr_attributes)] +//! # }; +//! ``` +//! +//! As a stable alternative, this crate provides a function-level attribute +//! called `#[remain::check]` which makes match-expression and let-statement +//! attributes work on any rustc version 1.31+. Place this attribute on any +//! function containing `#[sorted]` to make them work on a stable compiler. +//! +//! ``` +//! # use std::fmt::{self, Display}; +//! # +//! # enum Error {} +//! # +//! impl Display for Error { +//! #[remain::check] +//! fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { +//! use self::Error::*; +//! +//! #[sorted] +//! match self { +//! /* ... */ +//! # _ => unimplemented!(), +//! } +//! } +//! } +//! # +//! # fn main() {} +//! ``` + +#![allow(clippy::needless_doctest_main)] + +extern crate proc_macro; + +mod atom; +mod check; +mod compare; +mod emit; +mod format; +mod parse; +mod visit; + +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, ItemFn}; + +use crate::emit::emit; +use crate::parse::{Input, Nothing}; + +#[proc_macro_attribute] +pub fn sorted(args: TokenStream, input: TokenStream) -> TokenStream { + let _ = parse_macro_input!(args as Nothing); + let mut input = parse_macro_input!(input as Input); + let kind = input.kind(); + + let result = check::sorted(&mut input); + let output = TokenStream::from(quote!(#input)); + + match result { + Ok(_) => output, + Err(err) => emit(err, kind, output), + } +} + +#[proc_macro_attribute] +pub fn check(args: TokenStream, input: TokenStream) -> TokenStream { + let _ = parse_macro_input!(args as Nothing); + let mut input = parse_macro_input!(input as ItemFn); + + visit::check(&mut input); + + TokenStream::from(quote!(#input)) +} diff --git a/src/parse.rs b/src/parse.rs new file mode 100644 index 0000000..aff4972 --- /dev/null +++ b/src/parse.rs @@ -0,0 +1,91 @@ +use proc_macro2::{Span, TokenStream}; +use quote::ToTokens; +use syn::parse::{Parse, ParseStream}; +use syn::{Attribute, Error, Expr, Fields, Result, Stmt, Token, Visibility}; + +use crate::emit::Kind; + +pub struct Nothing; + +impl Parse for Nothing { + fn parse(_input: ParseStream) -> Result<Self> { + Ok(Nothing) + } +} + +pub enum Input { + Enum(syn::ItemEnum), + Match(syn::ExprMatch), + Struct(syn::ItemStruct), + Let(syn::ExprMatch), +} + +impl Input { + pub fn kind(&self) -> Kind { + match self { + Input::Enum(_) => Kind::Enum, + Input::Match(_) => Kind::Match, + Input::Struct(_) => Kind::Struct, + Input::Let(_) => Kind::Let, + } + } +} + +impl Parse for Input { + fn parse(input: ParseStream) -> Result<Self> { + let ahead = input.fork(); + let _ = ahead.call(Attribute::parse_outer)?; + + if ahead.peek(Token![match]) { + let expr = match input.parse()? { + Expr::Match(expr) => expr, + _ => unreachable!("expected match"), + }; + return Ok(Input::Match(expr)); + } + + if ahead.peek(Token![let]) { + let stmt = match input.parse()? { + Stmt::Local(stmt) => stmt, + _ => unreachable!("expected let"), + }; + let init = match stmt.init { + Some((_, init)) => *init, + None => return Err(unexpected()), + }; + let expr = match init { + Expr::Match(expr) => expr, + _ => return Err(unexpected()), + }; + return Ok(Input::Let(expr)); + } + + let _: Visibility = ahead.parse()?; + if ahead.peek(Token![enum]) { + return input.parse().map(Input::Enum); + } else if ahead.peek(Token![struct]) { + let input: syn::ItemStruct = input.parse()?; + if let Fields::Named(_) = &input.fields { + return Ok(Input::Struct(input)); + } + } + + Err(unexpected()) + } +} + +impl ToTokens for Input { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Input::Enum(item) => item.to_tokens(tokens), + Input::Struct(item) => item.to_tokens(tokens), + Input::Match(expr) | Input::Let(expr) => expr.to_tokens(tokens), + } + } +} + +fn unexpected() -> Error { + let span = Span::call_site(); + let msg = "expected enum, struct, or match expression"; + Error::new(span, msg) +} diff --git a/src/visit.rs b/src/visit.rs new file mode 100644 index 0000000..f5cecbd --- /dev/null +++ b/src/visit.rs @@ -0,0 +1,79 @@ +use quote::quote; +use syn::visit_mut::{self, VisitMut}; +use syn::{parse_quote, Attribute, Expr, ExprMatch, ItemFn, Local}; + +use crate::parse::Input; + +pub fn check(input: &mut ItemFn) { + Checker.visit_item_fn_mut(input); +} + +struct Checker; + +impl VisitMut for Checker { + fn visit_expr_mut(&mut self, expr: &mut Expr) { + visit_mut::visit_expr_mut(self, expr); + + let expr_match = match expr { + Expr::Match(expr) => expr, + _ => return, + }; + + if !take_sorted_attr(&mut expr_match.attrs) { + return; + } + + let input = expr_match.clone(); + check_and_insert_error(input, expr); + } + + fn visit_local_mut(&mut self, local: &mut Local) { + visit_mut::visit_local_mut(self, local); + + let init = match &local.init { + Some((_, init)) => init, + None => return, + }; + + let expr_match = match init.as_ref() { + Expr::Match(expr) => expr, + _ => return, + }; + + if !take_sorted_attr(&mut local.attrs) { + return; + } + + let input = expr_match.clone(); + let expr = local.init.as_mut().unwrap().1.as_mut(); + check_and_insert_error(input, expr); + } +} + +fn take_sorted_attr(attrs: &mut Vec<Attribute>) -> bool { + for i in 0..attrs.len() { + let path = &attrs[i].path; + let path = quote!(#path).to_string(); + if path == "sorted" || path == "remain :: sorted" { + attrs.remove(i); + return true; + } + } + + false +} + +fn check_and_insert_error(input: ExprMatch, out: &mut Expr) { + let mut input = Input::Match(input); + + *out = match crate::check::sorted(&mut input) { + Ok(_) => parse_quote!(#input), + Err(err) => { + let err = err.to_compile_error(); + parse_quote!({ + #err + #input + }) + } + }; +} |