use std::io;
use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ContractsError {
#[error("file not found: {}", path.display())]
FileNotFound {
path: PathBuf,
},
#[error("function '{function}' not found in {}", file.display())]
FunctionNotFound {
function: String,
file: PathBuf,
},
#[error("test path not found: {}", path.display())]
TestPathNotFound {
path: PathBuf,
},
#[error("line {line} is outside function '{function}' (lines {start}-{end})")]
LineOutsideFunction {
line: u32,
function: String,
start: u32,
end: u32,
},
#[error("parse error in {}: {message}", file.display())]
ParseError {
file: PathBuf,
message: String,
},
#[error("SSA construction failed: {0}")]
SsaError(String),
#[error("analysis did not converge after {iterations} iterations")]
DidNotConverge {
iterations: u32,
},
#[error("sub-analysis '{name}' failed: {message}")]
SubAnalysisFailed {
name: String,
message: String,
},
#[error("no test directory found in {}", project.display())]
NoTestDirectory {
project: PathBuf,
},
#[error("operation timed out after {timeout_secs}s")]
Timeout {
timeout_secs: u64,
},
#[error("file too large: {} ({bytes} bytes, max {max_bytes} bytes)", path.display())]
FileTooLarge {
path: PathBuf,
bytes: u64,
max_bytes: u64,
},
#[error("AST too deeply nested in {}: depth {depth} exceeds limit {max_depth}", file.display())]
AstTooDeep {
file: PathBuf,
depth: u32,
max_depth: u32,
},
#[error("SSA graph too large: {nodes} nodes exceeds limit {max_nodes}")]
SsaTooLarge {
nodes: u32,
max_nodes: u32,
},
#[error("slice computation exceeded depth limit of {max_depth}")]
SliceDepthExceeded {
max_depth: u32,
},
#[error("invalid function name: {reason}")]
InvalidFunctionName {
reason: String,
},
#[error("path traversal blocked: {} attempts to escape project root", path.display())]
PathTraversal {
path: PathBuf,
},
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
}
pub type ContractsResult<T> = Result<T, ContractsError>;
impl ContractsError {
pub fn file_not_found(path: impl Into<PathBuf>) -> Self {
Self::FileNotFound { path: path.into() }
}
pub fn function_not_found(function: impl Into<String>, file: impl Into<PathBuf>) -> Self {
Self::FunctionNotFound {
function: function.into(),
file: file.into(),
}
}
pub fn parse_error(file: impl Into<PathBuf>, message: impl Into<String>) -> Self {
Self::ParseError {
file: file.into(),
message: message.into(),
}
}
pub fn ssa_error(message: impl Into<String>) -> Self {
Self::SsaError(message.into())
}
pub fn line_outside_function(
line: u32,
function: impl Into<String>,
start: u32,
end: u32,
) -> Self {
Self::LineOutsideFunction {
line,
function: function.into(),
start,
end,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_file_not_found() {
let err = ContractsError::file_not_found("/path/to/file.py");
let msg = err.to_string();
assert!(msg.contains("file not found"));
assert!(msg.contains("file.py"));
}
#[test]
fn test_error_function_not_found() {
let err = ContractsError::function_not_found("my_func", "/path/to/file.py");
let msg = err.to_string();
assert!(msg.contains("my_func"));
assert!(msg.contains("not found"));
assert!(msg.contains("file.py"));
}
#[test]
fn test_error_parse_error() {
let err = ContractsError::parse_error("/path/to/file.py", "unexpected token");
let msg = err.to_string();
assert!(msg.contains("parse error"));
assert!(msg.contains("unexpected token"));
}
#[test]
fn test_error_ssa_error() {
let err = ContractsError::ssa_error("failed to compute dominators");
let msg = err.to_string();
assert!(msg.contains("SSA construction failed"));
assert!(msg.contains("dominators"));
}
#[test]
fn test_error_line_outside_function() {
let err = ContractsError::line_outside_function(100, "my_func", 10, 50);
let msg = err.to_string();
assert!(msg.contains("line 100"));
assert!(msg.contains("my_func"));
assert!(msg.contains("10-50"));
}
#[test]
fn test_error_test_path_not_found() {
let err = ContractsError::TestPathNotFound {
path: PathBuf::from("/path/to/tests"),
};
let msg = err.to_string();
assert!(msg.contains("test path not found"));
}
#[test]
fn test_error_did_not_converge() {
let err = ContractsError::DidNotConverge { iterations: 50 };
let msg = err.to_string();
assert!(msg.contains("did not converge"));
assert!(msg.contains("50"));
}
#[test]
fn test_error_timeout() {
let err = ContractsError::Timeout { timeout_secs: 60 };
let msg = err.to_string();
assert!(msg.contains("timed out"));
assert!(msg.contains("60s"));
}
#[test]
fn test_error_file_too_large() {
let err = ContractsError::FileTooLarge {
path: PathBuf::from("/path/to/large.py"),
bytes: 15_000_000,
max_bytes: 10_000_000,
};
let msg = err.to_string();
assert!(msg.contains("file too large"));
assert!(msg.contains("large.py"));
}
#[test]
fn test_error_path_traversal() {
let err = ContractsError::PathTraversal {
path: PathBuf::from("../../etc/passwd"),
};
let msg = err.to_string();
assert!(msg.contains("path traversal blocked"));
}
#[test]
fn test_error_sub_analysis_failed() {
let err = ContractsError::SubAnalysisFailed {
name: "contracts".to_string(),
message: "parse error".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("sub-analysis"));
assert!(msg.contains("contracts"));
}
#[test]
fn test_error_no_test_directory() {
let err = ContractsError::NoTestDirectory {
project: PathBuf::from("/path/to/project"),
};
let msg = err.to_string();
assert!(msg.contains("no test directory"));
}
#[test]
fn test_error_io_from() {
let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
let contracts_err: ContractsError = io_err.into();
assert!(matches!(contracts_err, ContractsError::Io(_)));
}
#[test]
fn test_error_json_from() {
let json_str = "{ invalid json }";
let json_result: Result<serde_json::Value, _> = serde_json::from_str(json_str);
let json_err = json_result.unwrap_err();
let contracts_err: ContractsError = json_err.into();
assert!(matches!(contracts_err, ContractsError::Json(_)));
}
#[test]
fn test_result_type_alias() {
fn example_fn() -> ContractsResult<i32> {
Ok(42)
}
fn example_err() -> ContractsResult<i32> {
Err(ContractsError::file_not_found("/test.py"))
}
assert_eq!(example_fn().unwrap(), 42);
assert!(example_err().is_err());
}
}