Merge pull request #2 from jeremyBanks/structs

Add support for sorting named struct fields.
diff --git a/Cargo.toml b/Cargo.toml
index fc097ab..a928b27 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,7 +4,7 @@
 authors = ["David Tolnay <dtolnay@gmail.com>"]
 edition = "2018"
 license = "MIT OR Apache-2.0"
-description = "Compile-time checks that an enum or match is written in sorted order."
+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"
diff --git a/README.md b/README.md
index fe1851a..3af2e72 100644
--- a/README.md
+++ b/README.md
@@ -16,8 +16,8 @@
 
 ## Syntax
 
-Place a `#[remain::sorted]` attribute on enums, on match-expressions, or on
-let-statements whose value is a match-expression.
+Place a `#[remain::sorted]` attribute on enums, on named structs, on
+match-expressions, or on let-statements whose value is a match-expression.
 
 Alternatively, import as `use remain::sorted;` and use `#[sorted]` as the
 attribute.
@@ -37,6 +37,16 @@
     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::*;
@@ -57,7 +67,7 @@
 }
 ```
 
-If an enum variant or match arm is inserted out of order,
+If an enum variant, struct field, or match arm is inserted out of order,
 
 ```diff
       NetDeviceNew(virtio::NetError),
@@ -78,7 +88,7 @@
 
 ## Compiler support
 
-The attribute on enums is supported on any rustc version 1.31+.
+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
diff --git a/src/check.rs b/src/check.rs
index 281c052..1b50442 100644
--- a/src/check.rs
+++ b/src/check.rs
@@ -1,5 +1,5 @@
 use syn::{Arm, Ident, Result, Variant};
-use syn::{Error, Pat, PatIdent};
+use syn::{Error, Field, Fields, Pat, PatIdent};
 
 use crate::compare::Path;
 use crate::format;
@@ -8,6 +8,13 @@
 pub fn sorted(input: Input) -> Result<()> {
     let paths = match input {
         Enum(item) => collect_paths(item.variants)?,
+        Struct(item) => {
+            if let Fields::Named(fields) = item.fields {
+                collect_paths(fields.named)?
+            } else {
+                unreachable!("must be named field")
+            }
+        }
         Match(expr) | Let(expr) => collect_paths(expr.arms)?,
     };
 
@@ -44,6 +51,14 @@
     }
 }
 
+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.
diff --git a/src/emit.rs b/src/emit.rs
index b98ad1d..d1ddda8 100644
--- a/src/emit.rs
+++ b/src/emit.rs
@@ -7,6 +7,7 @@
 pub enum Kind {
     Enum,
     Match,
+    Struct,
     Let,
 }
 
@@ -21,7 +22,7 @@
     let original = proc_macro2::TokenStream::from(original);
 
     let expanded = match kind {
-        Kind::Enum | Kind::Let => quote!(#err #original),
+        Kind::Enum | Kind::Let | Kind::Struct => quote!(#err #original),
         Kind::Match => quote!({ #err #original }),
     };
 
@@ -32,7 +33,7 @@
 // https://github.com/rust-lang/rust/issues/43081
 fn probably_has_spans(kind: Kind) -> bool {
     match kind {
-        Kind::Enum => true,
+        Kind::Enum | Kind::Struct => true,
         Kind::Match | Kind::Let => false,
     }
 }
diff --git a/src/lib.rs b/src/lib.rs
index dc1ab0b..4034303 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -4,8 +4,8 @@
 //!
 //! # Syntax
 //!
-//! Place a `#[remain::sorted]` attribute on enums, on match-expressions, or on
-//! let-statements whose value is a match-expression.
+//! Place a `#[remain::sorted]` attribute on enums, on named structs, on
+//! match-expressions, or on let-statements whose value is a match-expression.
 //!
 //! Alternatively, import as `use remain::sorted;` and use `#[sorted]` as the
 //! attribute.
@@ -76,7 +76,7 @@
 //! # fn main() {}
 //! ```
 //!
-//! If an enum variant or match arm is inserted out of order,
+//! If an enum variant, struct field, or match arm is inserted out of order,\
 //!
 //! ```diff
 //!       NetDeviceNew(virtio::NetError),
diff --git a/src/parse.rs b/src/parse.rs
index 77fd4bc..98b8c86 100644
--- a/src/parse.rs
+++ b/src/parse.rs
@@ -1,6 +1,6 @@
 use proc_macro2::Span;
 use syn::parse::{Parse, ParseStream};
-use syn::{Attribute, Error, Expr, Result, Stmt, Token, Visibility};
+use syn::{Attribute, Error, Expr, Fields, Result, Stmt, Token, Visibility};
 
 use crate::emit::Kind;
 
@@ -15,6 +15,7 @@
 pub enum Input {
     Enum(syn::ItemEnum),
     Match(syn::ExprMatch),
+    Struct(syn::ItemStruct),
     Let(syn::ExprMatch),
 }
 
@@ -23,6 +24,7 @@
         match self {
             Input::Enum(_) => Kind::Enum,
             Input::Match(_) => Kind::Match,
+            Input::Struct(_) => Kind::Struct,
             Input::Let(_) => Kind::Let,
         }
     }
@@ -61,6 +63,14 @@
         if ahead.peek(Token![enum]) {
             return input.parse().map(Input::Enum);
         }
+        if ahead.peek(Token![struct]) {
+            let input = input.parse().map(Input::Struct)?;
+            if let Input::Struct(ref item) = input {
+                if let Fields::Named(_) = item.fields {
+                    return Ok(input);
+                }
+            }
+        }
 
         Err(unexpected())
     }
@@ -68,6 +78,6 @@
 
 fn unexpected() -> Error {
     let span = Span::call_site();
-    let msg = "expected enum or match expression";
+    let msg = "expected enum, named struct, or match expression";
     Error::new(span, msg)
 }
diff --git a/tests/stable.rs b/tests/stable.rs
index 1bf49f4..a3b6a7b 100644
--- a/tests/stable.rs
+++ b/tests/stable.rs
@@ -1,3 +1,5 @@
+#![allow(dead_code)]
+
 #[remain::sorted]
 pub enum TestEnum {
     A,
@@ -6,6 +8,14 @@
     D,
 }
 
+#[remain::sorted]
+pub struct TestStruct {
+    a: usize,
+    b: usize,
+    c: usize,
+    d: usize,
+}
+
 #[test]
 #[remain::check]
 fn test_match() {
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/unsupported.rs b/tests/ui/unsupported.rs
index b11c974..dbda20c 100644
--- a/tests/ui/unsupported.rs
+++ b/tests/ui/unsupported.rs
@@ -8,3 +8,9 @@
         _ => {}
     }
 }
+
+#[remain::sorted]
+struct TestUnnamedStruct(usize, usize, usize, usize);
+
+#[remain::sorted]
+struct TestUnitStruct;
diff --git a/tests/ui/unsupported.stderr b/tests/ui/unsupported.stderr
index e471e1e..2e0d70f 100644
--- a/tests/ui/unsupported.stderr
+++ b/tests/ui/unsupported.stderr
@@ -3,3 +3,15 @@
   |
 7 |         0..=20 => {}
   |         ^^^^^^
+
+error: expected enum, named struct, or match expression
+  --> $DIR/unsupported.rs:12:1
+   |
+12 | #[remain::sorted]
+   | ^^^^^^^^^^^^^^^^^
+
+error: expected enum, named struct, or match expression
+  --> $DIR/unsupported.rs:15:1
+   |
+15 | #[remain::sorted]
+   | ^^^^^^^^^^^^^^^^^
diff --git a/tests/unstable.rs b/tests/unstable.rs
index 827c59b..78dbd08 100644
--- a/tests/unstable.rs
+++ b/tests/unstable.rs
@@ -1,3 +1,4 @@
+#![allow(dead_code)]
 #![cfg(not(remain_stable_testing))]
 #![feature(proc_macro_hygiene, stmt_expr_attributes)]
 
@@ -9,6 +10,14 @@
     D,
 }
 
+#[remain::sorted]
+pub struct TestStruct {
+    a: usize,
+    b: usize,
+    c: usize,
+    d: usize,
+}
+
 #[test]
 fn test_match() {
     let value = TestEnum::A;