use rustc_hir::def_id::DefId;
use rustc_middle::ty::TyCtxt;
use safety_parser::syn::Expr;
use crate::verify::source::assets::{AnyItem, PropertyEntry, get_std_contracts_from_assets};
use super::types::{ContractKind, Property, PropertyKind};
pub fn entry_to_property<'tcx>(
tcx: TyCtxt<'tcx>,
def_id: DefId,
entry: &PropertyEntry,
param_names: &[String],
has_names: bool,
) -> Option<Property<'tcx>> {
if entry.tag == "any" {
if let Some(disjuncts) = &entry.any {
if disjuncts.len() >= 2 {
let mut prop = any_entry_to_property(tcx, def_id, disjuncts, param_names, has_names);
prop.apply_kind(entry.kind.as_deref());
return Some(prop);
}
rap_error!(
"JSON any entry requires at least 2 disjuncts, got {}",
disjuncts.len()
);
return None;
}
rap_error!("JSON any entry missing 'any' field");
return None;
}
let exprs = resolve_json_args(&entry.args, param_names, has_names, &entry.tag);
if exprs.len() != entry.args.len() {
rap_error!(
"Parse JSON API args error: Failed to parse arg '{:?}' for tag {}",
entry.args, entry.tag
);
return None;
}
let mut property = Property::new(tcx, def_id, entry.tag.as_str(), &exprs);
property.apply_kind(entry.kind.as_deref());
if matches!(property.kind, PropertyKind::Unknown) {
rap_debug!(
"skip unsupported std safety contract tag '{}' for callee {:?}",
entry.tag, def_id
);
return None;
}
Some(property)
}
fn any_entry_to_property<'tcx>(
tcx: TyCtxt<'tcx>,
def_id: DefId,
disjuncts: &[AnyItem],
param_names: &[String],
has_names: bool,
) -> Property<'tcx> {
let mut groups: Vec<Vec<Box<Property<'tcx>>>> = Vec::new();
for item in disjuncts {
match item {
AnyItem::Single(entry) => {
if entry.tag == "any" {
rap_error!("Nested 'any' inside 'any' is not supported in JSON contracts");
continue;
}
let exprs =
resolve_json_args(&entry.args, param_names, has_names, &entry.tag);
if exprs.len() != entry.args.len() {
rap_error!(
"Parse any entry arg error: Failed to parse arg '{:?}' for tag {}",
entry.args, entry.tag
);
continue;
}
let mut prop = Property::new(tcx, def_id, entry.tag.as_str(), &exprs);
prop.apply_kind(entry.kind.as_deref());
groups.push(vec![Box::new(prop)]);
}
AnyItem::Group(entries) => {
let mut group: Vec<Box<Property<'tcx>>> = Vec::new();
for entry in entries {
if entry.tag == "any" {
rap_error!("Nested 'any' inside 'any' group is not supported");
continue;
}
let exprs =
resolve_json_args(&entry.args, param_names, has_names, &entry.tag);
if exprs.len() != entry.args.len() {
rap_error!(
"Parse any group entry arg error: failed to parse '{:?}' for tag {}",
entry.args, entry.tag
);
continue;
}
let mut prop =
Property::new(tcx, def_id, entry.tag.as_str(), &exprs);
prop.apply_kind(entry.kind.as_deref());
group.push(Box::new(prop));
}
if !group.is_empty() {
groups.push(group);
}
}
}
}
Property {
kind: PropertyKind::Or,
args: Vec::new(),
contract_kind: ContractKind::Precond,
null_guard: None,
or_alternatives: groups,
for_each: None,
}
}
pub fn resolve_json_args(
args: &[String],
param_names: &[String],
has_names: bool,
tag: &str,
) -> Vec<Expr> {
let mut exprs: Vec<Expr> = Vec::new();
for arg_str in args {
let resolved = if has_names {
resolve_json_param_name(arg_str, param_names)
} else {
arg_str.clone()
};
let normalized_arg = normalize_json_contract_arg(&resolved);
match syn::parse_str::<Expr>(&normalized_arg) {
Ok(expr) => exprs.push(expr),
Err(_) => {
if let Some(lifetime) = normalized_arg.strip_prefix('\'') {
if lifetime.chars().all(|c| c.is_alphabetic() || c == '_') {
match syn::parse_str::<Expr>(lifetime) {
Ok(expr) => exprs.push(expr),
Err(_) => {
rap_error!(
"JSON Contract Error: Failed to parse lifetime \
'{}' as Rust Expr for tag {}",
arg_str, tag
);
}
}
} else {
rap_error!(
"JSON Contract Error: Failed to parse arg '{}' as Rust Expr for tag {}",
arg_str, tag
);
}
} else {
rap_error!(
"JSON Contract Error: Failed to parse arg '{}' as Rust Expr for tag {}",
arg_str, tag
);
}
}
}
}
exprs
}
pub fn resolve_json_param_name(arg: &str, param_names: &[String]) -> String {
if arg.starts_with("arg:")
|| arg.starts_with("const:")
|| arg.starts_with("ty:")
|| arg.contains('(')
|| arg.contains('.')
|| arg.contains("::")
|| arg.contains(' ')
|| arg.starts_with('\'')
{
return arg.to_string();
}
if let Some(pos) = param_names.iter().position(|n| n == arg) {
format!("arg:{pos}")
} else {
arg.to_string()
}
}
pub fn normalize_json_contract_arg(arg: &str) -> String {
let bytes = arg.as_bytes();
let mut out = String::with_capacity(arg.len());
let mut i = 0;
while i < bytes.len() {
if arg[i..].starts_with("arg:") {
let start = i + "arg:".len();
let end = scan_while(arg, start, |ch| ch.is_ascii_digit());
if end > start {
out.push_str("Arg_");
out.push_str(&arg[start..end]);
i = end;
continue;
}
}
if arg[i..].starts_with("const:") {
let start = i + "const:".len();
let end = scan_while(arg, start, is_contract_token_char);
if end > start {
out.push_str(&arg[start..end]);
i = end;
continue;
}
}
if arg[i..].starts_with("ty:") {
let start = i + "ty:".len();
let end = scan_while(arg, start, is_contract_token_char);
if end > start {
out.push_str(&arg[start..end]);
i = end;
continue;
}
}
let ch = arg[i..].chars().next().unwrap();
out.push(ch);
i += ch.len_utf8();
}
out
}
fn scan_while(arg: &str, mut index: usize, predicate: impl Fn(char) -> bool) -> usize {
while index < arg.len() {
let ch = arg[index..].chars().next().unwrap();
if !predicate(ch) {
break;
}
index += ch.len_utf8();
}
index
}
fn is_contract_token_char(ch: char) -> bool {
ch.is_ascii_alphanumeric() || ch == '_' || ch == ':'
}
pub fn query_json_contracts<'tcx>(
tcx: TyCtxt<'tcx>,
def_id: DefId,
) -> Vec<Property<'tcx>> {
let entries = get_std_contracts_from_assets(tcx, def_id);
if entries.is_empty() {
return Vec::new();
}
let (param_names, _) = crate::helpers::name::parse_signature(tcx, def_id);
let has_names =
!param_names.is_empty() && !param_names[0].chars().all(|c| c.is_ascii_digit());
let mut results = Vec::new();
for entry in entries {
if let Some(prop) = entry_to_property(tcx, def_id, entry, ¶m_names, has_names) {
results.push(prop);
}
}
results
}