Skip to content

Commit ca6a24d

Browse files
committed
#8 feat add tier 1 engine renderers and cli check command
1 parent fd1533a commit ca6a24d

28 files changed

Lines changed: 3155 additions & 54 deletions

Cargo.lock

Lines changed: 1053 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,20 @@ schemars = "1"
3737
clap = { version = "4", features = ["derive"] }
3838
regex = "1"
3939
toml = "1"
40+
rayon = "1"
41+
ignore = "0.4"
42+
globset = "0.4"
43+
tracing = "0.1"
44+
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
45+
thiserror = "2"
46+
anyhow = "1"
47+
anstyle = "1"
48+
anstream = "1"
49+
insta = "1"
50+
assert_cmd = "2"
51+
predicates = "3"
52+
tempfile = "3"
53+
criterion = "0.8"
4054

4155
[workspace.lints.rust]
4256
unsafe_code = "forbid"

REUSE.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,9 @@ path = "Cargo.lock"
3232
precedence = "aggregate"
3333
SPDX-FileCopyrightText = "2026 RAprogramm <andrey.rozanov.vl@gmail.com>"
3434
SPDX-License-Identifier = "MIT"
35+
36+
[[annotations]]
37+
path = "crates/**/snapshots/*.snap"
38+
precedence = "aggregate"
39+
SPDX-FileCopyrightText = "2026 RAprogramm <andrey.rozanov.vl@gmail.com>"
40+
SPDX-License-Identifier = "MIT"

crates/rustmanifest-cli/Cargo.toml

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,31 @@ readme = "README.md"
1515
categories = ["command-line-utilities", "development-tools"]
1616
keywords = ["rust", "cli", "lint", "code-review"]
1717

18+
[lib]
19+
name = "rustmanifest_cli"
20+
path = "src/lib.rs"
21+
22+
[[bin]]
23+
name = "rustmanifest"
24+
path = "src/main.rs"
25+
1826
[dependencies]
27+
anstream = { workspace = true }
28+
anyhow = { workspace = true }
1929
clap = { workspace = true }
20-
rustmanifest-engine = { workspace = true }
2130
rustmanifest-config = { workspace = true }
31+
rustmanifest-engine = { workspace = true }
2232
rustmanifest-report = { workspace = true }
33+
rustmanifest-rules-core = { workspace = true }
34+
rustmanifest-schema = { workspace = true }
35+
tracing = { workspace = true }
36+
tracing-subscriber = { workspace = true }
2337

24-
[[bin]]
25-
name = "rustmanifest"
26-
path = "src/main.rs"
38+
[dev-dependencies]
39+
assert_cmd = { workspace = true }
40+
predicates = { workspace = true }
41+
serde_json = { workspace = true }
42+
tempfile = { workspace = true }
2743

2844
[lints]
2945
workspace = true

crates/rustmanifest-cli/src/lib.rs

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
// SPDX-FileCopyrightText: 2026 RAprogramm <andrey.rozanov.vl@gmail.com>
2+
// SPDX-License-Identifier: MIT
3+
4+
//! Library half of the `rustmanifest` CLI binary.
5+
//!
6+
//! The binary in `src/main.rs` is a thin shim that delegates to [`run`],
7+
//! which makes the command surface testable from integration tests without
8+
//! spawning a child process.
9+
10+
use std::{
11+
io::{IsTerminal, Write},
12+
path::PathBuf,
13+
process::ExitCode
14+
};
15+
16+
use anyhow::Context;
17+
use clap::{Parser, Subcommand, ValueEnum};
18+
use rustmanifest_engine::{OrchestratorBuilder, PatternAnalyzer, walker};
19+
use rustmanifest_report::{JsonRenderer, Renderer, SarifRenderer, TtyRenderer};
20+
use rustmanifest_rules_core::RULES;
21+
use rustmanifest_schema::{Finding, RuleDefinition, Severity};
22+
use tracing::info;
23+
24+
/// Default exit code: no findings at or above the requested severity.
25+
pub const EXIT_CLEAN: u8 = 0;
26+
/// Findings present at or above the requested severity.
27+
pub const EXIT_FINDINGS: u8 = 1;
28+
/// Operational failure (IO, argument parsing, walker error).
29+
pub const EXIT_ERROR: u8 = 2;
30+
31+
/// Top-level CLI arguments.
32+
#[derive(Debug, Parser)]
33+
#[command(
34+
name = "rustmanifest",
35+
version,
36+
about = "Production-grade Rust review engine — methodology-as-code"
37+
)]
38+
pub struct Cli {
39+
/// Increase verbosity. May be repeated (`-v`, `-vv`, `-vvv`).
40+
#[arg(short, long, action = clap::ArgAction::Count, global = true)]
41+
pub verbose: u8,
42+
43+
/// Selected subcommand.
44+
#[command(subcommand)]
45+
pub command: Command
46+
}
47+
48+
/// Available subcommands for the `rustmanifest` binary.
49+
#[derive(Debug, Subcommand)]
50+
pub enum Command {
51+
/// Run the analysis engine across one or more paths.
52+
Check(CheckArgs)
53+
}
54+
55+
/// Arguments for the `check` subcommand.
56+
#[derive(Debug, Parser)]
57+
pub struct CheckArgs {
58+
/// Files or directories to analyze. Defaults to the current directory.
59+
#[arg(default_value = ".")]
60+
pub paths: Vec<PathBuf>,
61+
62+
/// Output format.
63+
#[arg(long, value_enum, default_value_t = OutputFormat::Auto)]
64+
pub format: OutputFormat,
65+
66+
/// Minimum severity to report.
67+
#[arg(long, value_enum, default_value_t = SeverityFilter::Hint)]
68+
pub severity_filter: SeverityFilter,
69+
70+
/// Force-disable color output even on a TTY.
71+
#[arg(long)]
72+
pub no_color: bool,
73+
74+
/// Per-file memory budget in bytes.
75+
#[arg(long, default_value_t = rustmanifest_engine::orchestrator::DEFAULT_MAX_FILE_BYTES)]
76+
pub max_file_size: u64
77+
}
78+
79+
/// Output format options.
80+
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
81+
pub enum OutputFormat {
82+
/// Picks `tty` on a terminal, `json` otherwise.
83+
Auto,
84+
/// Canonical pretty-printed JSON array.
85+
Json,
86+
/// SARIF 2.1.0 document.
87+
Sarif,
88+
/// Human-readable terminal output.
89+
Tty
90+
}
91+
92+
/// Minimum severity reported in the output.
93+
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
94+
pub enum SeverityFilter {
95+
/// Report `error` only.
96+
Error,
97+
/// Report `error` and `warning`.
98+
Warning,
99+
/// Report `error`, `warning`, and `info`.
100+
Info,
101+
/// Report every severity.
102+
Hint
103+
}
104+
105+
impl SeverityFilter {
106+
const fn level(self) -> u8 {
107+
match self {
108+
Self::Error => 3,
109+
Self::Warning => 2,
110+
Self::Info => 1,
111+
Self::Hint => 0
112+
}
113+
}
114+
115+
const fn admits(self, severity: Severity) -> bool {
116+
let severity_level: u8 = match severity {
117+
Severity::Error => 3,
118+
Severity::Warning => 2,
119+
Severity::Info => 1,
120+
Severity::Hint => 0
121+
};
122+
severity_level >= self.level()
123+
}
124+
}
125+
126+
/// Entry point used by both the binary and integration tests.
127+
///
128+
/// # Errors
129+
///
130+
/// Returns an [`anyhow::Error`] for unrecoverable operational failures.
131+
pub fn run(cli: Cli) -> anyhow::Result<ExitCode> {
132+
init_tracing(cli.verbose);
133+
match cli.command {
134+
Command::Check(args) => run_check(&args)
135+
}
136+
}
137+
138+
fn init_tracing(verbose: u8) {
139+
let level = match verbose {
140+
0 => tracing::Level::WARN,
141+
1 => tracing::Level::INFO,
142+
2 => tracing::Level::DEBUG,
143+
_ => tracing::Level::TRACE
144+
};
145+
let subscriber = tracing_subscriber::fmt()
146+
.with_max_level(level)
147+
.with_target(false)
148+
.with_writer(std::io::stderr)
149+
.finish();
150+
let _ = tracing::subscriber::set_global_default(subscriber);
151+
}
152+
153+
fn run_check(args: &CheckArgs) -> anyhow::Result<ExitCode> {
154+
let analyzers: Vec<Box<dyn rustmanifest_engine::Analyzer>> = RULES
155+
.iter()
156+
.filter(|rule| matches!(rule.definition, RuleDefinition::Pattern { .. }))
157+
.map(|rule| {
158+
PatternAnalyzer::new(rule.clone())
159+
.map(|analyzer| Box::new(analyzer) as Box<dyn rustmanifest_engine::Analyzer>)
160+
})
161+
.collect::<Result<Vec<_>, _>>()
162+
.context("building pattern analyzers from bundled rules")?;
163+
164+
info!(
165+
analyzer_count = analyzers.len(),
166+
path_count = args.paths.len(),
167+
"running check"
168+
);
169+
170+
let orchestrator = OrchestratorBuilder::new()
171+
.analyzers(analyzers)
172+
.max_file_bytes(args.max_file_size)
173+
.build();
174+
175+
let files = walker::discover(&args.paths).context("file discovery failed")?;
176+
let mut findings = orchestrator
177+
.run(&files)
178+
.context("orchestrator run failed")?;
179+
findings.retain(|finding| args.severity_filter.admits(finding.severity));
180+
181+
render(&findings, args.format, args.no_color)?;
182+
183+
if findings.is_empty() {
184+
Ok(ExitCode::from(EXIT_CLEAN))
185+
} else {
186+
Ok(ExitCode::from(EXIT_FINDINGS))
187+
}
188+
}
189+
190+
fn render(findings: &[Finding], format: OutputFormat, no_color: bool) -> anyhow::Result<()> {
191+
let resolved = resolve_format(format);
192+
let stdout = std::io::stdout();
193+
let mut handle = stdout.lock();
194+
match resolved {
195+
OutputFormat::Json | OutputFormat::Auto => {
196+
let mut renderer = JsonRenderer::new(&mut handle);
197+
renderer.render(findings).context("rendering JSON output")?;
198+
}
199+
OutputFormat::Sarif => {
200+
let mut renderer = SarifRenderer::new(&mut handle, env!("CARGO_PKG_VERSION"));
201+
renderer
202+
.render(findings)
203+
.context("rendering SARIF output")?;
204+
}
205+
OutputFormat::Tty => {
206+
let color = !no_color && std::io::stdout().is_terminal();
207+
let mut renderer = TtyRenderer::new(&mut handle).with_color(color);
208+
renderer.render(findings).context("rendering TTY output")?;
209+
}
210+
}
211+
handle.flush().context("flushing stdout")?;
212+
Ok(())
213+
}
214+
215+
fn resolve_format(format: OutputFormat) -> OutputFormat {
216+
if format == OutputFormat::Auto {
217+
if std::io::stdout().is_terminal() {
218+
OutputFormat::Tty
219+
} else {
220+
OutputFormat::Json
221+
}
222+
} else {
223+
format
224+
}
225+
}
Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,20 @@
11
// SPDX-FileCopyrightText: 2026 RAprogramm <andrey.rozanov.vl@gmail.com>
22
// SPDX-License-Identifier: MIT
33

4-
//! Command-line frontend for the `rustmanifest` engine.
5-
//!
6-
//! Phase 0 exposes only `--version` and the subcommand surface so that the
7-
//! shape of the CLI is locked while the engine is still being built.
4+
//! Binary entry point — thin shim over [`rustmanifest_cli::run`].
85
9-
use clap::{Parser, Subcommand};
6+
use std::process::ExitCode;
107

11-
/// Top-level CLI arguments.
12-
#[derive(Debug, Parser)]
13-
#[command(
14-
name = "rustmanifest",
15-
version,
16-
about = "Production-grade Rust review engine — methodology-as-code"
17-
)]
18-
struct Cli {
19-
/// Selected subcommand.
20-
#[command(subcommand)]
21-
command: Command
22-
}
23-
24-
/// Available subcommands for the `rustmanifest` binary.
25-
#[derive(Debug, Subcommand)]
26-
enum Command {
27-
/// Print the build metadata and exit.
28-
Version
29-
}
8+
use clap::Parser;
9+
use rustmanifest_cli::{Cli, EXIT_ERROR};
3010

31-
fn main() {
11+
fn main() -> ExitCode {
3212
let cli = Cli::parse();
33-
match cli.command {
34-
Command::Version => {}
13+
match rustmanifest_cli::run(cli) {
14+
Ok(code) => code,
15+
Err(err) => {
16+
tracing::error!(error = %err, "rustmanifest failed");
17+
ExitCode::from(EXIT_ERROR)
18+
}
3519
}
3620
}

0 commit comments

Comments
 (0)