diff options
author | Jeff Vander Stoep <jeffv@google.com> | 2019-06-21 13:13:47 -0700 |
---|---|---|
committer | android-build-merger <android-build-merger@google.com> | 2019-06-21 13:13:47 -0700 |
commit | 33e56125440c4f95d6c5dba25d075af820e8b636 (patch) | |
tree | db22dc61f6d23fa1dd34d4b4356a989ee8c51f5a | |
parent | 999390bbb063512fa25468c6ac152b8d3e315cec (diff) | |
parent | 46a888dcabc80678e25d77d9c4ef203c988fda29 (diff) | |
download | remain-33e56125440c4f95d6c5dba25d075af820e8b636.tar.gz |
Merge remote-tracking branch 'aosp/upstream-master' into mymerge
am: 46a888dcab
Change-Id: Idb47f799199fa2cfbce3585708396ec4a31efd8e
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | .travis.yml | 14 | ||||
-rw-r--r-- | Cargo.toml | 22 | ||||
-rw-r--r-- | LICENSE-APACHE | 201 | ||||
-rw-r--r-- | LICENSE-MIT | 23 | ||||
-rw-r--r-- | README.md | 135 | ||||
-rw-r--r-- | src/check.rs | 83 | ||||
-rw-r--r-- | src/compare.rs | 45 | ||||
-rw-r--r-- | src/emit.rs | 39 | ||||
-rw-r--r-- | src/format.rs | 27 | ||||
-rw-r--r-- | src/lib.rs | 176 | ||||
-rw-r--r-- | src/parse.rs | 80 | ||||
-rw-r--r-- | src/visit.rs | 77 | ||||
-rw-r--r-- | tests/compiletest.rs | 6 | ||||
-rw-r--r-- | tests/stable.rs | 45 | ||||
-rw-r--r-- | tests/ui/enum.rs | 11 | ||||
-rw-r--r-- | tests/ui/enum.stderr | 5 | ||||
-rw-r--r-- | tests/ui/let-stable.rs | 19 | ||||
-rw-r--r-- | tests/ui/let-stable.stderr | 5 | ||||
-rw-r--r-- | tests/ui/let-unstable.rs | 22 | ||||
-rw-r--r-- | tests/ui/let-unstable.stderr | 5 | ||||
-rw-r--r-- | tests/ui/match-stable.rs | 19 | ||||
-rw-r--r-- | tests/ui/match-stable.stderr | 5 | ||||
-rw-r--r-- | tests/ui/match-unstable.rs | 22 | ||||
-rw-r--r-- | tests/ui/match-unstable.stderr | 5 | ||||
-rw-r--r-- | tests/ui/struct.rs | 11 | ||||
-rw-r--r-- | tests/ui/struct.stderr | 5 | ||||
-rw-r--r-- | tests/ui/unnamed-fields.rs | 7 | ||||
-rw-r--r-- | tests/ui/unnamed-fields.stderr | 11 | ||||
-rw-r--r-- | tests/ui/unsupported.rs | 10 | ||||
-rw-r--r-- | tests/ui/unsupported.stderr | 5 | ||||
-rw-r--r-- | tests/unstable.rs | 45 |
32 files changed, 1188 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6936990 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..fe51652 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: rust + +script: + - cargo test + +matrix: + include: + - rust: nightly + - rust: beta + env: RUSTFLAGS='--cfg remain_stable_testing' + - rust: stable + env: RUSTFLAGS='--cfg remain_stable_testing' + - rust: 1.31.0 + env: RUSTFLAGS='--cfg remain_stable_testing' diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c635f7a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "remain" +version = "0.1.3" # remember to update number in readme for major versions +authors = ["David Tolnay <dtolnay@gmail.com>"] +edition = "2018" +license = "MIT OR Apache-2.0" +description = "Compile-time checks that an enum, struct, or match is written in sorted order." +repository = "https://github.com/dtolnay/remain" +documentation = "https://docs.rs/remain" +readme = "README.md" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "0.4" +quote = "0.6" +syn = { version = "0.15", features = ["full", "visit-mut"] } + +[dev-dependencies] +select-rustc = "0.1" +trybuild = "1.0" diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..16fe87b --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..31aa793 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c3a4877 --- /dev/null +++ b/README.md @@ -0,0 +1,135 @@ +Remain sorted +============= + +[![Build Status](https://api.travis-ci.com/dtolnay/remain.svg?branch=master)](https://travis-ci.com/dtolnay/remain) +[![Latest Version](https://img.shields.io/crates/v/remain.svg)](https://crates.io/crates/remain) +[![Rust Documentation](https://img.shields.io/badge/api-rustdoc-blue.svg)](https://docs.rs/remain) + +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. + +```toml +[dependencies] +remain = "0.1" +``` + +## 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. + +```rust +#[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), +} + +#[remain::sorted] +#[derive(Debug)] +pub enum Registers { + ax: u16, + cx: u16, + di: u16, + si: u16, + sp: u16, +} + +impl Display for Error { + 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), + } + } +} +``` + +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 and structs 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: + +```rust +#![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. + +```rust +impl Display for Error { + #[remain::check] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::Error::*; + + #[sorted] + match self { + /* ... */ + } + } +} +``` + +<br> + +#### License + +<sup> +Licensed under either of <a href="LICENSE-APACHE">Apache License, Version +2.0</a> or <a href="LICENSE-MIT">MIT license</a> at your option. +</sup> + +<br> + +<sub> +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in this crate by you, as defined in the Apache-2.0 license, shall +be dual licensed as above, without any additional terms or conditions. +</sub> diff --git a/src/check.rs b/src/check.rs new file mode 100644 index 0000000..0995078 --- /dev/null +++ b/src/check.rs @@ -0,0 +1,83 @@ +use syn::{Arm, Ident, Result, Variant}; +use syn::{Error, Field, Pat, PatIdent}; + +use crate::compare::Path; +use crate::format; +use crate::parse::Input::{self, *}; + +pub fn sorted(input: Input) -> Result<()> { + let paths = match input { + Enum(item) => collect_paths(item.variants)?, + Struct(fields) => collect_paths(fields.named)?, + Match(expr) | Let(expr) => collect_paths(expr.arms)?, + }; + + for i in 1..paths.len() { + let cur = &paths[i]; + if *cur < paths[i - 1] { + let lesser = cur; + let correct_pos = paths[..i - 1].binary_search(cur).unwrap_err(); + let greater = &paths[correct_pos]; + return Err(format::error(lesser, greater)); + } + } + + Ok(()) +} + +fn collect_paths<I>(iter: I) -> Result<Vec<Path>> +where + I: IntoIterator, + I::Item: IntoPath, +{ + iter.into_iter().map(IntoPath::into_path).collect() +} + +trait IntoPath { + fn into_path(self) -> Result<Path>; +} + +impl IntoPath for Variant { + fn into_path(self) -> Result<Path> { + Ok(Path { + segments: vec![self.ident], + }) + } +} + +impl IntoPath for Field { + fn into_path(self) -> Result<Path> { + Ok(Path { + segments: vec![self.ident.expect("must be named field")], + }) + } +} + +impl IntoPath for Arm { + fn into_path(self) -> Result<Path> { + // Sort by just the first pat. + let pat = self.pats.into_iter().next().expect("at least one pat"); + + let segments = match pat { + Pat::Wild(pat) => vec![Ident::from(pat.underscore_token)], + 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::Ident(ref pat) if is_just_ident(pat) => vec![pat.ident.clone()], + other => { + let msg = "unsupported by #[remain::sorted]"; + return Err(Error::new_spanned(other, msg)); + } + }; + + Ok(Path { segments }) + } +} + +fn idents_of_path(path: syn::Path) -> Vec<Ident> { + path.segments.into_iter().map(|seg| seg.ident).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..59f997d --- /dev/null +++ b/src/compare.rs @@ -0,0 +1,45 @@ +use proc_macro2::Ident; +use std::cmp::Ordering; + +#[derive(PartialEq, Eq)] +pub struct Path { + pub segments: Vec<Ident>, +} + +impl PartialOrd for Path { + fn partial_cmp(&self, other: &Path) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl Ord for Path { + fn cmp(&self, other: &Path) -> Ordering { + // Lexicographic ordering across path segments. + for (lhs, rhs) in self.segments.iter().zip(&other.segments) { + match cmp(&lhs.to_string(), &rhs.to_string()) { + Ordering::Equal => {} + non_eq => return non_eq, + } + } + + self.segments.len().cmp(&other.segments.len()) + } +} + +// TODO: more intelligent comparison +// for example to handle numeric cases like E9 < E10. +fn cmp(lhs: &str, rhs: &str) -> Ordering { + // Sort `_` last. + match (lhs == "_", rhs == "_") { + (true, true) => return Ordering::Equal, + (true, false) => return Ordering::Greater, + (false, true) => return Ordering::Less, + (false, false) => {} + } + + let lhs = lhs.to_ascii_lowercase(); + let rhs = rhs.to_ascii_lowercase(); + + // For now: asciibetical ordering. + lhs.cmp(&rhs) +} diff --git a/src/emit.rs b/src/emit.rs new file mode 100644 index 0000000..d1ddda8 --- /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, original: 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 original = proc_macro2::TokenStream::from(original); + + let expanded = match kind { + Kind::Enum | Kind::Let | Kind::Struct => quote!(#err #original), + Kind::Match => quote!({ #err #original }), + }; + + 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..967143e --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,176 @@ +//! 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() {} +//! ``` + +extern crate proc_macro; + +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 original = input.clone(); + + let _ = parse_macro_input!(args as Nothing); + let input = parse_macro_input!(input as Input); + let kind = input.kind(); + + match check::sorted(input) { + Ok(()) => original, + Err(err) => emit(err, kind, original), + } +} + +#[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..97bd9f8 --- /dev/null +++ b/src/parse.rs @@ -0,0 +1,80 @@ +use proc_macro2::Span; +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::FieldsNamed), + 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 _ = input.call(Attribute::parse_outer)?; + + if input.peek(Token![match]) { + let expr = match input.parse()? { + Expr::Match(expr) => expr, + _ => unreachable!("expected match"), + }; + return Ok(Input::Match(expr)); + } + + if input.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 ahead = input.fork(); + 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(fields) = input.fields { + return Ok(Input::Struct(fields)); + } + } + + Err(unexpected()) + } +} + +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..f681aa9 --- /dev/null +++ b/src/visit.rs @@ -0,0 +1,77 @@ +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 original = quote!(#input); + let input = Input::Match(input); + + if let Err(err) = crate::check::sorted(input) { + let err = err.to_compile_error(); + *out = parse_quote!({ + #err + #original + }); + } +} diff --git a/tests/compiletest.rs b/tests/compiletest.rs new file mode 100644 index 0000000..2e861bf --- /dev/null +++ b/tests/compiletest.rs @@ -0,0 +1,6 @@ +#[rustc::attr(not(nightly), ignore)] +#[test] +fn ui() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/ui/*.rs"); +} diff --git a/tests/stable.rs b/tests/stable.rs new file mode 100644 index 0000000..a3b6a7b --- /dev/null +++ b/tests/stable.rs @@ -0,0 +1,45 @@ +#![allow(dead_code)] + +#[remain::sorted] +pub enum TestEnum { + A, + B, + C, + D, +} + +#[remain::sorted] +pub struct TestStruct { + a: usize, + b: usize, + c: usize, + d: usize, +} + +#[test] +#[remain::check] +fn test_match() { + let value = TestEnum::A; + + #[sorted] + let _ = match value { + TestEnum::A => {} + TestEnum::B => {} + TestEnum::C => {} + _ => {} + }; +} + +#[test] +#[remain::check] +fn test_let() { + let value = TestEnum::A; + + #[sorted] + match value { + TestEnum::A => {} + TestEnum::B => {} + TestEnum::C => {} + _ => {} + } +} diff --git a/tests/ui/enum.rs b/tests/ui/enum.rs new file mode 100644 index 0000000..dc98475 --- /dev/null +++ b/tests/ui/enum.rs @@ -0,0 +1,11 @@ +use remain::sorted; + +#[sorted] +enum E { + Aaa, + Ccc(u8), + Ddd { u: u8 }, + Bbb(u8, u8), +} + +fn main() {} diff --git a/tests/ui/enum.stderr b/tests/ui/enum.stderr new file mode 100644 index 0000000..50ffa8c --- /dev/null +++ b/tests/ui/enum.stderr @@ -0,0 +1,5 @@ +error: Bbb should sort before Ccc + --> $DIR/enum.rs:8:5 + | +8 | Bbb(u8, u8), + | ^^^ diff --git a/tests/ui/let-stable.rs b/tests/ui/let-stable.rs new file mode 100644 index 0000000..b05d3e3 --- /dev/null +++ b/tests/ui/let-stable.rs @@ -0,0 +1,19 @@ +enum E { + Aaa, + Bbb(u8, u8), + Ccc(u8), + Ddd { u: u8 }, +} + +#[remain::check] +fn main() { + let value = E::Aaa; + + #[sorted] + let _ = match value { + E::Aaa => {} + E::Ccc(_) => {} + E::Ddd { u: _ } => {} + E::Bbb(_, _) => {} + }; +} diff --git a/tests/ui/let-stable.stderr b/tests/ui/let-stable.stderr new file mode 100644 index 0000000..9073750 --- /dev/null +++ b/tests/ui/let-stable.stderr @@ -0,0 +1,5 @@ +error: E::Bbb should sort before E::Ccc + --> $DIR/let-stable.rs:17:9 + | +17 | E::Bbb(_, _) => {} + | ^^^^^^ diff --git a/tests/ui/let-unstable.rs b/tests/ui/let-unstable.rs new file mode 100644 index 0000000..8d65c52 --- /dev/null +++ b/tests/ui/let-unstable.rs @@ -0,0 +1,22 @@ +#![feature(proc_macro_hygiene, stmt_expr_attributes)] + +use remain::sorted; + +enum E { + Aaa, + Bbb(u8, u8), + Ccc(u8), + Ddd { u: u8 }, +} + +fn main() { + let value = E::Aaa; + + #[sorted] + let _ = match value { + E::Aaa => {} + E::Ccc(_) => {} + E::Ddd { u: _ } => {} + E::Bbb(_, _) => {} + }; +} diff --git a/tests/ui/let-unstable.stderr b/tests/ui/let-unstable.stderr new file mode 100644 index 0000000..97c5ba1 --- /dev/null +++ b/tests/ui/let-unstable.stderr @@ -0,0 +1,5 @@ +error: E::Bbb should sort before E::Ccc + --> $DIR/let-unstable.rs:15:5 + | +15 | #[sorted] + | ^^^^^^^^^ diff --git a/tests/ui/match-stable.rs b/tests/ui/match-stable.rs new file mode 100644 index 0000000..6bdade4 --- /dev/null +++ b/tests/ui/match-stable.rs @@ -0,0 +1,19 @@ +enum E { + Aaa, + Bbb(u8, u8), + Ccc(u8), + Ddd { u: u8 }, +} + +#[remain::check] +fn main() { + let value = E::Aaa; + + #[sorted] + match value { + E::Aaa => {} + E::Ccc(_) => {} + E::Ddd { u: _ } => {} + E::Bbb(_, _) => {} + } +} diff --git a/tests/ui/match-stable.stderr b/tests/ui/match-stable.stderr new file mode 100644 index 0000000..183daa9 --- /dev/null +++ b/tests/ui/match-stable.stderr @@ -0,0 +1,5 @@ +error: E::Bbb should sort before E::Ccc + --> $DIR/match-stable.rs:17:9 + | +17 | E::Bbb(_, _) => {} + | ^^^^^^ diff --git a/tests/ui/match-unstable.rs b/tests/ui/match-unstable.rs new file mode 100644 index 0000000..db047f1 --- /dev/null +++ b/tests/ui/match-unstable.rs @@ -0,0 +1,22 @@ +#![feature(proc_macro_hygiene, stmt_expr_attributes)] + +use remain::sorted; + +enum E { + Aaa, + Bbb(u8, u8), + Ccc(u8), + Ddd { u: u8 }, +} + +fn main() { + let value = E::Aaa; + + #[sorted] + match value { + E::Aaa => {} + E::Ccc(_) => {} + E::Ddd { u: _ } => {} + E::Bbb(_, _) => {} + } +} diff --git a/tests/ui/match-unstable.stderr b/tests/ui/match-unstable.stderr new file mode 100644 index 0000000..b2bdea0 --- /dev/null +++ b/tests/ui/match-unstable.stderr @@ -0,0 +1,5 @@ +error: E::Bbb should sort before E::Ccc + --> $DIR/match-unstable.rs:15:5 + | +15 | #[sorted] + | ^^^^^^^^^ diff --git a/tests/ui/struct.rs b/tests/ui/struct.rs new file mode 100644 index 0000000..5f2cd41 --- /dev/null +++ b/tests/ui/struct.rs @@ -0,0 +1,11 @@ +use remain::sorted; + +#[sorted] +struct TestStruct { + d: usize, + c: usize, + a: usize, + b: usize, +} + +fn main() {} diff --git a/tests/ui/struct.stderr b/tests/ui/struct.stderr new file mode 100644 index 0000000..2ead15b --- /dev/null +++ b/tests/ui/struct.stderr @@ -0,0 +1,5 @@ +error: c should sort before d + --> $DIR/struct.rs:6:5 + | +6 | c: usize, + | ^ diff --git a/tests/ui/unnamed-fields.rs b/tests/ui/unnamed-fields.rs new file mode 100644 index 0000000..2fcfda9 --- /dev/null +++ b/tests/ui/unnamed-fields.rs @@ -0,0 +1,7 @@ +#[remain::sorted] +struct TupleStruct(usize, usize, usize); + +#[remain::sorted] +struct UnitStruct; + +fn main() {} diff --git a/tests/ui/unnamed-fields.stderr b/tests/ui/unnamed-fields.stderr new file mode 100644 index 0000000..b7e28f5 --- /dev/null +++ b/tests/ui/unnamed-fields.stderr @@ -0,0 +1,11 @@ +error: expected enum, struct, or match expression + --> $DIR/unnamed-fields.rs:1:1 + | +1 | #[remain::sorted] + | ^^^^^^^^^^^^^^^^^ + +error: expected enum, struct, or match expression + --> $DIR/unnamed-fields.rs:4:1 + | +4 | #[remain::sorted] + | ^^^^^^^^^^^^^^^^^ diff --git a/tests/ui/unsupported.rs b/tests/ui/unsupported.rs new file mode 100644 index 0000000..b11c974 --- /dev/null +++ b/tests/ui/unsupported.rs @@ -0,0 +1,10 @@ +#[remain::check] +fn main() { + let value = 0; + + #[sorted] + match value { + 0..=20 => {} + _ => {} + } +} diff --git a/tests/ui/unsupported.stderr b/tests/ui/unsupported.stderr new file mode 100644 index 0000000..e471e1e --- /dev/null +++ b/tests/ui/unsupported.stderr @@ -0,0 +1,5 @@ +error: unsupported by #[remain::sorted] + --> $DIR/unsupported.rs:7:9 + | +7 | 0..=20 => {} + | ^^^^^^ diff --git a/tests/unstable.rs b/tests/unstable.rs new file mode 100644 index 0000000..78dbd08 --- /dev/null +++ b/tests/unstable.rs @@ -0,0 +1,45 @@ +#![allow(dead_code)] +#![cfg(not(remain_stable_testing))] +#![feature(proc_macro_hygiene, stmt_expr_attributes)] + +#[remain::sorted] +pub enum TestEnum { + A, + B, + C, + D, +} + +#[remain::sorted] +pub struct TestStruct { + a: usize, + b: usize, + c: usize, + d: usize, +} + +#[test] +fn test_match() { + let value = TestEnum::A; + + #[remain::sorted] + let _ = match value { + TestEnum::A => {} + TestEnum::B => {} + TestEnum::C => {} + _ => {} + }; +} + +#[test] +fn test_let() { + let value = TestEnum::A; + + #[remain::sorted] + match value { + TestEnum::A => {} + TestEnum::B => {} + TestEnum::C => {} + _ => {} + } +} |