Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
members = [
"packages/rules",
"libs/engine",
"apps/api"
"apps/api",
"crates/engine"
]

[workspace.package]
Expand Down
6 changes: 6 additions & 0 deletions crates/engine/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[package]
name = "engine"
version = "0.1.0"
edition = "2024"

[dependencies]
28 changes: 28 additions & 0 deletions crates/engine/src/analyzer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use crate::parser::Contract;
use crate::report::AnalysisReport;
use anyhow::Result;

/// Analyzer trait: a pluggable analysis unit that inspects a Contract and returns a report.
pub trait Analyzer: Send + Sync {
fn name(&self) -> &'static str;
fn analyze(&self, contract: &Contract) -> Result<AnalysisReport>;
}

/// A simple placeholder analyzer that produces no issues.
pub struct PlaceholderAnalyzer;

impl PlaceholderAnalyzer {
pub fn new() -> Self {
Self
}
}

impl Analyzer for PlaceholderAnalyzer {
fn name(&self) -> &'static str {
"placeholder"
}

fn analyze(&self, _contract: &Contract) -> Result<AnalysisReport> {
Ok(AnalysisReport::default())
}
}
60 changes: 60 additions & 0 deletions crates/engine/src/engine.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use crate::analyzer::Analyzer;
use crate::parser::Contract;
use crate::report::AnalysisReport;
use anyhow::Result;
use std::sync::Arc;

/// Engine: owns analyzers and coordinates parsing + rule execution.
pub struct Engine {
analyzers: Vec<Arc<dyn Analyzer>>,
}

impl Engine {
pub fn new() -> Self {
Self {
analyzers: Vec::new(),
}
}

pub fn register_analyzer(&mut self, analyzer: Arc<dyn Analyzer>) {
self.analyzers.push(analyzer);
}

pub fn run_on_path(&self, path: impl Into<std::path::PathBuf>) -> Result<AnalysisReport> {
let contract = Contract::load(path)?;
self.run_on_contract(&contract)
}

pub fn run_on_contract(&self, contract: &Contract) -> Result<AnalysisReport> {
let mut merged = AnalysisReport::default();
for a in &self.analyzers {
let r = a.analyze(contract)?;
merged.merge(r);
}
Ok(merged)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::analyzer::PlaceholderAnalyzer;
use std::fs::File;
use std::io::Write;
use tempfile::tempdir;
use std::sync::Arc;

#[test]
fn engine_runs_with_placeholder_analyzer() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("contract.wasm");
let mut f = File::create(&file_path).unwrap();
f.write_all(&[0u8, 1, 2, 3]).unwrap();
f.flush().unwrap();

let mut engine = Engine::new();
engine.register_analyzer(Arc::new(PlaceholderAnalyzer::new()));
let report = engine.run_on_path(file_path).expect("engine run failed");
assert_eq!(report.issues.len(), 0);
}
}
14 changes: 14 additions & 0 deletions crates/engine/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
pub fn add(left: u64, right: u64) -> u64 {
left + right
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
36 changes: 36 additions & 0 deletions crates/engine/src/parser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use crate::report::Issue;
use serde_json::Value;
use std::fs;
use std::path::PathBuf;

/// Minimal representation of a loaded contract.
/// For Soroban-first design, metadata can hold Soroban-specific info extracted later.
#[derive(Debug, Clone)]
pub struct Contract {
pub path: PathBuf,
pub bytes: Vec<u8>,
pub metadata: Option<Value>,
}

impl Contract {
pub fn load(path: impl Into<PathBuf>) -> anyhow::Result<Self> {
let path = path.into();
let bytes = fs::read(&path)?;
let metadata = None;
Ok(Self {
path,
bytes,
metadata,
})
}
}

pub fn parsing_issue(id: impl Into<String>, title: impl Into<String>, desc: Option<String>) -> Issue {
Issue {
id: id.into(),
title: title.into(),
description: desc,
severity: crate::report::Severity::Error,
source: Some("parser".to_string()),
}
}
28 changes: 28 additions & 0 deletions crates/engine/src/report.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub enum Severity {
Info,
Warning,
Error,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Issue {
pub id: String,
pub title: String,
pub description: Option<String>,
pub severity: Severity,
pub source: Option<String>,
}

#[derive(Debug, Default)]
pub struct AnalysisReport {
pub issues: Vec<Issue>,
}

impl AnalysisReport {
pub fn merge(&mut self, other: AnalysisReport) {
self.issues.extend(other.issues);
}
}
Empty file added crates/engine/src/rule.rs
Empty file.
36 changes: 27 additions & 9 deletions packages/rules/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
pub mod rule_engine;
pub mod unused_state_variables;
pub mod vyper;
pub mod soroban;

pub use rule_engine::*;
pub use unused_state_variables::*;
pub use vyper::*;
pub use soroban::*;
// pub mod rule_engine;
// pub mod unused_state_variables;
// pub mod vyper;
// pub mod soroban;

// pub use rule_engine::RuleEngine;
// pub use unused_state_variables::UnusedStateVariablesRule;
// pub use vyper::parser;
// pub use soroban::{SorobanAnalyzer, SorobanContract};


//! gasguard_engine: core static analysis engine skeleton
//! - Provides Engine, Analyzer and Rule traits
//! - Minimal parser to load a contract (raw bytes)
//! - Placeholder analyzer to satisfy acceptance criteria

pub mod analyzer;
pub mod engine;
pub mod parser;
pub mod report;
pub mod rule;

pub use analyzer::{Analyzer, PlaceholderAnalyzer};
pub use engine::Engine;
pub use parser::Contract;
pub use report::{AnalysisReport, Issue, Severity};
pub use rule::Rule;
1 change: 1 addition & 0 deletions packages/rules/src/rule_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub enum ViolationSeverity {
Error,
Warning,
Info,
Medium,
}

pub trait Rule {
Expand Down
2 changes: 1 addition & 1 deletion packages/rules/src/soroban/analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ impl SorobanAnalyzer {
violations.push(RuleViolation {
rule_name: "inefficient-integer-type".to_string(),
description: format!("Field '{}' uses {} which may be unnecessarily large", field.name, field.type_name),
suggestion: format!("Consider using a smaller integer type like u64 or u32 if the range permits", field.name),
suggestion: format!("Consider using a smaller integer type like u64 or u32 if the range permits: {}", field.name),
line_number: field.line_number,
variable_name: field.name.clone(),
severity: ViolationSeverity::Info,
Expand Down
2 changes: 1 addition & 1 deletion packages/rules/src/soroban/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ impl SorobanParser {
let mut params = Vec::new();

// Split by comma, handling nested parentheses
let param_parts = Self::split_preserving_parentheses(params_section, ',');
let param_parts = Self::split_preserving_parentheses(&params_section, ',');

for param_part in param_parts {
let param_part = param_part.trim();
Expand Down
17 changes: 8 additions & 9 deletions packages/rules/src/soroban/rule_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

use super::soroban::{SorobanAnalyzer, SorobanContract, SorobanParser, SorobanResult};
use crate::{RuleViolation, ViolationSeverity};
use std::collections::HashMap;

/// Soroban-specific rule engine
pub struct SorobanRuleEngine {
Expand Down Expand Up @@ -39,14 +38,14 @@ impl SorobanRuleEngine {

/// Add all default Soroban rules
fn add_default_rules(&mut self) {
self.add_rule(UnusedStateVariablesRule)
.add_rule(InefficientStorageAccessRule)
.add_rule(UnboundedLoopRule)
.add_rule(ExpensiveStringOperationsRule)
.add_rule(MissingConstructorRule)
.add_rule(AdminPatternRule)
.add_rule(InefficientIntegerTypesRule)
.add_rule(MissingErrorHandlingRule);
self.add_rule(UnusedStateVariablesRule { enabled: true })
.add_rule(InefficientStorageAccessRule { enabled: true })
.add_rule(UnboundedLoopRule { enabled: true })
.add_rule(ExpensiveStringOperationsRule { enabled: true })
.add_rule(MissingConstructorRule { enabled: true })
.add_rule(AdminPatternRule { enabled: true })
.add_rule(InefficientIntegerTypesRule { enabled: true })
.add_rule(MissingErrorHandlingRule { enabled: true });
}

/// Analyze Soroban contract source code
Expand Down