blob: cf80eb07725e0696e5aa37211f32b2d14e433391 [file] [log] [blame]
// 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)
}
}
}