| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| mod allow; |
| mod api; |
| mod bugspec; |
| mod command_ext; |
| mod fix; |
| mod issues; |
| mod lint; |
| mod mock; |
| mod owners; |
| mod rollout; |
| mod span; |
| |
| use anyhow::{anyhow, bail, Result}; |
| use argh::FromArgs; |
| |
| use std::{ |
| env, |
| fs::{self, File}, |
| io::{self, BufRead, BufReader}, |
| path::{Path, PathBuf}, |
| process::Command, |
| }; |
| |
| use crate::api::Api; |
| |
| const DEFAULT_ROLLOUT_PATH: &str = "./rollout.json~"; |
| |
| #[derive(Debug, FromArgs)] |
| /// Silence rustc and clippy lints with allow attributes and autofixes |
| struct Args { |
| #[argh(subcommand)] |
| action: Action, |
| /// path to a binary for API calls |
| #[argh(option)] |
| api: Option<PathBuf>, |
| /// mock API issue creation calls |
| #[argh(switch)] |
| mock: bool, |
| /// print details of created issues to the command line |
| #[argh(switch)] |
| verbose: bool, |
| /// print API calls to the command line |
| #[argh(switch)] |
| log_api: bool, |
| } |
| |
| impl Args { |
| pub fn api(&self) -> Result<Box<dyn Api>> { |
| let api = self.api.as_ref().map(|path| { |
| Box::new(bugspec::Bugspec::new(path.clone(), self.log_api)) as Box<dyn Api> |
| }); |
| |
| if self.mock { |
| Ok(Box::new(mock::Mock::new(self.log_api, api))) |
| } else { |
| Ok(api.ok_or_else(|| anyhow!("--api is required when shush is not mocked"))?) |
| } |
| } |
| } |
| |
| #[derive(Debug, FromArgs)] |
| #[argh(subcommand)] |
| enum Action { |
| Lint(Lint), |
| Rollout(Rollout), |
| } |
| |
| /// address existing lints |
| #[derive(FromArgs, Debug)] |
| #[argh(subcommand, name = "lint")] |
| struct Lint { |
| /// how to address the lints |
| #[argh(subcommand)] |
| action: LintAction, |
| /// path to the root dir of the fuchsia source tree |
| #[argh(option)] |
| fuchsia_dir: Option<PathBuf>, |
| /// don't modify source files |
| #[argh(switch)] |
| dryrun: bool, |
| /// modify files even if there are local uncommitted changes |
| #[argh(switch)] |
| force: bool, |
| /// lint (or category) to deal with e.g. clippy::needless_return |
| #[argh(option)] |
| lint: Vec<String>, |
| /// file containing json lints (uses stdin if not given) |
| #[argh(positional)] |
| lint_file: Option<PathBuf>, |
| } |
| |
| impl Lint { |
| pub fn change_to_fuchsia_root(&self) -> Result<PathBuf> { |
| if let Some(fuchsia_dir) = |
| self.fuchsia_dir.clone().or_else(|| env::var("FUCHSIA_DIR").ok().map(Into::into)) |
| { |
| env::set_current_dir(&fuchsia_dir.canonicalize()?)?; |
| Ok(fuchsia_dir) |
| } else { |
| Ok(std::env::current_dir()?.canonicalize()?) |
| } |
| } |
| |
| pub fn try_get_filter(&self) -> Result<&[String]> { |
| if self.lint.is_empty() { |
| Err(anyhow!("Must filter on at least one lint or category with '--lint'")) |
| } else { |
| Ok(&self.lint) |
| } |
| } |
| |
| pub fn read_lints(&self) -> Box<dyn BufRead> { |
| if let Some(ref f) = self.lint_file { |
| Box::new(BufReader::new(File::open(f).unwrap())) |
| } else { |
| Box::new(BufReader::new(io::stdin())) |
| } |
| } |
| } |
| |
| #[derive(Debug, FromArgs)] |
| #[argh(subcommand)] |
| enum LintAction { |
| Fix(Fix), |
| Allow(Allow), |
| } |
| |
| /// use rustfix to auto-fix the lints |
| #[derive(FromArgs, Debug)] |
| #[argh(subcommand, name = "fix")] |
| struct Fix {} |
| |
| /// add allow attributes |
| #[derive(FromArgs, Debug)] |
| #[argh(subcommand, name = "allow")] |
| struct Allow { |
| /// the tag to link to on codesearch |
| #[argh(option)] |
| codesearch_tag: Option<String>, |
| /// path to an issue description template containing "INSERT_DETAILS_HERE" |
| #[argh(option)] |
| template: Option<PathBuf>, |
| /// the issue to mark created issues as blocking |
| #[argh(option)] |
| blocking_issue: Option<String>, |
| /// the maximum number of additional users to CC on created issues |
| #[argh(option, default = "3")] |
| max_cc_users: usize, |
| /// the holding component to place newly-created bugs into (default "LanguagePlatforms>Rust") |
| #[argh(option)] |
| holding_component: Option<String>, |
| /// the path to the rollout file |
| #[argh(option)] |
| rollout: Option<PathBuf>, |
| } |
| |
| impl Allow { |
| fn load_template(&self) -> Result<Option<String>> { |
| Ok(self.template.as_ref().map(|path| fs::read_to_string(path)).transpose()?) |
| } |
| |
| pub fn rollout_path(&self) -> &Path { |
| self.rollout.as_deref().unwrap_or(Path::new(DEFAULT_ROLLOUT_PATH)) |
| } |
| } |
| |
| /// roll out lints generated by allow |
| #[derive(FromArgs, Debug)] |
| #[argh(subcommand, name = "rollout")] |
| struct Rollout { |
| /// the path to the rollout file |
| #[argh(option)] |
| rollout: Option<PathBuf>, |
| } |
| |
| impl Rollout { |
| pub fn rollout_path(&self) -> &Path { |
| self.rollout.as_deref().unwrap_or(Path::new(DEFAULT_ROLLOUT_PATH)) |
| } |
| } |
| |
| fn check_clean() -> Result<()> { |
| let git_status = |
| Command::new("jiri").args(["runp", "git", "status", "--porcelain"]).output()?; |
| |
| if !git_status.status.success() || !git_status.stdout.is_empty() { |
| bail!("The current directory is dirty, pass the --force flag or commit the local changes"); |
| } |
| |
| Ok(()) |
| } |
| |
| fn main() -> Result<()> { |
| let args: Args = argh::from_env(); |
| |
| match args.action { |
| Action::Lint(ref lint_args) => { |
| if lint_args.dryrun || lint_args.force { |
| check_clean()?; |
| } |
| |
| if lint_args.dryrun && !args.mock { |
| bail!("dry runs require a mocked API"); |
| } |
| |
| match lint_args.action { |
| LintAction::Fix(_) => fix::fix( |
| &mut lint_args.read_lints(), |
| lint_args.try_get_filter()?, |
| lint_args.dryrun, |
| ), |
| LintAction::Allow(ref allow_args) => { |
| let mut api = args.api()?; |
| let mut issue_template = issues::IssueTemplate::new( |
| &lint_args.lint, |
| allow_args.codesearch_tag.as_deref(), |
| allow_args.load_template()?, |
| allow_args.blocking_issue.as_deref(), |
| allow_args.max_cc_users, |
| ); |
| |
| let rollout_path = allow_args.rollout_path(); |
| if rollout_path.exists() { |
| return Err(anyhow!( |
| "The rollout path {} already exists, delete it or specify an alternate path.", |
| rollout_path.to_str().unwrap_or("<non-utf8 path>"), |
| )); |
| } |
| |
| allow::allow( |
| &mut lint_args.read_lints(), |
| lint_args.try_get_filter()?, |
| &lint_args.change_to_fuchsia_root()?, |
| &mut *api, |
| &mut issue_template, |
| rollout_path, |
| allow_args |
| .holding_component |
| .as_ref() |
| .map(String::as_str) |
| .unwrap_or("LanguagePlatforms>Rust"), |
| lint_args.dryrun, |
| args.verbose, |
| ) |
| } |
| } |
| } |
| Action::Rollout(ref rollout_args) => { |
| let mut api = args.api()?; |
| |
| let rollout_path = rollout_args.rollout_path(); |
| if !rollout_path.exists() { |
| return Err(anyhow!( |
| "The rollout path {} does not exist, run shush allow to generate a rollout file.", |
| rollout_path.to_str().unwrap_or("<non-utf8 path>"), |
| )); |
| } |
| |
| rollout::rollout(&mut *api, rollout_path, args.verbose) |
| } |
| } |
| } |