about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorMazdak Farrokhzad <twingoow@gmail.com>2019-05-03 16:24:56 +0200
committerGitHub <noreply@github.com>2019-05-03 16:24:56 +0200
commit9199bb5f81eae1f8ea47c9945fc3f4c4dded4989 (patch)
tree6bd237d38a7b5ec41ae7c3412e7bec8216814969 /src
parent06e1d88de6de15c34cd9a9fa34d7608bc346c6ec (diff)
parent201f14b88b19d43615845bfc2a6de9bc31985b13 (diff)
downloadrust-9199bb5f81eae1f8ea47c9945fc3f4c4dded4989.tar.gz
rust-9199bb5f81eae1f8ea47c9945fc3f4c4dded4989.zip
Rollup merge of #60373 - rasendubi:lang-features-sort-since, r=Centril
Tidy: ensure lang features are sorted by since

This is the tidy side of https://github.com/rust-lang/rust/issues/60361.

What is left is actually splitting features into groups and sorting by since.

This PR also likely to produce a small (a couple of lines) merge conflict with https://github.com/rust-lang/rust/pull/60362.

r? @Centril
Diffstat (limited to 'src')
-rw-r--r--src/libstd/sys/redox/ext/net.rs82
-rw-r--r--src/libsyntax/feature_gate.rs33
-rw-r--r--src/tools/tidy/Cargo.toml1
-rw-r--r--src/tools/tidy/src/features.rs128
-rw-r--r--src/tools/tidy/src/features/version.rs92
-rw-r--r--src/tools/tidy/src/lib.rs1
6 files changed, 255 insertions, 82 deletions
diff --git a/src/libstd/sys/redox/ext/net.rs b/src/libstd/sys/redox/ext/net.rs
index 096d0681959..b3ef5f3064c 100644
--- a/src/libstd/sys/redox/ext/net.rs
+++ b/src/libstd/sys/redox/ext/net.rs
@@ -1,4 +1,4 @@
-#![stable(feature = "unix_socket_redox", since = "1.29")]
+#![stable(feature = "unix_socket_redox", since = "1.29.0")]
 
 //! Unix-specific networking functionality
 
@@ -27,7 +27,7 @@ use crate::sys::{cvt, fd::FileDesc, syscall};
 /// let addr = socket.local_addr().expect("Couldn't get local address");
 /// ```
 #[derive(Clone)]
-#[stable(feature = "unix_socket_redox", since = "1.29")]
+#[stable(feature = "unix_socket_redox", since = "1.29.0")]
 pub struct SocketAddr(());
 
 impl SocketAddr {
@@ -55,7 +55,7 @@ impl SocketAddr {
     /// let addr = socket.local_addr().expect("Couldn't get local address");
     /// assert_eq!(addr.as_pathname(), None);
     /// ```
-    #[stable(feature = "unix_socket_redox", since = "1.29")]
+    #[stable(feature = "unix_socket_redox", since = "1.29.0")]
     pub fn as_pathname(&self) -> Option<&Path> {
         None
     }
@@ -83,12 +83,12 @@ impl SocketAddr {
     /// let addr = socket.local_addr().expect("Couldn't get local address");
     /// assert_eq!(addr.is_unnamed(), true);
     /// ```
-    #[stable(feature = "unix_socket_redox", since = "1.29")]
+    #[stable(feature = "unix_socket_redox", since = "1.29.0")]
     pub fn is_unnamed(&self) -> bool {
         false
     }
 }
-#[stable(feature = "unix_socket_redox", since = "1.29")]
+#[stable(feature = "unix_socket_redox", since = "1.29.0")]
 impl fmt::Debug for SocketAddr {
     fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
         write!(fmt, "SocketAddr")
@@ -109,10 +109,10 @@ impl fmt::Debug for SocketAddr {
 /// stream.read_to_string(&mut response).unwrap();
 /// println!("{}", response);
 /// ```
-#[stable(feature = "unix_socket_redox", since = "1.29")]
+#[stable(feature = "unix_socket_redox", since = "1.29.0")]
 pub struct UnixStream(FileDesc);
 
-#[stable(feature = "unix_socket_redox", since = "1.29")]
+#[stable(feature = "unix_socket_redox", since = "1.29.0")]
 impl fmt::Debug for UnixStream {
     fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
         let mut builder = fmt.debug_struct("UnixStream");
@@ -143,7 +143,7 @@ impl UnixStream {
     ///     }
     /// };
     /// ```
-    #[stable(feature = "unix_socket_redox", since = "1.29")]
+    #[stable(feature = "unix_socket_redox", since = "1.29.0")]
     pub fn connect<P: AsRef<Path>>(path: P) -> io::Result<UnixStream> {
         if let Some(s) = path.as_ref().to_str() {
             cvt(syscall::open(format!("chan:{}", s), syscall::O_CLOEXEC))
@@ -174,7 +174,7 @@ impl UnixStream {
     ///     }
     /// };
     /// ```
-    #[stable(feature = "unix_socket_redox", since = "1.29")]
+    #[stable(feature = "unix_socket_redox", since = "1.29.0")]
     pub fn pair() -> io::Result<(UnixStream, UnixStream)> {
         let server = cvt(syscall::open("chan:", syscall::O_CREAT | syscall::O_CLOEXEC))
             .map(FileDesc::new)?;
@@ -198,7 +198,7 @@ impl UnixStream {
     /// let socket = UnixStream::connect("/tmp/sock").unwrap();
     /// let sock_copy = socket.try_clone().expect("Couldn't clone socket");
     /// ```
-    #[stable(feature = "unix_socket_redox", since = "1.29")]
+    #[stable(feature = "unix_socket_redox", since = "1.29.0")]
     pub fn try_clone(&self) -> io::Result<UnixStream> {
         self.0.duplicate().map(UnixStream)
     }
@@ -213,7 +213,7 @@ impl UnixStream {
     /// let socket = UnixStream::connect("/tmp/sock").unwrap();
     /// let addr = socket.local_addr().expect("Couldn't get local address");
     /// ```
-    #[stable(feature = "unix_socket_redox", since = "1.29")]
+    #[stable(feature = "unix_socket_redox", since = "1.29.0")]
     pub fn local_addr(&self) -> io::Result<SocketAddr> {
         Err(Error::new(ErrorKind::Other, "UnixStream::local_addr unimplemented on redox"))
     }
@@ -228,7 +228,7 @@ impl UnixStream {
     /// let socket = UnixStream::connect("/tmp/sock").unwrap();
     /// let addr = socket.peer_addr().expect("Couldn't get peer address");
     /// ```
-    #[stable(feature = "unix_socket_redox", since = "1.29")]
+    #[stable(feature = "unix_socket_redox", since = "1.29.0")]
     pub fn peer_addr(&self) -> io::Result<SocketAddr> {
         Err(Error::new(ErrorKind::Other, "UnixStream::peer_addr unimplemented on redox"))
     }
@@ -267,7 +267,7 @@ impl UnixStream {
     /// let err = result.unwrap_err();
     /// assert_eq!(err.kind(), io::ErrorKind::InvalidInput)
     /// ```
-    #[stable(feature = "unix_socket_redox", since = "1.29")]
+    #[stable(feature = "unix_socket_redox", since = "1.29.0")]
     pub fn set_read_timeout(&self, _timeout: Option<Duration>) -> io::Result<()> {
         Err(Error::new(ErrorKind::Other, "UnixStream::set_read_timeout unimplemented on redox"))
     }
@@ -306,7 +306,7 @@ impl UnixStream {
     /// let err = result.unwrap_err();
     /// assert_eq!(err.kind(), io::ErrorKind::InvalidInput)
     /// ```
-    #[stable(feature = "unix_socket_redox", since = "1.29")]
+    #[stable(feature = "unix_socket_redox", since = "1.29.0")]
     pub fn set_write_timeout(&self, _timeout: Option<Duration>) -> io::Result<()> {
         Err(Error::new(ErrorKind::Other, "UnixStream::set_write_timeout unimplemented on redox"))
     }
@@ -323,7 +323,7 @@ impl UnixStream {
     /// socket.set_read_timeout(Some(Duration::new(1, 0))).expect("Couldn't set read timeout");
     /// assert_eq!(socket.read_timeout().unwrap(), Some(Duration::new(1, 0)));
     /// ```
-    #[stable(feature = "unix_socket_redox", since = "1.29")]
+    #[stable(feature = "unix_socket_redox", since = "1.29.0")]
     pub fn read_timeout(&self) -> io::Result<Option<Duration>> {
         Err(Error::new(ErrorKind::Other, "UnixStream::read_timeout unimplemented on redox"))
     }
@@ -340,7 +340,7 @@ impl UnixStream {
     /// socket.set_write_timeout(Some(Duration::new(1, 0))).expect("Couldn't set write timeout");
     /// assert_eq!(socket.write_timeout().unwrap(), Some(Duration::new(1, 0)));
     /// ```
-    #[stable(feature = "unix_socket_redox", since = "1.29")]
+    #[stable(feature = "unix_socket_redox", since = "1.29.0")]
     pub fn write_timeout(&self) -> io::Result<Option<Duration>> {
         Err(Error::new(ErrorKind::Other, "UnixStream::write_timeout unimplemented on redox"))
     }
@@ -355,7 +355,7 @@ impl UnixStream {
     /// let socket = UnixStream::connect("/tmp/sock").unwrap();
     /// socket.set_nonblocking(true).expect("Couldn't set nonblocking");
     /// ```
-    #[stable(feature = "unix_socket_redox", since = "1.29")]
+    #[stable(feature = "unix_socket_redox", since = "1.29.0")]
     pub fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> {
         self.0.set_nonblocking(nonblocking)
     }
@@ -375,7 +375,7 @@ impl UnixStream {
     ///
     /// # Platform specific
     /// On Redox this always returns `None`.
-    #[stable(feature = "unix_socket_redox", since = "1.29")]
+    #[stable(feature = "unix_socket_redox", since = "1.29.0")]
     pub fn take_error(&self) -> io::Result<Option<io::Error>> {
         Ok(None)
     }
@@ -397,13 +397,13 @@ impl UnixStream {
     /// let socket = UnixStream::connect("/tmp/sock").unwrap();
     /// socket.shutdown(Shutdown::Both).expect("shutdown function failed");
     /// ```
-    #[stable(feature = "unix_socket_redox", since = "1.29")]
+    #[stable(feature = "unix_socket_redox", since = "1.29.0")]
     pub fn shutdown(&self, _how: Shutdown) -> io::Result<()> {
         Err(Error::new(ErrorKind::Other, "UnixStream::shutdown unimplemented on redox"))
     }
 }
 
-#[stable(feature = "unix_socket_redox", since = "1.29")]
+#[stable(feature = "unix_socket_redox", since = "1.29.0")]
 impl io::Read for UnixStream {
     fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
         io::Read::read(&mut &*self, buf)
@@ -415,7 +415,7 @@ impl io::Read for UnixStream {
     }
 }
 
-#[stable(feature = "unix_socket_redox", since = "1.29")]
+#[stable(feature = "unix_socket_redox", since = "1.29.0")]
 impl<'a> io::Read for &'a UnixStream {
     fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
         self.0.read(buf)
@@ -427,7 +427,7 @@ impl<'a> io::Read for &'a UnixStream {
     }
 }
 
-#[stable(feature = "unix_socket_redox", since = "1.29")]
+#[stable(feature = "unix_socket_redox", since = "1.29.0")]
 impl io::Write for UnixStream {
     fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
         io::Write::write(&mut &*self, buf)
@@ -438,7 +438,7 @@ impl io::Write for UnixStream {
     }
 }
 
-#[stable(feature = "unix_socket_redox", since = "1.29")]
+#[stable(feature = "unix_socket_redox", since = "1.29.0")]
 impl<'a> io::Write for &'a UnixStream {
     fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
         self.0.write(buf)
@@ -449,21 +449,21 @@ impl<'a> io::Write for &'a UnixStream {
     }
 }
 
-#[stable(feature = "unix_socket_redox", since = "1.29")]
+#[stable(feature = "unix_socket_redox", since = "1.29.0")]
 impl AsRawFd for UnixStream {
     fn as_raw_fd(&self) -> RawFd {
         self.0.raw()
     }
 }
 
-#[stable(feature = "unix_socket_redox", since = "1.29")]
+#[stable(feature = "unix_socket_redox", since = "1.29.0")]
 impl FromRawFd for UnixStream {
     unsafe fn from_raw_fd(fd: RawFd) -> UnixStream {
         UnixStream(FileDesc::new(fd))
     }
 }
 
-#[stable(feature = "unix_socket_redox", since = "1.29")]
+#[stable(feature = "unix_socket_redox", since = "1.29.0")]
 impl IntoRawFd for UnixStream {
     fn into_raw_fd(self) -> RawFd {
         self.0.into_raw()
@@ -498,10 +498,10 @@ impl IntoRawFd for UnixStream {
 ///     }
 /// }
 /// ```
-#[stable(feature = "unix_socket_redox", since = "1.29")]
+#[stable(feature = "unix_socket_redox", since = "1.29.0")]
 pub struct UnixListener(FileDesc);
 
-#[stable(feature = "unix_socket_redox", since = "1.29")]
+#[stable(feature = "unix_socket_redox", since = "1.29.0")]
 impl fmt::Debug for UnixListener {
     fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
         let mut builder = fmt.debug_struct("UnixListener");
@@ -529,7 +529,7 @@ impl UnixListener {
     ///     }
     /// };
     /// ```
-    #[stable(feature = "unix_socket_redox", since = "1.29")]
+    #[stable(feature = "unix_socket_redox", since = "1.29.0")]
     pub fn bind<P: AsRef<Path>>(path: P) -> io::Result<UnixListener> {
         if let Some(s) = path.as_ref().to_str() {
             cvt(syscall::open(format!("chan:{}", s), syscall::O_CREAT | syscall::O_CLOEXEC))
@@ -563,7 +563,7 @@ impl UnixListener {
     ///     Err(e) => println!("accept function failed: {:?}", e),
     /// }
     /// ```
-    #[stable(feature = "unix_socket_redox", since = "1.29")]
+    #[stable(feature = "unix_socket_redox", since = "1.29.0")]
     pub fn accept(&self) -> io::Result<(UnixStream, SocketAddr)> {
         self.0.duplicate_path(b"listen").map(|fd| (UnixStream(fd), SocketAddr(())))
     }
@@ -583,7 +583,7 @@ impl UnixListener {
     ///
     /// let listener_copy = listener.try_clone().expect("try_clone failed");
     /// ```
-    #[stable(feature = "unix_socket_redox", since = "1.29")]
+    #[stable(feature = "unix_socket_redox", since = "1.29.0")]
     pub fn try_clone(&self) -> io::Result<UnixListener> {
         self.0.duplicate().map(UnixListener)
     }
@@ -599,7 +599,7 @@ impl UnixListener {
     ///
     /// let addr = listener.local_addr().expect("Couldn't get local address");
     /// ```
-    #[stable(feature = "unix_socket_redox", since = "1.29")]
+    #[stable(feature = "unix_socket_redox", since = "1.29.0")]
     pub fn local_addr(&self) -> io::Result<SocketAddr> {
         Err(Error::new(ErrorKind::Other, "UnixListener::local_addr unimplemented on redox"))
     }
@@ -615,7 +615,7 @@ impl UnixListener {
     ///
     /// listener.set_nonblocking(true).expect("Couldn't set non blocking");
     /// ```
-    #[stable(feature = "unix_socket_redox", since = "1.29")]
+    #[stable(feature = "unix_socket_redox", since = "1.29.0")]
     pub fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> {
         self.0.set_nonblocking(nonblocking)
     }
@@ -636,7 +636,7 @@ impl UnixListener {
     ///
     /// # Platform specific
     /// On Redox this always returns `None`.
-    #[stable(feature = "unix_socket_redox", since = "1.29")]
+    #[stable(feature = "unix_socket_redox", since = "1.29.0")]
     pub fn take_error(&self) -> io::Result<Option<io::Error>> {
         Ok(None)
     }
@@ -672,34 +672,34 @@ impl UnixListener {
     ///     }
     /// }
     /// ```
-    #[stable(feature = "unix_socket_redox", since = "1.29")]
+    #[stable(feature = "unix_socket_redox", since = "1.29.0")]
     pub fn incoming<'a>(&'a self) -> Incoming<'a> {
         Incoming { listener: self }
     }
 }
 
-#[stable(feature = "unix_socket_redox", since = "1.29")]
+#[stable(feature = "unix_socket_redox", since = "1.29.0")]
 impl AsRawFd for UnixListener {
     fn as_raw_fd(&self) -> RawFd {
         self.0.raw()
     }
 }
 
-#[stable(feature = "unix_socket_redox", since = "1.29")]
+#[stable(feature = "unix_socket_redox", since = "1.29.0")]
 impl FromRawFd for UnixListener {
     unsafe fn from_raw_fd(fd: RawFd) -> UnixListener {
         UnixListener(FileDesc::new(fd))
     }
 }
 
-#[stable(feature = "unix_socket_redox", since = "1.29")]
+#[stable(feature = "unix_socket_redox", since = "1.29.0")]
 impl IntoRawFd for UnixListener {
     fn into_raw_fd(self) -> RawFd {
         self.0.into_raw()
     }
 }
 
-#[stable(feature = "unix_socket_redox", since = "1.29")]
+#[stable(feature = "unix_socket_redox", since = "1.29.0")]
 impl<'a> IntoIterator for &'a UnixListener {
     type Item = io::Result<UnixStream>;
     type IntoIter = Incoming<'a>;
@@ -740,12 +740,12 @@ impl<'a> IntoIterator for &'a UnixListener {
 /// }
 /// ```
 #[derive(Debug)]
-#[stable(feature = "unix_socket_redox", since = "1.29")]
+#[stable(feature = "unix_socket_redox", since = "1.29.0")]
 pub struct Incoming<'a> {
     listener: &'a UnixListener,
 }
 
-#[stable(feature = "unix_socket_redox", since = "1.29")]
+#[stable(feature = "unix_socket_redox", since = "1.29.0")]
 impl<'a> Iterator for Incoming<'a> {
     type Item = io::Result<UnixStream>;
 
diff --git a/src/libsyntax/feature_gate.rs b/src/libsyntax/feature_gate.rs
index 012fcbdd8c8..2a1f3c48014 100644
--- a/src/libsyntax/feature_gate.rs
+++ b/src/libsyntax/feature_gate.rs
@@ -109,15 +109,14 @@ macro_rules! declare_features {
 // stable (active).
 //
 // Note that the features should be grouped into internal/user-facing
-// and then sorted by version inside those groups.
-// FIXME(60361): Enforce ^-- with tidy.
+// and then sorted by version inside those groups. This is inforced with tidy.
 //
 // N.B., `tools/tidy/src/features.rs` parses this information directly out of the
 // source, so take care when modifying it.
 
 declare_features! (
     // -------------------------------------------------------------------------
-    // Internal feature gates.
+    // feature-group-start: internal feature gates
     // -------------------------------------------------------------------------
 
     // no tracking issue START
@@ -211,12 +210,12 @@ declare_features! (
 
     // no tracking issue END
 
-    // Allows using the `may_dangle` attribute (RFC 1327).
-    (active, dropck_eyepatch, "1.10.0", Some(34761), None),
-
     // Allows using `#[structural_match]` which indicates that a type is structurally matchable.
     (active, structural_match, "1.8.0", Some(31434), None),
 
+    // Allows using the `may_dangle` attribute (RFC 1327).
+    (active, dropck_eyepatch, "1.10.0", Some(34761), None),
+
     // Allows using the `#![panic_runtime]` attribute.
     (active, panic_runtime, "1.10.0", Some(32837), None),
 
@@ -252,7 +251,11 @@ declare_features! (
     (active, test_2018_feature, "1.31.0", Some(0), Some(Edition::Edition2018)),
 
     // -------------------------------------------------------------------------
-    // Actual feature gates (target features).
+    // feature-group-end: internal feature gates
+    // -------------------------------------------------------------------------
+
+    // -------------------------------------------------------------------------
+    // feature-group-start: actual feature gates (target features)
     // -------------------------------------------------------------------------
 
     // FIXME: Document these and merge with the list below.
@@ -275,7 +278,11 @@ declare_features! (
     (active, f16c_target_feature, "1.36.0", Some(44839), None),
 
     // -------------------------------------------------------------------------
-    // Actual feature gates.
+    // feature-group-end: actual feature gates (target features)
+    // -------------------------------------------------------------------------
+
+    // -------------------------------------------------------------------------
+    // feature-group-start: actual feature gates
     // -------------------------------------------------------------------------
 
     // Allows using `asm!` macro with which inline assembly can be embedded.
@@ -340,9 +347,6 @@ declare_features! (
     // Permits specifying whether a function should permit unwinding or abort on unwind.
     (active, unwind_attributes, "1.4.0", Some(58760), None),
 
-    // Allows using `#[naked]` on functions.
-    (active, naked_functions, "1.9.0", Some(32408), None),
-
     // Allows `#[no_debug]`.
     (active, no_debug, "1.5.0", Some(29721), None),
 
@@ -358,6 +362,9 @@ declare_features! (
     // Allows specialization of implementations (RFC 1210).
     (active, specialization, "1.7.0", Some(31844), None),
 
+    // Allows using `#[naked]` on functions.
+    (active, naked_functions, "1.9.0", Some(32408), None),
+
     // Allows `cfg(target_has_atomic = "...")`.
     (active, cfg_target_has_atomic, "1.9.0", Some(32976), None),
 
@@ -545,6 +552,10 @@ declare_features! (
 
     // Allows using C-variadics.
     (active, c_variadic, "1.34.0", Some(44930), None),
+
+    // -------------------------------------------------------------------------
+    // feature-group-end: actual feature gates
+    // -------------------------------------------------------------------------
 );
 
 // Some features are known to be incomplete and using them is likely to have
diff --git a/src/tools/tidy/Cargo.toml b/src/tools/tidy/Cargo.toml
index f7b491823f8..f5db2487618 100644
--- a/src/tools/tidy/Cargo.toml
+++ b/src/tools/tidy/Cargo.toml
@@ -4,6 +4,7 @@ version = "0.1.0"
 authors = ["Alex Crichton <alex@alexcrichton.com>"]
 
 [dependencies]
+regex = "1"
 serde = "1.0.8"
 serde_derive = "1.0.8"
 serde_json = "1.0.2"
diff --git a/src/tools/tidy/src/features.rs b/src/tools/tidy/src/features.rs
index 8239fd9dce0..3144df6dd4c 100644
--- a/src/tools/tidy/src/features.rs
+++ b/src/tools/tidy/src/features.rs
@@ -7,6 +7,7 @@
 //! * Library features have at most one stability level.
 //! * Library features have at most one `since` value.
 //! * All unstable lang features have tests to ensure they are actually unstable.
+//! * Language features in a group are sorted by `since` value.
 
 use std::collections::HashMap;
 use std::fmt;
@@ -14,6 +15,14 @@ use std::fs::{self, File};
 use std::io::prelude::*;
 use std::path::Path;
 
+use regex::{Regex, escape};
+
+mod version;
+use self::version::Version;
+
+const FEATURE_GROUP_START_PREFIX: &str = "// feature-group-start";
+const FEATURE_GROUP_END_PREFIX: &str = "// feature-group-end";
+
 #[derive(Debug, PartialEq, Clone)]
 pub enum Status {
     Stable,
@@ -35,7 +44,7 @@ impl fmt::Display for Status {
 #[derive(Debug, Clone)]
 pub struct Feature {
     pub level: Status,
-    pub since: String,
+    pub since: Option<Version>,
     pub has_gate_test: bool,
     pub tracking_issue: Option<u32>,
 }
@@ -129,20 +138,8 @@ pub fn check(path: &Path, bad: &mut bool, quiet: bool) {
     }
 
     let mut lines = Vec::new();
-    for (name, feature) in features.iter() {
-        lines.push(format!("{:<32} {:<8} {:<12} {:<8}",
-                           name,
-                           "lang",
-                           feature.level,
-                           feature.since));
-    }
-    for (name, feature) in lib_features {
-        lines.push(format!("{:<32} {:<8} {:<12} {:<8}",
-                           name,
-                           "lib",
-                           feature.level,
-                           feature.since));
-    }
+    lines.extend(format_features(&features, "lang"));
+    lines.extend(format_features(&lib_features, "lib"));
 
     lines.sort();
     for line in lines {
@@ -150,11 +147,31 @@ pub fn check(path: &Path, bad: &mut bool, quiet: bool) {
     }
 }
 
+fn format_features<'a>(features: &'a Features, family: &'a str) -> impl Iterator<Item = String> + 'a {
+    features.iter().map(move |(name, feature)| {
+        format!("{:<32} {:<8} {:<12} {:<8}",
+                name,
+                family,
+                feature.level,
+                feature.since.map_or("None".to_owned(),
+                                     |since| since.to_string()))
+    })
+}
+
 fn find_attr_val<'a>(line: &'a str, attr: &str) -> Option<&'a str> {
-    line.find(attr)
-        .and_then(|i| line[i..].find('"').map(|j| i + j + 1))
-        .and_then(|i| line[i..].find('"').map(|j| (i, i + j)))
-        .map(|(i, j)| &line[i..j])
+    let r = Regex::new(&format!(r#"{}\s*=\s*"([^"]*)""#, escape(attr)))
+        .expect("malformed regex for find_attr_val");
+    r.captures(line)
+        .and_then(|c| c.get(1))
+        .map(|m| m.as_str())
+}
+
+#[test]
+fn test_find_attr_val() {
+    let s = r#"#[unstable(feature = "checked_duration_since", issue = "58402")]"#;
+    assert_eq!(find_attr_val(s, "feature"), Some("checked_duration_since"));
+    assert_eq!(find_attr_val(s, "issue"), Some("58402"));
+    assert_eq!(find_attr_val(s, "since"), None);
 }
 
 fn test_filen_gate(filen_underscore: &str, features: &mut Features) -> bool {
@@ -177,6 +194,9 @@ pub fn collect_lang_features(base_src_path: &Path, bad: &mut bool) -> Features {
     // without one inside `// no tracking issue START` and `// no tracking issue END`.
     let mut next_feature_omits_tracking_issue = false;
 
+    let mut in_feature_group = false;
+    let mut prev_since = None;
+
     contents.lines().zip(1..)
         .filter_map(|(line, line_number)| {
             let line = line.trim();
@@ -194,6 +214,25 @@ pub fn collect_lang_features(base_src_path: &Path, bad: &mut bool) -> Features {
                 _ => {}
             }
 
+            if line.starts_with(FEATURE_GROUP_START_PREFIX) {
+                if in_feature_group {
+                    tidy_error!(
+                        bad,
+                        // ignore-tidy-linelength
+                        "libsyntax/feature_gate.rs:{}: new feature group is started without ending the previous one",
+                        line_number,
+                    );
+                }
+
+                in_feature_group = true;
+                prev_since = None;
+                return None;
+            } else if line.starts_with(FEATURE_GROUP_END_PREFIX) {
+                in_feature_group = false;
+                prev_since = None;
+                return None;
+            }
+
             let mut parts = line.split(',');
             let level = match parts.next().map(|l| l.trim().trim_start_matches('(')) {
                 Some("active") => Status::Unstable,
@@ -202,7 +241,33 @@ pub fn collect_lang_features(base_src_path: &Path, bad: &mut bool) -> Features {
                 _ => return None,
             };
             let name = parts.next().unwrap().trim();
-            let since = parts.next().unwrap().trim().trim_matches('"');
+
+            let since_str = parts.next().unwrap().trim().trim_matches('"');
+            let since = match since_str.parse() {
+                Ok(since) => Some(since),
+                Err(err) => {
+                    tidy_error!(
+                        bad,
+                        "libsyntax/feature_gate.rs:{}: failed to parse since: {} ({:?})",
+                        line_number,
+                        since_str,
+                        err,
+                    );
+                    None
+                }
+            };
+            if in_feature_group {
+                if prev_since > since {
+                    tidy_error!(
+                        bad,
+                        "libsyntax/feature_gate.rs:{}: feature {} is not sorted by since",
+                        line_number,
+                        name,
+                    );
+                }
+                prev_since = since;
+            }
+
             let issue_str = parts.next().unwrap().trim();
             let tracking_issue = if issue_str.starts_with("None") {
                 if level == Status::Unstable && !next_feature_omits_tracking_issue {
@@ -222,7 +287,7 @@ pub fn collect_lang_features(base_src_path: &Path, bad: &mut bool) -> Features {
             Some((name.to_owned(),
                 Feature {
                     level,
-                    since: since.to_owned(),
+                    since,
                     has_gate_test: false,
                     tracking_issue,
                 }))
@@ -239,7 +304,7 @@ pub fn collect_lib_features(base_src_path: &Path) -> Features {
     // add it to the set of known library features so we can still generate docs.
     lib_features.insert("compiler_builtins_lib".to_owned(), Feature {
         level: Status::Unstable,
-        since: String::new(),
+        since: None,
         has_gate_test: false,
         tracking_issue: None,
     });
@@ -336,11 +401,11 @@ fn map_lib_features(base_src_path: &Path,
                 // `const fn` features are handled specially.
                 let feature_name = match find_attr_val(line, "feature") {
                     Some(name) => name,
-                    None => err!("malformed stability attribute"),
+                    None => err!("malformed stability attribute: missing `feature` key"),
                 };
                 let feature = Feature {
                     level: Status::Unstable,
-                    since: "None".to_owned(),
+                    since: None,
                     has_gate_test: false,
                     // FIXME(#57563): #57563 is now used as a common tracking issue,
                     // although we would like to have specific tracking issues for each
@@ -359,20 +424,23 @@ fn map_lib_features(base_src_path: &Path,
             };
             let feature_name = match find_attr_val(line, "feature") {
                 Some(name) => name,
-                None => err!("malformed stability attribute"),
+                None => err!("malformed stability attribute: missing `feature` key"),
             };
-            let since = match find_attr_val(line, "since") {
-                Some(name) => name,
+            let since = match find_attr_val(line, "since").map(|x| x.parse()) {
+                Some(Ok(since)) => Some(since),
+                Some(Err(_err)) => {
+                    err!("malformed stability attribute: can't parse `since` key");
+                },
                 None if level == Status::Stable => {
-                    err!("malformed stability attribute");
+                    err!("malformed stability attribute: missing the `since` key");
                 }
-                None => "None",
+                None => None,
             };
             let tracking_issue = find_attr_val(line, "issue").map(|s| s.parse().unwrap());
 
             let feature = Feature {
                 level,
-                since: since.to_owned(),
+                since,
                 has_gate_test: false,
                 tracking_issue,
             };
diff --git a/src/tools/tidy/src/features/version.rs b/src/tools/tidy/src/features/version.rs
new file mode 100644
index 00000000000..6027e7d35e2
--- /dev/null
+++ b/src/tools/tidy/src/features/version.rs
@@ -0,0 +1,92 @@
+use std::str::FromStr;
+use std::num::ParseIntError;
+use std::fmt;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub struct Version {
+    parts: [u32; 3],
+}
+
+impl fmt::Display for Version {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.pad(&format!("{}.{}.{}", self.parts[0], self.parts[1], self.parts[2]))
+    }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum ParseVersionError {
+    ParseIntError(ParseIntError),
+    WrongNumberOfParts,
+}
+
+impl From<ParseIntError> for ParseVersionError {
+    fn from(err: ParseIntError) -> Self {
+        ParseVersionError::ParseIntError(err)
+    }
+}
+
+impl FromStr for Version {
+    type Err = ParseVersionError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let mut iter = s.split('.').map(|part| Ok(part.parse()?));
+
+        let parts = {
+            let mut part = || {
+                iter.next()
+                    .unwrap_or(Err(ParseVersionError::WrongNumberOfParts))
+            };
+
+            [part()?, part()?, part()?]
+        };
+
+        if let Some(_) = iter.next() {
+            // Ensure we don't have more than 3 parts.
+            return Err(ParseVersionError::WrongNumberOfParts);
+        }
+
+        Ok(Self { parts })
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::Version;
+
+    #[test]
+    fn test_try_from_invalid_version() {
+        assert!("".parse::<Version>().is_err());
+        assert!("hello".parse::<Version>().is_err());
+        assert!("1.32.hi".parse::<Version>().is_err());
+        assert!("1.32..1".parse::<Version>().is_err());
+        assert!("1.32".parse::<Version>().is_err());
+        assert!("1.32.0.1".parse::<Version>().is_err());
+    }
+
+    #[test]
+    fn test_try_from_single() {
+        assert_eq!("1.32.0".parse(), Ok(Version { parts: [1, 32, 0] }));
+        assert_eq!("1.0.0".parse(), Ok(Version { parts: [1, 0, 0] }));
+    }
+
+    #[test]
+    fn test_compare() {
+        let v_1_0_0 = "1.0.0".parse::<Version>().unwrap();
+        let v_1_32_0 = "1.32.0".parse::<Version>().unwrap();
+        let v_1_32_1 = "1.32.1".parse::<Version>().unwrap();
+        assert!(v_1_0_0 < v_1_32_1);
+        assert!(v_1_0_0 < v_1_32_0);
+        assert!(v_1_32_0 < v_1_32_1);
+    }
+
+    #[test]
+    fn test_to_string() {
+        let v_1_0_0 = "1.0.0".parse::<Version>().unwrap();
+        let v_1_32_1 = "1.32.1".parse::<Version>().unwrap();
+
+        assert_eq!(v_1_0_0.to_string(), "1.0.0");
+        assert_eq!(v_1_32_1.to_string(), "1.32.1");
+        assert_eq!(format!("{:<8}", v_1_32_1), "1.32.1  ");
+        assert_eq!(format!("{:>8}", v_1_32_1), "  1.32.1");
+    }
+}
diff --git a/src/tools/tidy/src/lib.rs b/src/tools/tidy/src/lib.rs
index c4a1246ffdf..30080452edc 100644
--- a/src/tools/tidy/src/lib.rs
+++ b/src/tools/tidy/src/lib.rs
@@ -5,6 +5,7 @@
 
 #![deny(rust_2018_idioms)]
 
+extern crate regex;
 extern crate serde_json;
 #[macro_use]
 extern crate serde_derive;