Skip to content

Commit

Permalink
Support configuration of toolbox - for now this is just enabling/disa…
Browse files Browse the repository at this point in the history
…bling rules (#35)

1. Refactor the CLI parser into it's own file 
2. Add support for basic configuration - toolbox.toml controls how the
toolbox linter will behave
3. Add documentation to how config works
4. Added subcommand `genconfig` that prints out default config
  • Loading branch information
EliSchleifer authored Feb 27, 2024
1 parent b97e4ef commit f49d103
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 32 deletions.
7 changes: 7 additions & 0 deletions .config/toolbox.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Configuration for trunk toolbox. Generate default by calling 'trunk-toolbox genconfig'

[ifchange]
enabled = true

[donotland]
enabled = true
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ serde_json = "1.0.85"
serde-sarif = "0.3.4"
content_inspector = "0.2.4"
rayon = "1.5.1"
confique = "0.2.5"

[dev-dependencies]
assert_cmd = "2.0"
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ To enable the toolbox rules in your repository run:
trunk check enable trunk-toolbox
```

### Configuration

Toolbox can be configured via the toolbox.toml file. There is an example config file [here](.config/toolbox.toml). A full example file can be generated by calling

```bash
trunk-toolbox genconfig
```

### Rules

#### do-not-land
Expand Down
31 changes: 31 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// trunk-ignore-all(trunk-toolbox/do-not-land)
use confique::toml::{self, FormatOptions};
use confique::Config;

#[derive(Config)]
pub struct Conf {
#[config(nested)]
pub ifchange: IfChangeConf,

#[config(nested)]
pub donotland: PlsNotLandConf,
}

impl Conf {
pub fn print_default() {
let default_config = toml::template::<Conf>(FormatOptions::default());
println!("{}", default_config);
}
}

#[derive(Config)]
pub struct IfChangeConf {
#[config(default = true)]
pub enabled: bool,
}

#[derive(Config)]
pub struct PlsNotLandConf {
#[config(default = true)]
pub enabled: bool,
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod config;
pub mod diagnostic;
pub mod git;
pub mod rules;
pub mod run;
53 changes: 30 additions & 23 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,44 @@
use clap::Parser;
use confique::Config;
use horton::config::Conf;
use horton::diagnostic;
use horton::rules::if_change_then_change::ictc;
use horton::rules::pls_no_land::pls_no_land;
use horton::run::{Cli, Run, Subcommands};

use serde_sarif::sarif;
use std::collections::HashSet;
use std::path::PathBuf;
use std::time::Instant;
#[derive(Parser, Debug)]
#[clap(version = env!("CARGO_PKG_VERSION"), author = "Trunk Technologies Inc.")]
struct Opts {
// #[arg(short, long, num_args = 1..)]
files: Vec<String>,

#[clap(long)]
#[arg(default_value_t = String::from("HEAD"))]
upstream: String,

#[clap(long)]
#[arg(default_value_t = String::from(""))]
results: String,
}

fn run() -> anyhow::Result<()> {
let start = Instant::now();
let opts: Opts = Opts::parse();
let cli: Cli = Cli::parse();

if let Some(Subcommands::Genconfig {}) = &cli.subcommand {
Conf::print_default();
return Ok(());
}

let mut ret = diagnostic::Diagnostics::default();

// Convert to PathBufs
let paths: HashSet<PathBuf> = opts.files.into_iter().map(PathBuf::from).collect();
let config = Conf::builder()
.env()
.file("toolbox.toml")
.file(".config/toolbox.toml")
.file(".trunk/config/toolbox.toml")
.load()
.unwrap_or_else(|err| {
eprintln!("Toolbox cannot run: {}", err);
std::process::exit(1);
});

let run = Run {
paths: cli.files.into_iter().map(PathBuf::from).collect(),
config,
};

let (pls_no_land_result, ictc_result): (Result<_, _>, Result<_, _>) =
rayon::join(|| pls_no_land(&paths), || ictc(&paths, &opts.upstream));
rayon::join(|| pls_no_land(&run), || ictc(&run, &cli.upstream));

match pls_no_land_result {
Ok(result) => ret.diagnostics.extend(result),
Expand Down Expand Up @@ -90,7 +97,7 @@ fn run() -> anyhow::Result<()> {
sarif::MessageBuilder::default()
.text(format!(
"{:?} files processed in {:?}",
paths.len(),
run.paths.len(),
start.elapsed()
))
.build()
Expand Down Expand Up @@ -124,10 +131,10 @@ fn run() -> anyhow::Result<()> {

let sarif = serde_json::to_string_pretty(&sarif_built)?;

if opts.results.is_empty() {
println!("{}", sarif);
if let Some(outfile) = &cli.results {
std::fs::write(outfile, sarif)?;
} else {
std::fs::write(opts.results, sarif)?;
println!("{}", sarif);
}

Ok(())
Expand Down
15 changes: 10 additions & 5 deletions src/rules/if_change_then_change.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::run::Run;
use anyhow::Context;
use log::debug;
use std::collections::{HashMap, HashSet};
Expand Down Expand Up @@ -112,13 +113,17 @@ pub fn find_ictc_blocks(path: &PathBuf) -> anyhow::Result<Vec<IctcBlock>> {
Ok(blocks)
}

pub fn ictc(
files: &HashSet<PathBuf>,
upstream: &str,
) -> anyhow::Result<Vec<diagnostic::Diagnostic>> {
pub fn ictc(run: &Run, upstream: &str) -> anyhow::Result<Vec<diagnostic::Diagnostic>> {
let config = &run.config.ifchange;

if !config.enabled {
return Ok(vec![]);
}

// Build up list of files that actually have a ifchange block - this way we can avoid
// processing git modified chunks if none are present
let all_blocks: Vec<_> = files
let all_blocks: Vec<_> = run
.paths
.par_iter()
.filter_map(|file| find_ictc_blocks(file).ok())
.flatten()
Expand Down
22 changes: 18 additions & 4 deletions src/rules/pls_no_land.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
extern crate regex;

use crate::diagnostic;
use crate::run::Run;
use anyhow::Context;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use regex::Regex;
use std::collections::HashSet;
use std::fs::File;
use std::io::Read;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::path::{Path, PathBuf};

lazy_static::lazy_static! {
static ref RE: Regex = Regex::new(r"(?i)(DO[\s_-]*NOT[\s_-]*LAND)").unwrap();
Expand All @@ -22,12 +22,22 @@ pub fn is_binary_file(path: &PathBuf) -> std::io::Result<bool> {
Ok(buffer[..n].contains(&0))
}

pub fn is_ignored_file(path: &Path) -> bool {
// Filter out well known files that should have the word donotland in them (like toolbox.toml)
path.file_name().map_or(false, |f| f == "toolbox.toml")
}

// Checks for $re and other forms thereof in source code
//
// Note that this is named "pls_no_land" to avoid causing DNL matches everywhere in trunk-toolbox.
pub fn pls_no_land(paths: &HashSet<PathBuf>) -> anyhow::Result<Vec<diagnostic::Diagnostic>> {
pub fn pls_no_land(run: &Run) -> anyhow::Result<Vec<diagnostic::Diagnostic>> {
let config = &run.config.donotland;
if !config.enabled {
return Ok(vec![]);
}

// Scan files in parallel
let results: Result<Vec<_>, _> = paths.par_iter().map(pls_no_land_impl).collect();
let results: Result<Vec<_>, _> = run.paths.par_iter().map(pls_no_land_impl).collect();

match results {
Ok(v) => Ok(v.into_iter().flatten().collect()),
Expand All @@ -41,6 +51,10 @@ fn pls_no_land_impl(path: &PathBuf) -> anyhow::Result<Vec<diagnostic::Diagnostic
return Ok(vec![]);
}

if is_ignored_file(path) {
return Ok(vec![]);
}

let in_file = File::open(path).with_context(|| format!("failed to open: {:#?}", path))?;
let mut in_buf = BufReader::new(in_file);

Expand Down
34 changes: 34 additions & 0 deletions src/run.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use crate::config::Conf;
use std::collections::HashSet;
use std::path::PathBuf;

use clap::{Parser, Subcommand};

#[derive(Parser, Debug)]
#[clap(version = env!("CARGO_PKG_VERSION"), author = "Trunk Technologies Inc.")]
pub struct Cli {
#[command(subcommand)]
pub subcommand: Option<Subcommands>,

pub files: Vec<String>,

#[clap(long)]
#[arg(default_value_t = String::from("HEAD"))]
pub upstream: String,

#[clap(long)]
/// optional path to write results to
pub results: Option<String>,
}

#[derive(Subcommand, Debug)]
pub enum Subcommands {
// print default config for toolbox
/// Generate default configuration content for toolbox
Genconfig,
}

pub struct Run {
pub paths: HashSet<PathBuf>,
pub config: Conf,
}
26 changes: 26 additions & 0 deletions tests/do_not_land_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,29 @@ fn binary_files_ignored() -> anyhow::Result<()> {
);
Ok(())
}

#[test]
fn honor_disabled_in_config() -> anyhow::Result<()> {
let test_repo = TestRepo::make()?;
test_repo.write("alpha.foo", "do-not-land\n".as_bytes());
test_repo.git_add_all()?;

{
let horton = test_repo.run_horton()?;
assert_that(&horton.stdout).contains("Found 'do-not-land'");
}

let config = r#"
[donotland]
enabled = false
"#;

// Now disable the rule
test_repo.write("toolbox.toml", config.as_bytes());
{
let horton = test_repo.run_horton().unwrap();
assert_that(&horton.stdout.contains("Found 'do-not-land'")).is_false();
}

Ok(())
}

0 comments on commit f49d103

Please sign in to comment.