#![cfg_attr(coverage_nightly, coverage(off))]
pub mod analysis;
pub mod analysis_helpers;
pub mod analysis_utilities;
pub mod args;
pub mod colors;
pub mod command_dispatcher;
pub mod command_structure;
pub mod commands;
pub mod coverage_helpers;
pub mod dead_code_formatter;
pub mod defect_formatter;
pub mod defect_helpers;
pub mod defect_prediction_helpers;
pub mod diagnose;
pub mod drift_detector;
pub mod enums;
pub mod error_context;
pub mod formatting_helpers;
pub mod handlers;
pub mod help_generator;
pub mod language_analyzer;
pub mod mcp_schema_generator;
pub mod name_similarity_helpers;
pub mod output;
pub mod progress;
pub mod proof_annotation_formatter;
pub mod proof_annotation_helpers;
pub mod provability_helpers;
pub mod registry;
pub mod semantic_commands;
pub mod symbol_table_helpers;
pub mod tdg_helpers;
pub mod unified_help;
pub mod verify;
pub use commands::{
AgentCommands, AnalyzeCommands, Cli, Commands, EnforceCommands, Mode, RefactorCommands,
};
pub use enums::*;
pub use handlers::get_timeline_help_text; pub use help_generator::HelpGenerator; pub use mcp_schema_generator::McpSchemaGenerator; pub use registry::{
ArgumentMetadata, CommandMetadata, CommandRegistry, ExampleMetadata, McpToolMetadata,
}; pub use unified_help::{HelpResponse, HelpSearchResult, UnifiedHelpService};
use crate::stateless_server::StatelessTemplateServer;
use command_dispatcher::CommandDispatcher;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use tracing::{debug, info};
#[derive(Debug, Clone)]
pub struct NameInfo {
pub name: String,
pub kind: String,
pub file_path: PathBuf,
pub line: usize,
}
#[derive(Debug, Clone)]
pub struct NameSimilarityResult {
pub name: String,
pub kind: String,
pub file_path: PathBuf,
pub line: usize,
pub similarity: f32,
pub phonetic_match: bool,
pub fuzzy_match: bool,
}
#[derive(Debug, Clone)]
pub struct DuplicateHandlerConfig {
pub project_path: PathBuf,
pub detection_type: DuplicateType,
pub threshold: f32,
pub min_lines: usize,
pub max_tokens: usize,
pub format: DuplicateOutputFormat,
pub perf: bool,
pub include: Option<String>,
pub exclude: Option<String>,
pub output: Option<PathBuf>,
}
#[derive(Debug, Clone)]
pub struct EarlyCliArgs {
pub verbose: bool,
pub debug: bool,
pub trace: bool,
pub trace_filter: Option<String>,
pub is_mcp_server: bool,
}
#[must_use]
#[cfg_attr(coverage_nightly, coverage(off))]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn parse_early_for_tracing() -> EarlyCliArgs {
let args: Vec<String> = std::env::args().collect();
let verbose = args.iter().any(|arg| arg == "-v" || arg == "--verbose");
let debug = args.iter().any(|arg| arg == "--debug");
let trace = args.iter().any(|arg| arg == "--trace");
let is_mcp_server = args.len() >= 3 && args[1] == "agent" && args[2] == "mcp-server";
let trace_filter = args
.iter()
.position(|arg| arg == "--trace-filter")
.and_then(|pos| args.get(pos + 1))
.cloned()
.or_else(|| std::env::var("RUST_LOG").ok());
EarlyCliArgs {
verbose,
debug,
trace,
trace_filter,
is_mcp_server,
}
}
#[must_use]
pub fn forced_mode_from_args(args: &[String]) -> Option<crate::cli::commands::Mode> {
use crate::cli::commands::Mode;
let mut forced = None;
let mut rest = args.iter().skip(1);
while let Some(arg) = rest.next() {
let value = if arg == "--mode" {
rest.next().map(String::as_str)
} else {
arg.strip_prefix("--mode=")
};
match value {
Some("mcp") => forced = Some(Mode::Mcp),
Some("cli") => forced = Some(Mode::Cli),
_ => {}
}
}
forced
}
#[must_use]
pub fn color_mode_from_args(args: &[String]) -> Option<crate::cli::commands::ColorMode> {
use crate::cli::commands::ColorMode;
let mut chosen = None;
let mut rest = args.iter().skip(1);
while let Some(arg) = rest.next() {
let value = if arg == "--color" {
rest.next().map(String::as_str)
} else {
arg.strip_prefix("--color=")
};
match value {
Some("never") => chosen = Some(ColorMode::Never),
Some("always") => chosen = Some(ColorMode::Always),
Some("auto") => chosen = Some(ColorMode::Auto),
_ => {}
}
}
chosen
}
#[must_use]
pub fn tracing_ansi_enabled(args: &[String], stderr_is_tty: bool) -> bool {
let is_set = |k: &str| std::env::var_os(k).is_some_and(|v| !v.is_empty());
tracing_ansi_from(
color_mode_from_args(args),
is_set("NO_COLOR"),
is_set("CLICOLOR_FORCE"),
stderr_is_tty,
)
}
#[must_use]
pub fn tracing_ansi_from(
flag: Option<crate::cli::commands::ColorMode>,
no_color_env: bool,
clicolor_force_env: bool,
stderr_is_tty: bool,
) -> bool {
use crate::cli::commands::ColorMode;
colors::colors_enabled_from(
matches!(flag, Some(ColorMode::Never)) || no_color_env,
matches!(flag, Some(ColorMode::Always)) || clicolor_force_env,
stderr_is_tty,
)
}
pub fn write_fatal_error<W: std::io::Write>(mut w: W, error: &anyhow::Error) {
let _ = writeln!(w, "Error: {error:#}");
}
pub fn ensure_analysis_path_exists(path: &Path) -> anyhow::Result<()> {
if !path.exists() {
anyhow::bail!("Path not found: {}", path.display());
}
if path.is_dir() {
if let Err(e) = std::fs::read_dir(path) {
return Err(
anyhow::Error::new(e).context(format!("Path not readable: {}", path.display()))
);
}
}
Ok(())
}
include!("cli_run_command.rs");
include!("cli_language_detection.rs");
include!("cli_deep_context.rs");
include!("cli_handler_stubs.rs");
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod language_detection_tests;
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod analysis_utilities_property_tests;
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod analysis_path_guard_tests {
use super::ensure_analysis_path_exists;
use std::path::Path;
#[test]
fn rejects_a_path_that_does_not_exist() {
let err = ensure_analysis_path_exists(Path::new("/definitely-not-a-real-path-9f3a"))
.expect_err("a missing path must not be reported as analysable");
let msg = err.to_string();
assert!(msg.contains("Path not found"), "got: {msg}");
assert!(
msg.contains("/definitely-not-a-real-path-9f3a"),
"must name the offending path so a typo is obvious, got: {msg}"
);
}
#[test]
fn accepts_an_existing_directory() {
let dir = tempfile::TempDir::new().expect("tempdir");
assert!(ensure_analysis_path_exists(dir.path()).is_ok());
}
#[test]
fn accepts_an_existing_file() {
let dir = tempfile::TempDir::new().expect("tempdir");
let file = dir.path().join("a.rs");
std::fs::write(&file, "fn main() {}").expect("write");
assert!(ensure_analysis_path_exists(&file).is_ok());
}
#[cfg(unix)]
#[test]
fn rejects_a_directory_it_cannot_read() {
use std::os::unix::fs::PermissionsExt;
let parent = tempfile::TempDir::new().expect("tempdir");
let locked = parent.path().join("noread");
std::fs::create_dir(&locked).expect("create dir");
std::fs::write(locked.join("main.rs"), "fn main() {}").expect("write");
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000))
.expect("chmod 000");
let permissions_bite = std::fs::read_dir(&locked).is_err();
let result = ensure_analysis_path_exists(&locked);
let _ = std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o755));
if !permissions_bite {
return;
}
let err = result.expect_err("an unreadable directory must not be reported as analysable");
let msg = err.to_string();
assert!(msg.contains("Path not readable"), "got: {msg}");
assert!(
format!("{err:#}").contains("Permission denied"),
"the underlying cause must survive in the chain, got: {err:#}"
);
assert!(
!msg.to_lowercase().contains("permission"),
"outermost message must not trip the 126 classifier, got: {msg}"
);
assert!(
msg.contains("noread"),
"must name the offending path, got: {msg}"
);
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod forced_mode_tests {
use super::forced_mode_from_args;
use crate::cli::commands::Mode;
fn argv(rest: &[&str]) -> Vec<String> {
std::iter::once("pmat")
.chain(rest.iter().copied())
.map(str::to_string)
.collect()
}
#[test]
fn mode_mcp_is_recognised_in_every_position() {
for args in [
vec!["--mode", "mcp"],
vec!["--mode=mcp"],
vec!["analyze", "complexity", "--mode", "mcp"],
vec!["list", "--mode=mcp"],
] {
assert_eq!(
forced_mode_from_args(&argv(&args)),
Some(Mode::Mcp),
"{args:?}"
);
}
}
#[test]
fn mode_cli_is_recognised() {
assert_eq!(
forced_mode_from_args(&argv(&["--mode", "cli", "list"])),
Some(Mode::Cli)
);
assert_eq!(
forced_mode_from_args(&argv(&["--mode=cli"])),
Some(Mode::Cli)
);
}
#[test]
fn no_mode_flag_means_auto_detect() {
assert_eq!(forced_mode_from_args(&argv(&[])), None);
assert_eq!(
forced_mode_from_args(&argv(&["analyze", "complexity"])),
None
);
}
#[test]
fn an_unknown_mode_value_is_left_for_clap() {
assert_eq!(forced_mode_from_args(&argv(&["--mode", "wat"])), None);
assert_eq!(forced_mode_from_args(&argv(&["--mode=MCP"])), None);
}
#[test]
fn the_program_name_is_not_scanned() {
assert_eq!(forced_mode_from_args(&["--mode=mcp".to_string()]), None);
}
#[test]
fn a_dangling_mode_flag_is_none() {
assert_eq!(forced_mode_from_args(&argv(&["--mode"])), None);
}
#[test]
fn the_last_occurrence_wins() {
assert_eq!(
forced_mode_from_args(&argv(&["--mode", "cli", "--mode", "mcp"])),
Some(Mode::Mcp)
);
}
#[test]
fn color_never_turns_off_tracing_ansi_even_on_a_tty() {
for args in [
vec!["--color", "never", "analyze", "defect-prediction"],
vec!["--color=never"],
] {
let flag = super::color_mode_from_args(&argv(&args));
assert!(
!super::tracing_ansi_from(flag, false, true, true),
"{args:?}: --color never must win over CLICOLOR_FORCE and a tty"
);
}
}
#[test]
fn color_always_turns_tracing_ansi_on_off_a_terminal() {
let flag = super::color_mode_from_args(&argv(&["--color", "always"]));
assert!(super::tracing_ansi_from(flag, false, false, false));
}
#[test]
fn auto_follows_whether_stderr_is_a_terminal() {
let flag = super::color_mode_from_args(&argv(&["list"]));
assert_eq!(flag, None);
assert!(super::tracing_ansi_from(flag.clone(), false, false, true));
assert!(!super::tracing_ansi_from(flag, false, false, false));
}
#[test]
fn an_unknown_color_value_is_left_for_clap() {
assert_eq!(
super::color_mode_from_args(&argv(&["--color", "chartreuse"])),
None
);
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod fatal_error_tests {
use super::write_fatal_error;
fn rendered(error: &anyhow::Error) -> String {
let mut buf = Vec::new();
write_fatal_error(&mut buf, error);
String::from_utf8(buf).expect("diagnostic must be UTF-8")
}
#[test]
fn writes_the_error_message() {
let out = rendered(&anyhow::anyhow!(
"Agent daemon feature not enabled. Build with --features agent-daemon"
));
assert!(
out.contains("Agent daemon feature not enabled"),
"fatal diagnostic must carry the message, got: {out:?}"
);
assert!(out.starts_with("Error: "), "got: {out:?}");
assert!(
out.ends_with('\n'),
"must be newline-terminated, got: {out:?}"
);
}
#[test]
fn includes_the_full_context_chain() {
let error = anyhow::anyhow!("root cause").context("outer");
let out = rendered(&error);
assert!(out.contains("outer"), "got: {out:?}");
assert!(
out.contains("root cause"),
"must render the whole anyhow context chain, got: {out:?}"
);
}
#[test]
fn never_writes_an_empty_diagnostic() {
for message in ["x", "", " "] {
let out = rendered(&anyhow::anyhow!(message.to_string()));
assert!(
!out.trim().is_empty(),
"fatal path must never produce an empty diagnostic, got: {out:?}"
);
}
}
}