diff --git a/zk_toolbox/crates/zk_supervisor/src/commands/lint.rs b/zk_toolbox/crates/zk_supervisor/src/commands/lint.rs new file mode 100644 index 00000000000..bbad72f6537 --- /dev/null +++ b/zk_toolbox/crates/zk_supervisor/src/commands/lint.rs @@ -0,0 +1,189 @@ +use clap::{Parser, ValueEnum}; +use common::{cmd::Cmd, logger, spinner::Spinner}; +use config::EcosystemConfig; +use strum::EnumIter; +use xshell::{cmd, Shell}; + +use crate::messages::{ + msg_running_linter_for_extension_spinner, msg_running_linters_for_files, + MSG_LINT_CONFIG_PATH_ERR, MSG_RUNNING_CONTRACTS_LINTER_SPINNER, +}; + +const IGNORED_DIRS: [&str; 18] = [ + "target", + "node_modules", + "volumes", + "build", + "dist", + ".git", + "generated", + "grafonnet-lib", + "prettier-config", + "lint-config", + "cache", + "artifacts", + "typechain", + "binaryen", + "system-contracts", + "artifacts-zk", + "cache-zk", + // Ignore directories with OZ and forge submodules. + "contracts/l1-contracts/lib", +]; + +const IGNORED_FILES: [&str; 4] = [ + "KeysWithPlonkVerifier.sol", + "TokenInit.sol", + ".tslintrc.js", + ".prettierrc.js", +]; + +const CONFIG_PATH: &str = "etc/lint-config"; + +#[derive(Debug, Parser)] +pub struct LintArgs { + #[clap(long, short = 'c')] + pub check: bool, + #[clap(long, short = 'e')] + pub extensions: Vec, +} + +#[derive(Debug, ValueEnum, EnumIter, strum::Display, PartialEq, Eq, Clone)] +#[strum(serialize_all = "lowercase")] +pub enum Extension { + Rs, + Md, + Sol, + Js, + Ts, +} + +pub fn run(shell: &Shell, args: LintArgs) -> anyhow::Result<()> { + let extensions = if args.extensions.is_empty() { + vec![ + Extension::Rs, + Extension::Md, + Extension::Sol, + Extension::Js, + Extension::Ts, + ] + } else { + args.extensions.clone() + }; + + logger::info(msg_running_linters_for_files(&extensions)); + + let ecosystem = EcosystemConfig::from_file(shell)?; + + for extension in extensions { + match extension { + Extension::Rs => lint_rs(shell, &ecosystem)?, + Extension::Sol => lint_contracts(shell, &ecosystem, args.check)?, + ext => lint(shell, &ecosystem, &ext, args.check)?, + } + } + + Ok(()) +} + +fn lint_rs(shell: &Shell, ecosystem: &EcosystemConfig) -> anyhow::Result<()> { + let spinner = Spinner::new(&msg_running_linter_for_extension_spinner(&Extension::Rs)); + + let link_to_code = &ecosystem.link_to_code; + let lint_to_prover = &ecosystem.link_to_code.join("prover"); + let link_to_toolbox = &ecosystem.link_to_code.join("zk_toolbox"); + let paths = vec![link_to_code, lint_to_prover, link_to_toolbox]; + + for path in paths { + let _dir_guard = shell.push_dir(path); + Cmd::new(cmd!( + shell, + "cargo clippy --locked -- -D warnings -D unstable_features" + )) + .run()?; + } + + spinner.finish(); + + Ok(()) +} + +fn get_linter(extension: &Extension) -> Vec { + match extension { + Extension::Rs => vec!["cargo".to_string(), "clippy".to_string()], + Extension::Md => vec!["markdownlint".to_string()], + Extension::Sol => vec!["solhint".to_string()], + Extension::Js => vec!["eslint".to_string()], + Extension::Ts => vec!["eslint".to_string(), "--ext".to_string(), "ts".to_string()], + } +} + +fn lint( + shell: &Shell, + ecosystem: &EcosystemConfig, + extension: &Extension, + check: bool, +) -> anyhow::Result<()> { + let spinner = Spinner::new(&msg_running_linter_for_extension_spinner(extension)); + let _dir_guard = shell.push_dir(&ecosystem.link_to_code); + let files = get_unignored_files(shell, extension)?; + + let cmd = cmd!(shell, "yarn"); + let config_path = ecosystem.link_to_code.join(CONFIG_PATH); + let config_path = config_path.join(format!("{}.js", extension)); + let config_path = config_path + .to_str() + .expect(MSG_LINT_CONFIG_PATH_ERR) + .to_string(); + + let linter = get_linter(extension); + + let fix_option = if check { + vec![] + } else { + vec!["--fix".to_string()] + }; + + let args = [ + linter.as_slice(), + &fix_option, + &["--config".to_string(), config_path], + files.as_slice(), + ] + .concat(); + + Cmd::new(cmd.args(&args)).run()?; + spinner.finish(); + Ok(()) +} + +fn lint_contracts(shell: &Shell, ecosystem: &EcosystemConfig, check: bool) -> anyhow::Result<()> { + lint(shell, ecosystem, &Extension::Sol, check)?; + + let spinner = Spinner::new(MSG_RUNNING_CONTRACTS_LINTER_SPINNER); + let _dir_guard = shell.push_dir(&ecosystem.link_to_code); + let cmd = cmd!(shell, "yarn"); + let linter = if check { "lint:check" } else { "lint:fix" }; + let args = ["--cwd", "contracts", linter]; + Cmd::new(cmd.args(&args)).run()?; + spinner.finish(); + + Ok(()) +} + +fn get_unignored_files(shell: &Shell, extension: &Extension) -> anyhow::Result> { + let mut files = Vec::new(); + let output = cmd!(shell, "git ls-files").read()?; + + for line in output.lines() { + let path = line.to_string(); + if !IGNORED_DIRS.iter().any(|dir| path.contains(dir)) + && !IGNORED_FILES.contains(&path.as_str()) + && path.ends_with(&format!(".{}", extension)) + { + files.push(path); + } + } + + Ok(files) +} diff --git a/zk_toolbox/crates/zk_supervisor/src/commands/mod.rs b/zk_toolbox/crates/zk_supervisor/src/commands/mod.rs index cc2b0a12b33..b7a6a54f121 100644 --- a/zk_toolbox/crates/zk_supervisor/src/commands/mod.rs +++ b/zk_toolbox/crates/zk_supervisor/src/commands/mod.rs @@ -1,4 +1,5 @@ pub mod clean; pub mod database; +pub mod lint; pub mod snapshot; pub mod test; diff --git a/zk_toolbox/crates/zk_supervisor/src/main.rs b/zk_toolbox/crates/zk_supervisor/src/main.rs index 2976fb55418..51b8f00ef37 100644 --- a/zk_toolbox/crates/zk_supervisor/src/main.rs +++ b/zk_toolbox/crates/zk_supervisor/src/main.rs @@ -1,5 +1,7 @@ use clap::{Parser, Subcommand}; -use commands::{database::DatabaseCommands, snapshot::SnapshotCommands, test::TestCommands}; +use commands::{ + database::DatabaseCommands, lint::LintArgs, snapshot::SnapshotCommands, test::TestCommands, +}; use common::{ check_general_prerequisites, config::{global_config, init_global_config, GlobalConfig}, @@ -9,7 +11,7 @@ use common::{ use config::EcosystemConfig; use messages::{ msg_global_chain_does_not_exist, MSG_SUBCOMMAND_CLEAN, MSG_SUBCOMMAND_DATABASE_ABOUT, - MSG_SUBCOMMAND_TESTS_ABOUT, + MSG_SUBCOMMAND_LINT_ABOUT, MSG_SUBCOMMAND_TESTS_ABOUT, }; use xshell::Shell; @@ -38,6 +40,8 @@ enum SupervisorSubcommands { Clean(CleanCommands), #[command(subcommand, about = "Snapshots creator")] Snapshot(SnapshotCommands), + #[command(about = MSG_SUBCOMMAND_LINT_ABOUT, alias = "l")] + Lint(LintArgs), #[command(hide = true)] Markdown, } @@ -94,6 +98,7 @@ async fn run_subcommand(args: Supervisor, shell: &Shell) -> anyhow::Result<()> { SupervisorSubcommands::Markdown => { clap_markdown::print_help_markdown::(); } + SupervisorSubcommands::Lint(args) => commands::lint::run(shell, args)?, } Ok(()) } diff --git a/zk_toolbox/crates/zk_supervisor/src/messages.rs b/zk_toolbox/crates/zk_supervisor/src/messages.rs index bb58b0983e7..6368cb4e3d5 100644 --- a/zk_toolbox/crates/zk_supervisor/src/messages.rs +++ b/zk_toolbox/crates/zk_supervisor/src/messages.rs @@ -1,3 +1,5 @@ +use crate::commands::lint::Extension; + // Ecosystem related messages pub(super) const MSG_CHAIN_NOT_FOUND_ERR: &str = "Chain not found"; @@ -9,6 +11,7 @@ pub(super) fn msg_global_chain_does_not_exist(chain: &str, available_chains: &st pub(super) const MSG_SUBCOMMAND_DATABASE_ABOUT: &str = "Database related commands"; pub(super) const MSG_SUBCOMMAND_TESTS_ABOUT: &str = "Run tests"; pub(super) const MSG_SUBCOMMAND_CLEAN: &str = "Clean artifacts"; +pub(super) const MSG_SUBCOMMAND_LINT_ABOUT: &str = "Lint code"; // Database related messages pub(super) const MSG_NO_DATABASES_SELECTED: &str = "No databases selected"; @@ -135,3 +138,19 @@ pub(super) const MSG_CONTRACTS_CLEANING_FINISHED: &str = /// Snapshot creator related messages pub(super) const MSG_RUNNING_SNAPSHOT_CREATOR: &str = "Running snapshot creator"; + +// Lint related messages +pub(super) fn msg_running_linters_for_files(extensions: &[Extension]) -> String { + let extensions: Vec = extensions.iter().map(|e| format!(".{}", e)).collect(); + format!( + "Running linters for files with extensions: {:?}", + extensions + ) +} + +pub(super) fn msg_running_linter_for_extension_spinner(extension: &Extension) -> String { + format!("Running linter for files with extension: .{}", extension) +} + +pub(super) const MSG_LINT_CONFIG_PATH_ERR: &str = "Lint config path error"; +pub(super) const MSG_RUNNING_CONTRACTS_LINTER_SPINNER: &str = "Running contracts linter..";