From e8ba753f8e1941c68a277b2974b0183d2d0abe72 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 21 Aug 2024 18:15:26 +0300 Subject: [PATCH] add new cddl-linter tool, cleanups --- rust/{hermes-ipfs => }/.gitignore | 0 rust/cbork/Cargo.toml | 2 + rust/cbork/Earthfile | 2 +- rust/cbork/abnf-parser/Cargo.toml | 3 +- rust/cbork/abnf-parser/src/lib.rs | 14 ++--- rust/cbork/cddl-linter/Cargo.toml | 17 ++++++ rust/cbork/cddl-linter/README.md | 13 ++++ rust/cbork/cddl-linter/src/cli.rs | 88 +++++++++++++++++++++++++++ rust/cbork/cddl-linter/src/errors.rs | 90 ++++++++++++++++++++++++++++ rust/cbork/cddl-linter/src/main.rs | 9 +++ rust/cbork/cddl-parser/Cargo.toml | 3 +- rust/cbork/cddl-parser/src/lib.rs | 9 ++- 12 files changed, 234 insertions(+), 16 deletions(-) rename rust/{hermes-ipfs => }/.gitignore (100%) create mode 100644 rust/cbork/cddl-linter/Cargo.toml create mode 100644 rust/cbork/cddl-linter/README.md create mode 100644 rust/cbork/cddl-linter/src/cli.rs create mode 100644 rust/cbork/cddl-linter/src/errors.rs create mode 100644 rust/cbork/cddl-linter/src/main.rs diff --git a/rust/hermes-ipfs/.gitignore b/rust/.gitignore similarity index 100% rename from rust/hermes-ipfs/.gitignore rename to rust/.gitignore diff --git a/rust/cbork/Cargo.toml b/rust/cbork/Cargo.toml index 12719c13a..d1d9fbf79 100644 --- a/rust/cbork/Cargo.toml +++ b/rust/cbork/Cargo.toml @@ -1,9 +1,11 @@ [workspace] package.edition = "2021" +package.license = "MIT OR Apache-2.0" resolver = "2" members = [ "cddl-parser", "abnf-parser", + "cddl-linter", ] [workspace.lints.rust] diff --git a/rust/cbork/Earthfile b/rust/cbork/Earthfile index e4772fb87..f6b73bb9e 100644 --- a/rust/cbork/Earthfile +++ b/rust/cbork/Earthfile @@ -8,7 +8,7 @@ IMPORT github.com/input-output-hk/catalyst-ci/earthly/rust:v3.1.24 AS rust-ci builder: DO rust-ci+SETUP - COPY --dir .cargo .config cddl-parser abnf-parser . + COPY --dir .cargo .config cddl-parser abnf-parser cddl-linter . COPY Cargo.toml clippy.toml deny.toml rustfmt.toml . # RUN cargo generate-lockfile diff --git a/rust/cbork/abnf-parser/Cargo.toml b/rust/cbork/abnf-parser/Cargo.toml index d4fb94c56..33045ab32 100644 --- a/rust/cbork/abnf-parser/Cargo.toml +++ b/rust/cbork/abnf-parser/Cargo.toml @@ -4,7 +4,7 @@ name = "abnf-parser" version = "0.1.0" edition.workspace = true -license = "MIT OR Apache-2.0" +license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -15,3 +15,4 @@ workspace = true derive_more = "0.99.17" pest = { version = "2.7.2", features = ["std", "pretty-print", "memchr", "const_prec_climber"] } pest_derive = { version = "2.7.2", features = ["grammar-extras"] } +thiserror = "1.0.56" diff --git a/rust/cbork/abnf-parser/src/lib.rs b/rust/cbork/abnf-parser/src/lib.rs index 691674843..6a62dc43e 100644 --- a/rust/cbork/abnf-parser/src/lib.rs +++ b/rust/cbork/abnf-parser/src/lib.rs @@ -1,12 +1,10 @@ +//! A parser for ABNF, utilized for parsing in accordance with RFC 5234. + // cspell: words Naur #![allow(missing_docs)] // TODO(apskhem): Temporary, to bo removed in a subsequent PR -//! A parser for ABNF, utilized for parsing in accordance with RFC 5234. - -use std::fmt::Debug; - -use derive_more::{Display, From}; +use derive_more::From; pub use pest::Parser; use pest::{error::Error, iterators::Pairs}; @@ -27,14 +25,14 @@ pub mod abnf_test { pub struct ABNFTestParser; } +/// Abstract Syntax Tree (AST) representing parsed ABNF syntax. #[derive(Debug)] #[allow(dead_code)] -/// Abstract Syntax Tree (AST) representing parsed ABNF syntax. pub struct AST<'a>(Pairs<'a, abnf::Rule>); /// Represents an error that may occur during ABNF parsing. -#[derive(Display, Debug, From)] -/// Error type for ABNF parsing. +#[derive(thiserror::Error, Debug, From)] +#[error("{0}")] pub struct ABNFError(Error); /// Parses the input string containing ABNF (Augmented Backus-Naur Form) syntax and diff --git a/rust/cbork/cddl-linter/Cargo.toml b/rust/cbork/cddl-linter/Cargo.toml new file mode 100644 index 000000000..c2f32ed86 --- /dev/null +++ b/rust/cbork/cddl-linter/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "cddl-linter" +version = "0.1.0" +edition.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lints] +workspace = true + +[dependencies] +cddl-parser = { path = "../cddl-parser", version = "0.1.0" } +clap = { version = "4.5.3", features = ["derive", "env"] } +anyhow = "1.0.71" +console = "0.15.8" + diff --git a/rust/cbork/cddl-linter/README.md b/rust/cbork/cddl-linter/README.md new file mode 100644 index 000000000..69e2b597f --- /dev/null +++ b/rust/cbork/cddl-linter/README.md @@ -0,0 +1,13 @@ +# CDDL linter + +[CDDL](https://datatracker.ietf.org/doc/html/rfc8610) (Concise Data Definition Language) +linting cli tool, +enabling users to check their CDDL code for errors, inconsistencies, and compliance with the CDDL specification. + +## Install + +To install this tool run + +```shell +cargo install --git https://github.com/input-output-hk/hermes.git cddl-linter +``` diff --git a/rust/cbork/cddl-linter/src/cli.rs b/rust/cbork/cddl-linter/src/cli.rs new file mode 100644 index 000000000..bc7f81dd4 --- /dev/null +++ b/rust/cbork/cddl-linter/src/cli.rs @@ -0,0 +1,88 @@ +//! CLI interpreter for the cbork lint tool + +use std::{path::PathBuf, process::exit}; + +use clap::Parser; +use console::{style, Emoji}; + +/// CDDL linter cli tool +#[derive(Parser)] +pub(crate) struct Cli { + /// Path to the CDDL files definition. + /// It could path to the standalone file, or to the directory. + /// So all files with the `.cddl` extension inside the directory will be linted. + path: PathBuf, +} + +impl Cli { + /// Execute the CLI + pub(crate) fn exec(self) { + let res = if self.path.is_file() { + check_file_with_print(&self.path) + } else { + check_dir_with_print(&self.path) + }; + + if !res { + exit(1); + } + } +} + +/// Check the CDDL file, return any errors +fn check_file(file_path: &PathBuf) -> anyhow::Result<()> { + let mut content = std::fs::read_to_string(file_path)?; + cddl_parser::parse_cddl(&mut content, &cddl_parser::Extension::CDDLParser)?; + Ok(()) +} + +/// Check the CDDL file, prints any errors into the stdout +fn check_file_with_print(file_path: &PathBuf) -> bool { + if let Err(e) = check_file(file_path) { + println!( + "{} {}:\n{}", + Emoji::new("🚨", "Errors"), + file_path.display(), + style(e).red() + ); + false + } else { + println!("{} {}", Emoji::new("✅", "Success"), file_path.display(),); + true + } +} + +/// CDDL file extension. Filter directory files to apply the linter only on the CDDL +/// files. +const CDDL_FILE_EXTENSION: &str = "cddl"; + +/// Check the directory, prints any errors into the stdout +fn check_dir_with_print(dir_path: &PathBuf) -> bool { + let fun = |dir_path| -> anyhow::Result { + let mut res = true; + for entry in std::fs::read_dir(dir_path)? { + let entry = entry?; + let path = entry.path(); + if path.is_file() { + if path.extension().is_some_and(|e| e.eq(CDDL_FILE_EXTENSION)) { + res = check_file_with_print(&path); + } + } else if path.is_dir() { + res = check_dir_with_print(&path); + } + } + Ok(res) + }; + + if let Err(e) = fun(dir_path) { + println!( + "{} {}:\n{}", + Emoji::new("🚨", "Errors"), + dir_path.display(), + style(e).red() + ); + false + } else { + true + } +} diff --git a/rust/cbork/cddl-linter/src/errors.rs b/rust/cbork/cddl-linter/src/errors.rs new file mode 100644 index 000000000..f3cf795cb --- /dev/null +++ b/rust/cbork/cddl-linter/src/errors.rs @@ -0,0 +1,90 @@ +//! Errors module. + +#![allow(dead_code)] + +use std::{error::Error, fmt::Display}; + +/// Errors struct which holds a collection of errors +#[derive(Debug)] +pub(crate) struct Errors(Vec); + +impl Error for Errors {} + +impl Display for Errors { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for err in &self.0 { + write!(f, "- ")?; + let err_str = err.to_string(); + let mut err_lines = err_str.lines(); + if let Some(first_line) = err_lines.next() { + writeln!(f, "{first_line}")?; + for line in err_lines { + writeln!(f, " {line}")?; + } + } + } + Ok(()) + } +} + +impl Errors { + /// Create a new empty `Errors` + pub(crate) fn new() -> Self { + Self(Vec::new()) + } + + /// Returns `true` if the `Errors` contains no elements. + pub(crate) fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Add an error to the `Errors` + pub(crate) fn add_err(&mut self, err: E) + where E: Into { + let err = err.into(); + match err.downcast::() { + Ok(errs) => self.0.extend(errs.0), + Err(err) => self.0.push(err), + } + } + + /// Return a closure that adds an error to the `Errors` + pub(crate) fn get_add_err_fn(&mut self) -> impl FnOnce(E) + '_ + where E: Into { + |err| self.add_err(err) + } + + /// Return errors if `Errors` is not empty or return `Ok(val)` + pub(crate) fn return_result(self, val: T) -> anyhow::Result { + if self.0.is_empty() { + Ok(val) + } else { + Err(self.into()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_errors() { + let mut errors_1 = Errors::new(); + errors_1.add_err(anyhow::anyhow!("error 1")); + errors_1.add_err(anyhow::anyhow!("error 2")); + + let mut errors_2 = Errors::new(); + errors_2.add_err(anyhow::anyhow!("error 3")); + errors_2.add_err(anyhow::anyhow!("error 4")); + + let mut combined_errors = Errors::new(); + combined_errors.add_err(errors_1); + combined_errors.add_err(errors_2); + + assert_eq!( + combined_errors.to_string(), + "- error 1\n- error 2\n- error 3\n- error 4\n" + ); + } +} diff --git a/rust/cbork/cddl-linter/src/main.rs b/rust/cbork/cddl-linter/src/main.rs new file mode 100644 index 000000000..421ddd4fc --- /dev/null +++ b/rust/cbork/cddl-linter/src/main.rs @@ -0,0 +1,9 @@ +//! CDDL linter cli tool + +mod cli; + +fn main() { + use clap::Parser; + + cli::Cli::parse().exec(); +} diff --git a/rust/cbork/cddl-parser/Cargo.toml b/rust/cbork/cddl-parser/Cargo.toml index a693c65d9..a060cbd75 100644 --- a/rust/cbork/cddl-parser/Cargo.toml +++ b/rust/cbork/cddl-parser/Cargo.toml @@ -4,7 +4,7 @@ name = "cddl-parser" version = "0.1.0" edition.workspace = true -license = "MIT OR Apache-2.0" +license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -15,3 +15,4 @@ workspace = true derive_more = "0.99.17" pest = { version = "2.7.2", features = ["std", "pretty-print", "memchr", "const_prec_climber"] } pest_derive = { version = "2.7.2", features = ["grammar-extras"] } +thiserror = "1.0.56" diff --git a/rust/cbork/cddl-parser/src/lib.rs b/rust/cbork/cddl-parser/src/lib.rs index b452c87ea..2e1b75ec1 100644 --- a/rust/cbork/cddl-parser/src/lib.rs +++ b/rust/cbork/cddl-parser/src/lib.rs @@ -1,8 +1,6 @@ -#![allow(missing_docs)] // TODO(apskhem): Temporary, to bo removed in a subsequent PR - //! A parser for CDDL, utilized for parsing in accordance with RFC 8610. -use std::fmt::Debug; +#![allow(missing_docs)] // TODO(apskhem): Temporary, to bo removed in a subsequent PR use derive_more::{Display, From}; pub use pest::Parser; @@ -60,9 +58,9 @@ pub enum Extension { // CDDL Standard Postlude - read from an external file pub const POSTLUDE: &str = include_str!("grammar/postlude.cddl"); +/// Abstract Syntax Tree (AST) representing parsed CDDL syntax. // TODO: this is temporary. need to add more pragmatic nodes #[derive(Debug)] -/// Abstract Syntax Tree (AST) representing parsed CDDL syntax. pub enum AST<'a> { /// Represents the AST for RFC 8610 CDDL rules. RFC8610(Pairs<'a, rfc_8610::Rule>), @@ -84,7 +82,8 @@ pub enum CDDLErrorType { } /// Represents an error that may occur during CDDL parsing. -#[derive(Display, Debug, From)] +#[derive(thiserror::Error, Debug, From)] +#[error("{0}")] pub struct CDDLError(CDDLErrorType); /// Parses and checks semantically a CDDL input string.