#[derive(Debug, Default, serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct UpgradeSummary {
total_findings: usize,
warning_count: usize,
error_count: usize,
auto_compatible_count: usize,
manual_migration_count: usize,
manifest_review_count: usize,
}
#[derive(Debug, serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct FileUpgradeReport {
input: String,
compile_success: bool,
contracts: Vec<String>,
summary: UpgradeSummary,
findings: Vec<neo_devpack_solidity::solidity::UpgradeFinding>,
}
fn serialize_upgrade_reports(reports: &[FileUpgradeReport]) -> Result<String, String> {
let payload = json!({ "files": reports });
serde_json::to_string_pretty(&payload)
.map_err(|err| format!("failed to serialize analyze report: {err}"))
}
fn summarize_upgrade_findings(
findings: &[neo_devpack_solidity::solidity::UpgradeFinding],
) -> UpgradeSummary {
use neo_devpack_solidity::solidity::{UpgradeCategory, UpgradeSeverity};
let mut summary = UpgradeSummary {
total_findings: findings.len(),
..UpgradeSummary::default()
};
for finding in findings {
match finding.severity {
UpgradeSeverity::Warning => summary.warning_count += 1,
UpgradeSeverity::Error => summary.error_count += 1,
UpgradeSeverity::Info => {}
}
match finding.category {
UpgradeCategory::AutoCompatible => summary.auto_compatible_count += 1,
UpgradeCategory::ManualMigration => summary.manual_migration_count += 1,
UpgradeCategory::ManifestReview => summary.manifest_review_count += 1,
}
}
summary
}
fn classify_upgrade_category(message: &str) -> neo_devpack_solidity::solidity::UpgradeCategory {
use neo_devpack_solidity::solidity::UpgradeCategory;
let lower = message.to_ascii_lowercase();
if lower.contains("wildcard manifest permissions")
|| lower.contains("wildcard contract manifest permissions")
|| lower.contains("wildcard method manifest permissions")
|| (lower.contains("manifest") && lower.contains("permissions"))
{
UpgradeCategory::ManifestReview
} else if lower.contains("auto-mapped") || lower.contains("compiler-compatible") {
UpgradeCategory::AutoCompatible
} else {
UpgradeCategory::ManualMigration
}
}
fn collect_overload_findings(
metadata: &ContractMetadata,
) -> Vec<neo_devpack_solidity::solidity::UpgradeFinding> {
use neo_devpack_solidity::solidity::{UpgradeCategory, UpgradeFinding, UpgradeSeverity};
let mut groups: HashMap<&str, usize> = HashMap::new();
for method in &metadata.methods {
if !matches!(method.kind, FunctionKind::Regular) {
continue;
}
if !matches!(
method.visibility,
VisibilityKind::Public | VisibilityKind::External
) {
continue;
}
*groups.entry(method.name.as_str()).or_insert(0) += 1;
}
groups
.into_iter()
.filter(|&(_, count)| count > 1)
.map(|(name, _)| {
UpgradeFinding::new(
"analysis",
UpgradeSeverity::Warning,
UpgradeCategory::ManifestReview,
Some("SCAN_EXPORTED_OVERLOADS"),
Some(metadata.name.clone()),
format!(
"contract '{}' exports overloaded function '{}'; Neo manifest ABI keeps one canonical name and mangles the other overloads.",
metadata.name, name
),
Some(
"Avoid overloaded public/external APIs, or ensure downstream callers use the Neo manifest method names.",
),
)
})
.collect()
}
fn build_upgrade_report(
input_file: &str,
analysis_source: &str,
compile_source: &str,
verbose: bool,
options: &CompileOptions,
contract_filters: &[String],
) -> Option<FileUpgradeReport> {
use neo_devpack_solidity::solidity::{
analyse_all_sources, analyze_upgrade_patterns, UpgradeCategory, UpgradeFinding,
UpgradeSeverity,
};
let mut findings = analyze_upgrade_patterns(analysis_source);
let analyzed_contracts = analyse_all_sources(compile_source).unwrap_or_default();
let mut contracts: Vec<String> = analyzed_contracts
.iter()
.map(|metadata| metadata.name.clone())
.collect();
if !contract_filters.is_empty() {
contracts.retain(|name| contract_filters.iter().any(|filter| filter == name));
if contracts.is_empty() {
return None;
}
}
for metadata in &analyzed_contracts {
if !contract_filters.is_empty()
&& !contract_filters
.iter()
.any(|filter| filter == &metadata.name)
{
continue;
}
findings.extend(collect_overload_findings(metadata));
}
let compile_success =
match compile_contracts_with_options(compile_source, verbose, options.clone()) {
Ok(mut artifacts) => {
if !contract_filters.is_empty() {
artifacts.retain(|artifact| {
contract_filters
.iter()
.any(|filter| filter == &artifact.metadata.name)
});
}
for artifact in artifacts {
let contract_name = artifact.metadata.name.clone();
for warning in artifact.warnings {
findings.push(UpgradeFinding::new(
"compile",
match warning.severity {
DiagnosticSeverity::Warning => UpgradeSeverity::Warning,
DiagnosticSeverity::Error => UpgradeSeverity::Error,
},
classify_upgrade_category(&warning.message),
warning.code,
Some(contract_name.clone()),
warning.message,
warning.suggestion,
));
}
}
true
}
Err(CompileError::Diagnostics(diags)) => {
for diag in diags {
findings.push(UpgradeFinding::new(
"validation",
match diag.severity {
DiagnosticSeverity::Warning => UpgradeSeverity::Warning,
DiagnosticSeverity::Error => UpgradeSeverity::Error,
},
diag.code
.as_deref()
.map(classify_upgrade_category)
.unwrap_or_else(|| classify_upgrade_category(&diag.message)),
diag.code,
None::<String>,
diag.message,
diag.suggestion,
));
}
false
}
Err(CompileError::Semantic(diags)) => {
for diag in diags {
findings.push(UpgradeFinding::new(
"semantic",
match diag.severity {
DiagnosticSeverity::Warning => UpgradeSeverity::Warning,
DiagnosticSeverity::Error => UpgradeSeverity::Error,
},
classify_upgrade_category(&diag.message),
diag.code,
None::<String>,
diag.message,
diag.suggestion,
));
}
false
}
Err(CompileError::Ir(errors)) => {
for error in errors {
findings.push(UpgradeFinding::new(
"ir",
UpgradeSeverity::Error,
classify_upgrade_category(&error.message),
error.code,
None::<String>,
format!("function '{}': {}", error.function_name, error.message),
error.suggestion,
));
}
false
}
Err(CompileError::Manifest(message)) => {
findings.push(UpgradeFinding::new(
"manifest",
UpgradeSeverity::Error,
UpgradeCategory::ManifestReview,
Some("MANIFEST_GENERATION_ERROR"),
None::<String>,
message,
Some(
"Review the inferred Neo permissions and contract metadata before deployment.",
),
));
false
}
Err(CompileError::ParseErrors(diags)) => {
for diag in diags {
let category = classify_upgrade_category(&diag.message);
findings.push(UpgradeFinding::new(
"compile",
UpgradeSeverity::Error,
category,
Some("PARSE_ERROR"),
None::<String>,
diag.message,
None::<String>,
));
}
false
}
Err(CompileError::Message(message)) => {
findings.push(UpgradeFinding::new(
"compile",
UpgradeSeverity::Error,
classify_upgrade_category(&message),
Some("GENERIC_ERROR"),
None::<String>,
message,
None::<String>,
));
false
}
};
Some(FileUpgradeReport {
input: input_file.to_string(),
compile_success,
contracts,
summary: summarize_upgrade_findings(&findings),
findings,
})
}
fn print_upgrade_reports(reports: &[FileUpgradeReport]) {
match serialize_upgrade_reports(reports) {
Ok(serialized) => println!("{serialized}"),
Err(err) => {
eprintln!("error: {err}");
std::process::exit(1);
}
}
}
fn write_upgrade_reports(path: &str, reports: &[FileUpgradeReport]) -> Result<(), String> {
let serialized = serialize_upgrade_reports(reports)?;
ensure_output_dir(path)?;
fs::write(path, serialized)
.map_err(|err| format!("Failed to write analyze report '{path}': {err}"))
}