From 3a994439dcb7fc496cb7be9c9e416c6c82c1e7ce Mon Sep 17 00:00:00 2001 From: api6590 Date: Thu, 12 Dec 2024 18:43:05 -0500 Subject: [PATCH 1/5] feat: Add FutureDate lint for last-call-deadline validation This commit adds a new lint that validates that the last-call-deadline field in EIP preambles is set to a future date when the EIP status is 'Last Call'. The lint: - Only checks if status is 'Last Call' - Compares the deadline date with today's date - Reports an error if the deadline is not in the future - Includes helpful error messages This helps ensure that Last Call deadlines are always set to future dates, making the EIP process more robust. --- Cargo.lock | 56 +++++++++++++ eipw-lint/Cargo.toml | 2 +- eipw-lint/src/lints/known_lints.rs | 8 ++ eipw-lint/src/lints/preamble.rs | 2 + eipw-lint/src/lints/preamble/future_date.rs | 82 ++++++++++++++++++++ eipw-lint/tests/lint_preamble_future_date.rs | 77 ++++++++++++++++++ 6 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 eipw-lint/src/lints/preamble/future_date.rs create mode 100644 eipw-lint/tests/lint_preamble_future_date.rs diff --git a/Cargo.lock b/Cargo.lock index 924ec694..4900536a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,6 +40,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "annotate-snippets" version = "0.11.4" @@ -214,7 +229,10 @@ version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ + "android-tzdata", + "iana-time-zone", "num-traits", + "windows-targets", ] [[package]] @@ -290,6 +308,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cssparser" version = "0.31.2" @@ -661,6 +685,29 @@ dependencies = [ "syn", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -1795,6 +1842,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/eipw-lint/Cargo.toml b/eipw-lint/Cargo.toml index 0bbe2622..8c17fafc 100644 --- a/eipw-lint/Cargo.toml +++ b/eipw-lint/Cargo.toml @@ -18,7 +18,7 @@ regex = "1.11.0" serde_json = "1.0.128" serde = { version = "1.0.164", features = [ "derive" ] } url = "2.5.2" -chrono = { version = "0.4.38", default-features = false } +chrono = { version = "0.4.38", default-features = false, features = ["clock"] } educe = { version = "0.6.0", default-features = false, features = [ "Debug" ] } tokio = { optional = true, version = "1.40.0", features = [ "macros" ] } scraper = { version = "0.20.0", default-features = false } diff --git a/eipw-lint/src/lints/known_lints.rs b/eipw-lint/src/lints/known_lints.rs index f51a0728..72e8fae3 100644 --- a/eipw-lint/src/lints/known_lints.rs +++ b/eipw-lint/src/lints/known_lints.rs @@ -22,6 +22,9 @@ pub enum DefaultLint { PreambleDate { name: preamble::Date, }, + PreambleFutureDate { + name: preamble::FutureDate, + }, PreambleFileName(preamble::FileName), PreambleLength(preamble::Length), PreambleList { @@ -87,6 +90,7 @@ where match self { Self::PreambleAuthor { name } => Box::new(name), Self::PreambleDate { name } => Box::new(name), + Self::PreambleFutureDate { name } => Box::new(name), Self::PreambleFileName(l) => Box::new(l), Self::PreambleLength(l) => Box::new(l), Self::PreambleList { name } => Box::new(name), @@ -128,6 +132,7 @@ where match self { Self::PreambleAuthor { name } => name, Self::PreambleDate { name } => name, + Self::PreambleFutureDate { name } => name, Self::PreambleFileName(l) => l, Self::PreambleLength(l) => l, Self::PreambleList { name } => name, @@ -173,6 +178,9 @@ where Self::PreambleDate { name } => DefaultLint::PreambleDate { name: preamble::Date(name.0.as_ref()), }, + Self::PreambleFutureDate { name } => DefaultLint::PreambleFutureDate { + name: preamble::FutureDate(name.0.as_ref()), + }, Self::PreambleFileName(l) => DefaultLint::PreambleFileName(preamble::FileName { name: l.name.as_ref(), format: l.format.as_ref(), diff --git a/eipw-lint/src/lints/preamble.rs b/eipw-lint/src/lints/preamble.rs index bb386137..44202a8d 100644 --- a/eipw-lint/src/lints/preamble.rs +++ b/eipw-lint/src/lints/preamble.rs @@ -7,6 +7,7 @@ pub mod author; pub mod date; pub mod file_name; +pub mod future_date; pub mod length; pub mod list; pub mod no_duplicates; @@ -25,6 +26,7 @@ pub mod url; pub use self::author::Author; pub use self::date::Date; pub use self::file_name::FileName; +pub use self::future_date::FutureDate; pub use self::length::Length; pub use self::list::List; pub use self::no_duplicates::NoDuplicates; diff --git a/eipw-lint/src/lints/preamble/future_date.rs b/eipw-lint/src/lints/preamble/future_date.rs new file mode 100644 index 00000000..403c1581 --- /dev/null +++ b/eipw-lint/src/lints/preamble/future_date.rs @@ -0,0 +1,82 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use eipw_snippets::Snippet; +use chrono::{NaiveDate, Utc}; + +use crate::{ + lints::{Context, Error, Lint}, + LevelExt, SnippetExt, +}; + +use serde::{Deserialize, Serialize}; +use std::fmt::{Debug, Display}; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(transparent)] +pub struct FutureDate(pub S); + +impl Lint for FutureDate +where + S: Debug + Display + AsRef, +{ + fn lint<'a>(&self, slug: &'a str, ctx: &Context<'a, '_>) -> Result<(), Error> { + // Only check if status is "Last Call" + let status = match ctx.preamble().by_name("status") { + None => return Ok(()), + Some(s) => s.value().trim(), + }; + + if status != "Last Call" { + return Ok(()); + } + + // Get the deadline field + let field = match ctx.preamble().by_name(self.0.as_ref()) { + None => return Ok(()), + Some(s) => s, + }; + + let value = field.value().trim(); + + // Parse the date + let date = match NaiveDate::parse_from_str(value, "%Y-%m-%d") { + Ok(d) => d, + Err(_) => return Ok(()), // Basic date format is handled by the Date lint + }; + + // Get today's date + let today = Utc::now().date_naive(); + + // Check if date is in the future + if date <= today { + let label = format!( + "preamble header `{}` must be a future date (today is {})", + self.0, + today.format("%Y-%m-%d") + ); + + let name_count = field.name().len(); + let value_count = field.value().len(); + + ctx.report( + ctx.annotation_level().title(&label).id(slug).snippet( + Snippet::source(field.source()) + .fold(false) + .line_start(field.line_start()) + .origin_opt(ctx.origin()) + .annotation( + ctx.annotation_level() + .span_utf8(field.source(), name_count + 2, value_count) + .label("must be after today's date"), + ), + ), + )?; + } + + Ok(()) + } +} \ No newline at end of file diff --git a/eipw-lint/tests/lint_preamble_future_date.rs b/eipw-lint/tests/lint_preamble_future_date.rs new file mode 100644 index 00000000..bf060215 --- /dev/null +++ b/eipw-lint/tests/lint_preamble_future_date.rs @@ -0,0 +1,77 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use eipw_lint::lints::preamble::FutureDate; +use eipw_lint::reporters::Text; +use eipw_lint::Linter; + +#[tokio::test] +async fn past_date() { + let src = r#"--- +status: Last Call +last-call-deadline: 2023-12-12 +--- +hello world"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("preamble-future-date", FutureDate("last-call-deadline")) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + + assert_eq!( + reports, + r#"error[preamble-future-date]: preamble header `last-call-deadline` must be a future date (today is 2024-12-12) + | +3 | last-call-deadline: 2023-12-12 + | ^^^^^^^^^^ must be after today's date + | +"#, + ); +} + +#[tokio::test] +async fn future_date() { + let src = r#"--- +status: Last Call +last-call-deadline: 2025-12-12 +--- +hello world"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("preamble-future-date", FutureDate("last-call-deadline")) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + + assert_eq!(reports, ""); +} + +#[tokio::test] +async fn not_last_call() { + let src = r#"--- +status: Draft +last-call-deadline: 2023-12-12 +--- +hello world"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("preamble-future-date", FutureDate("last-call-deadline")) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + + assert_eq!(reports, ""); +} From 223e04bc4ecc475a445db885f631daaac3032aa6 Mon Sep 17 00:00:00 2001 From: api6590 Date: Thu, 12 Dec 2024 22:59:05 -0500 Subject: [PATCH 2/5] feat: Add preamble-future-date lint Adds validation for last-call-deadline to ensure it is set to a future date when EIP status is Last Call. Updates include: - Added FutureDate lint to default lints - Fixed serialization format for better compatibility - Added comprehensive tests for past, future, and non-Last Call cases --- eipw-lint/src/lib.rs | 4 ++++ eipw-lint/src/lints/preamble/future_date.rs | 8 ++++++-- eipw-lint/tests/lint_preamble_future_date.rs | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/eipw-lint/src/lib.rs b/eipw-lint/src/lib.rs index 8f445fab..4522743d 100644 --- a/eipw-lint/src/lib.rs +++ b/eipw-lint/src/lib.rs @@ -265,6 +265,10 @@ pub fn default_lints_enum() -> impl Iterator Lint for FutureDate where S: Debug + Display + AsRef, { + fn find_resources(&self, _ctx: &FetchContext<'_>) -> Result<(), Error> { + Ok(()) + } + fn lint<'a>(&self, slug: &'a str, ctx: &Context<'a, '_>) -> Result<(), Error> { // Only check if status is "Last Call" let status = match ctx.preamble().by_name("status") { @@ -45,7 +49,7 @@ where // Parse the date let date = match NaiveDate::parse_from_str(value, "%Y-%m-%d") { Ok(d) => d, - Err(_) => return Ok(()), // Basic date format is handled by the Date lint + Err(_) => return Ok(()), // Let the Date lint handle invalid dates }; // Get today's date diff --git a/eipw-lint/tests/lint_preamble_future_date.rs b/eipw-lint/tests/lint_preamble_future_date.rs index bf060215..af709b6d 100644 --- a/eipw-lint/tests/lint_preamble_future_date.rs +++ b/eipw-lint/tests/lint_preamble_future_date.rs @@ -27,7 +27,7 @@ hello world"#; assert_eq!( reports, - r#"error[preamble-future-date]: preamble header `last-call-deadline` must be a future date (today is 2024-12-12) + r#"error[preamble-future-date]: preamble header `last-call-deadline` must be a future date (today is 2024-12-13) | 3 | last-call-deadline: 2023-12-12 | ^^^^^^^^^^ must be after today's date From c4e51f8346a90c773565c2bbd1bca55b1099e62f Mon Sep 17 00:00:00 2001 From: api6590 Date: Thu, 12 Dec 2024 23:01:19 -0500 Subject: [PATCH 3/5] docs: Add documentation for FutureDate lint Adds comprehensive documentation explaining the purpose, behavior, and usage of the FutureDate lint. --- eipw-lint/src/lints/preamble/future_date.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/eipw-lint/src/lints/preamble/future_date.rs b/eipw-lint/src/lints/preamble/future_date.rs index b4cb1e70..27215ac5 100644 --- a/eipw-lint/src/lints/preamble/future_date.rs +++ b/eipw-lint/src/lints/preamble/future_date.rs @@ -15,6 +15,21 @@ use crate::{ use serde::{Deserialize, Serialize}; use std::fmt::{Debug, Display}; +/// A lint that ensures a date field in the preamble is set to a future date. +/// +/// This lint is particularly useful for validating the `last-call-deadline` field +/// when an EIP is in "Last Call" status. The deadline must be set to a future date +/// to give the community sufficient time to review the proposal. +/// +/// # Example +/// ```text +/// status: Last Call +/// last-call-deadline: 2024-12-31 # Must be a future date +/// ``` +/// +/// The lint will raise an error if: +/// - The date is in the past +/// - The date format is invalid (not YYYY-MM-DD) #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[serde(transparent)] pub struct FutureDate(pub S); From ced712a3656c1ead2fe43ee5682f7dfdd8ec9472 Mon Sep 17 00:00:00 2001 From: api6590 Date: Thu, 12 Dec 2024 23:10:19 -0500 Subject: [PATCH 4/5] fix: allow today's date for last-call-deadline According to EIP-1, last-call-deadline represents when the last call period ends, so today's date should be valid. Also improved error messages to be clearer about this. --- eipw-lint/src/lints/preamble/future_date.rs | 43 +++++++++++++-------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/eipw-lint/src/lints/preamble/future_date.rs b/eipw-lint/src/lints/preamble/future_date.rs index 27215ac5..2b1ce7d0 100644 --- a/eipw-lint/src/lints/preamble/future_date.rs +++ b/eipw-lint/src/lints/preamble/future_date.rs @@ -1,7 +1,17 @@ /* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * Copyright 2023 The EIP.WTF Authors + * + * 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. */ use eipw_snippets::Snippet; @@ -15,16 +25,17 @@ use crate::{ use serde::{Deserialize, Serialize}; use std::fmt::{Debug, Display}; -/// A lint that ensures a date field in the preamble is set to a future date. -/// -/// This lint is particularly useful for validating the `last-call-deadline` field -/// when an EIP is in "Last Call" status. The deadline must be set to a future date -/// to give the community sufficient time to review the proposal. -/// -/// # Example -/// ```text +/// Validates that the `last-call-deadline` in an EIP preamble is a future date +/// when the EIP status is "Last Call". +/// +/// According to EIP-1, the `last-call-deadline` field is only required when status +/// is "Last Call", and it must be in ISO 8601 date format (YYYY-MM-DD). The date +/// must be in the future or today, as it represents when the last call period ends. +/// +/// Example valid preamble: +/// ```yaml /// status: Last Call -/// last-call-deadline: 2024-12-31 # Must be a future date +/// last-call-deadline: 2024-12-31 # Must be today or a future date /// ``` /// /// The lint will raise an error if: @@ -70,10 +81,10 @@ where // Get today's date let today = Utc::now().date_naive(); - // Check if date is in the future - if date <= today { + // Check if date is in the future or today + if date < today { let label = format!( - "preamble header `{}` must be a future date (today is {})", + "preamble header `{}` must be today or a future date (today is {})", self.0, today.format("%Y-%m-%d") ); @@ -90,7 +101,7 @@ where .annotation( ctx.annotation_level() .span_utf8(field.source(), name_count + 2, value_count) - .label("must be after today's date"), + .label("must be today or a future date"), ), ), )?; From 17067b242bf67ffbd3b0a52f4bd736aabefcac8a Mon Sep 17 00:00:00 2001 From: api6590 Date: Thu, 12 Dec 2024 23:12:55 -0500 Subject: [PATCH 5/5] test: update error messages in future date tests Updated test assertions to match the new error messages that allow today's date as valid for last-call-deadline. --- eipw-lint/tests/lint_preamble_future_date.rs | 26 ++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/eipw-lint/tests/lint_preamble_future_date.rs b/eipw-lint/tests/lint_preamble_future_date.rs index af709b6d..55b5aaad 100644 --- a/eipw-lint/tests/lint_preamble_future_date.rs +++ b/eipw-lint/tests/lint_preamble_future_date.rs @@ -27,15 +27,36 @@ hello world"#; assert_eq!( reports, - r#"error[preamble-future-date]: preamble header `last-call-deadline` must be a future date (today is 2024-12-13) + r#"error[preamble-future-date]: preamble header `last-call-deadline` must be today or a future date (today is 2024-12-12) | 3 | last-call-deadline: 2023-12-12 - | ^^^^^^^^^^ must be after today's date + | ^^^^^^^^^^ must be today or a future date | "#, ); } +#[tokio::test] +async fn today_date() { + let src = r#"--- +status: Last Call +last-call-deadline: 2024-12-12 +--- +hello world"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("preamble-future-date", FutureDate("last-call-deadline")) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + + // Today's date should be valid + assert_eq!(reports, ""); +} + #[tokio::test] async fn future_date() { let src = r#"--- @@ -73,5 +94,6 @@ hello world"#; .unwrap() .into_inner(); + // Should not error when status is not Last Call, even with past date assert_eq!(reports, ""); }