Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .abide/rubric.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": 1,
"compiledAt": "2026-11-01T06:33:21Z",
"compiledBy": "botica 0.18.1",
"compiledAt": "2026-11-11T06:04:09Z",
"compiledBy": "botica 0.20.2",
"manual": true,
"sources": [
{
Expand Down Expand Up @@ -1020,6 +1020,7 @@
"crates/botica-core/src/extension_run.rs",
"crates/botica-core/src/external_config.rs",
"crates/botica-core/src/external_weakened.rs",
"crates/botica-core/src/fingerprint.rs",
"crates/botica-core/src/heavy_imports.rs",
"crates/botica-core/src/layout.rs",
"crates/botica-core/src/lib.rs",
Expand Down Expand Up @@ -1278,6 +1279,7 @@
"crates/botica-core/src/extension_run.rs",
"crates/botica-core/src/external_config.rs",
"crates/botica-core/src/external_weakened.rs",
"crates/botica-core/src/fingerprint.rs",
"crates/botica-core/src/heavy_imports.rs",
"crates/botica-core/src/layout.rs",
"crates/botica-core/src/lib.rs",
Expand Down
1 change: 1 addition & 0 deletions crates/botica-cli/src/ext_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ fn prepare(
fn run_quietly(argv: &[String], root: &std::path::Path) -> Result<(), String> {
let status = Command::new(&argv[0])
.args(&argv[1..])
.env_remove(botica_ext::HISTORY_KEY_VARIABLE)
.current_dir(root)
.stdin(Stdio::null())
.stdout(Stdio::null())
Expand Down
25 changes: 24 additions & 1 deletion crates/botica-cli/src/lint_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ mod editor;
mod extensions;
mod external;
mod external_weakened;
mod fingerprints;
mod git;
mod import_layers;
mod judged;
Expand Down Expand Up @@ -311,7 +312,8 @@ pub fn run(args: LintArgs) -> Result<ExitCode> {

let mut setup = Setup::build(&args)?;
if messages::asked(&args) {
let diagnostics = messages::findings(&args, &setup.resolution)?;
let mut diagnostics = messages::findings(&args, &setup.resolution)?;
fingerprints::stamp(format, None, fingerprints::Read::Messages, &mut diagnostics);
write_report(&args, format, &setup, &diagnostics, &[])?;
return Ok(exit_code(&[], &diagnostics, &args));
}
Expand Down Expand Up @@ -361,6 +363,16 @@ pub fn run(args: LintArgs) -> Result<ExitCode> {
if let Some(diff) = &diff {
diagnostics.retain(|diagnostic| diff.changes.keeps(diagnostic));
}
fingerprints::stamp(
format,
setup.prefix.as_deref(),
fingerprints::Read::Files {
sources: &sources,
root: &root,
staged: diff.as_ref().is_some_and(Diff::is_staged),
},
&mut diagnostics,
);
write_report(&args, format, &setup, &diagnostics, &errors)?;
if args.timings {
timings::print_rules(setup.engine.rule_times());
Expand Down Expand Up @@ -428,6 +440,17 @@ fn run_judged(
.diagnostics
.retain(|diagnostic| diff.changes.keeps(diagnostic));
}
let root = std::env::current_dir()?;
fingerprints::stamp(
format,
setup.prefix.as_deref(),
fingerprints::Read::Files {
sources: &[],
root: &root,
staged: diff.is_some_and(Diff::is_staged),
},
&mut judged.diagnostics,
);
for rule in &judged.left_out {
eprintln!(
"botica: `{rule}` is not a judgment rule of the catalog: its finding is left out"
Expand Down
2 changes: 1 addition & 1 deletion crates/botica-cli/src/lint_cmd/baseline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ fn key_of(prefix: Option<&str>, diagnostic: &Diagnostic) -> Option<Key> {
}

/// A path seen from the directory `prefix` inside the project, from its root.
fn from_root(prefix: Option<&str>, path: &str) -> String {
pub(super) fn from_root(prefix: Option<&str>, path: &str) -> String {
let mut parts: Vec<&str> = prefix
.map(|prefix| prefix.split('/').collect())
.unwrap_or_default();
Expand Down
73 changes: 73 additions & 0 deletions crates/botica-cli/src/lint_cmd/fingerprints.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//! The fingerprint of each finding in the json and sarif output (ADR 0019 decision 5).

use super::baseline::from_root;
use super::git;
use crate::output::eprintln;
use botica_core::fingerprint::FingerprintKey;
use botica_core::report::Format;
use botica_core::walk::SourceFile;
use botica_core::Diagnostic;
use std::collections::HashMap;
use std::path::Path;

/// What the findings were read from.
pub(super) enum Read<'a> {
/// The texts that the run linted. A path that they lack is read from the index when
/// `staged`, else from `root`.
Files {
sources: &'a [SourceFile],
root: &'a Path,
staged: bool,
},
/// Commit messages and titles: no finding has a line of a file.
Messages,
}

/// Sets the fingerprint of each finding when the output carries it. A `BOTICA_HISTORY_KEY` that
/// is too short leaves every fingerprint out, with a warning.
pub(super) fn stamp(
format: Format,
prefix: Option<&str>,
read: Read<'_>,
diagnostics: &mut [Diagnostic],
) {
if !matches!(format, Format::Json | Format::Sarif) {
return;
}
let key = match FingerprintKey::from_env() {
Ok(key) => key,
Err(error) => {
eprintln!("botica: warning: the findings carry no fingerprint: {error}");
return;
}
};
match read {
Read::Messages => {
for diagnostic in diagnostics {
diagnostic.fingerprint =
Some(key.of_message(&diagnostic.rule, "", &diagnostic.message));
}
}
Read::Files {
sources,
root,
staged,
} => {
let linted: HashMap<&str, &SourceFile> = sources
.iter()
.map(|source| (source.path.as_str(), source))
.collect();
key.stamp(
diagnostics,
|path| from_root(prefix, path),
|path| match linted.get(path) {
Some(source) => source.text().map(str::to_owned),
None if staged => {
git::show("", path).and_then(|bytes| String::from_utf8(bytes).ok())
}
None => std::fs::read_to_string(root.join(path)).ok(),
},
);
}
}
}
1 change: 1 addition & 0 deletions crates/botica-cli/src/lint_cmd/judged.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ fn diagnostic(found: Found, summary: &str, rule: &str, severity: Severity) -> Di
end_line: region.as_ref().and_then(|r| r.end_line).unwrap_or(line),
end_column: region.as_ref().and_then(|r| r.end_column).unwrap_or(column),
file_level: region.is_none(),
fingerprint: None,
measure: None,
message: match found.note {
Some(note) => format!("{text} ({note})"),
Expand Down
1 change: 1 addition & 0 deletions crates/botica-cli/src/rubric/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ fn candidate(path: &str, rule: &str) -> Diagnostic {
end_line: 1,
end_column: 1,
file_level: true,
fingerprint: None,
measure: None,
message: String::new(),
}
Expand Down
Loading
Loading