Skip to content
Merged
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
54 changes: 54 additions & 0 deletions src/analysis/stellar/complexity/complexity-analyzer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, expect, it } from '@jest/globals';
import { StellarComplexityAnalyzer } from './complexity-analyzer';

describe('StellarComplexityAnalyzer', () => {
it('calculates complexity for a simple function', () => {
const source = `pub struct Simple;
pub fn greet() -> u64 { 42 }`;

const analyzer = new StellarComplexityAnalyzer(source, 'simple.rs');
const report = analyzer.analyze();

expect(report.contractName).toBe('Simple');
expect(report.functions.length).toBe(1);
expect(report.functions[0].metrics.cyclomaticComplexity).toBe(1);
expect(report.functions[0].riskLevel).toBe('low');
});

it('detects high complexity from branches and conditions', () => {
const source = `pub fn complex(env: Env, x: u64) -> u64 {
if x > 10 {
if x > 20 {
return x * 2;
} else if x > 15 {
return x * 3;
}
match env {
a => 1,
b => 2,
c => 3,
}
}
0
}`;

const analyzer = new StellarComplexityAnalyzer(source, 'complex.rs');
const report = analyzer.analyze();

expect(report.functions[0].metrics.cyclomaticComplexity).toBeGreaterThan(3);
expect(report.totalComplexity).toBeGreaterThan(0);
});

it('provides risk breakdown', () => {
const source = `pub fn a() {}
pub fn b() { if true {} }
pub fn c() { if true { if false {} } match x { 1 => (), 2 => (), 3 => (), 4 => () } }`;

const analyzer = new StellarComplexityAnalyzer(source, 'multi.rs');
const report = analyzer.analyze();

expect(report.functions.length).toBe(3);
const totalRisk = report.riskBreakdown.low + report.riskBreakdown.medium + report.riskBreakdown.high + report.riskBreakdown.critical;
expect(totalRisk).toBe(3);
});
});
176 changes: 176 additions & 0 deletions src/analysis/stellar/complexity/complexity-analyzer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { ComplexityConfig, ComplexityMetrics, ComplexityReport, FunctionComplexity } from './types';

export class StellarComplexityAnalyzer {
private source: string;
private filePath: string;
private config: ComplexityConfig;

constructor(
source: string,
filePath: string,
config: ComplexityConfig = { thresholds: { low: 5, medium: 10, high: 20 }, includePrivate: true },
) {
this.source = source;
this.filePath = filePath;
this.config = config;
}

analyze(): ComplexityReport {
const contractName = this.extractContractName();
const functions = this.analyzeFunctions();

const totalComplexity = functions.reduce((sum, f) => sum + f.metrics.cyclomaticComplexity, 0);
const averageComplexity = functions.length > 0 ? Math.round(totalComplexity / functions.length) : 0;
const highestComplexity = functions.length > 0
? functions.reduce((max, f) => f.metrics.cyclomaticComplexity > max.metrics.cyclomaticComplexity ? f : max)
: null;

const riskBreakdown = { low: 0, medium: 0, high: 0, critical: 0 };
for (const f of functions) {
riskBreakdown[f.riskLevel]++;
}

return {
contractName,
functions,
totalComplexity,
averageComplexity,
highestComplexity,
riskBreakdown,
summary: this.generateSummary(contractName, functions.length, totalComplexity, averageComplexity, riskBreakdown),
};
}

private extractContractName(): string {
const match = this.source.match(/pub struct (\w+)/)
|| this.source.match(/#\[contract\]\s*\n.*?pub\s+(?:struct|fn)\s+(\w+)/)
|| this.source.match(/contract\s+(\w+)/);
return match ? match[1] : 'UnknownContract';
}

private analyzeFunctions(): FunctionComplexity[] {
const functions: FunctionComplexity[] = [];
const fnRegex = /(pub\s+)?fn\s+(\w+)\s*\(/g;
let match;

while ((match = fnRegex.exec(this.source)) !== null) {
const name = match[2];
const body = this.extractFunctionBody(match.index);

const metrics = this.calculateMetrics(body);
const riskLevel = this.determineRiskLevel(metrics.cyclomaticComplexity);
const recommendation = this.getRecommendation(name, metrics, riskLevel);

functions.push({
name,
line: this.getLineNumber(match.index),
metrics,
riskLevel,
recommendation,
});
}

return functions;
}

private extractFunctionBody(fnStart: number): string {
const openBrace = this.source.indexOf('{', fnStart);
if (openBrace === -1) return '';

let braceCount = 1;
let body = '';
for (let i = openBrace + 1; i < this.source.length && braceCount > 0; i++) {
if (this.source[i] === '{') braceCount++;
else if (this.source[i] === '}') braceCount--;
if (braceCount > 0) body += this.source[i];
}
return body;
}

private calculateMetrics(body: string): ComplexityMetrics {
const cyclomaticComplexity = 1
+ (body.match(/\bif\b/g) || []).length
+ (body.match(/\belse if\b/g) || []).length
+ (body.match(/\bmatch\b/g) || []).length
+ (body.match(/\bcase\b/g) || []).length
+ (body.match(/&&/g) || []).length
+ (body.match(/\|\|/g) || []).length;

const cognitiveComplexity = this.calculateCognitiveComplexity(body);
const branchCount = (body.match(/\bif\b|\belse\b/g) || []).length;
const loopCount = (body.match(/\bfor\b|\bwhile\b|\bloop\b/g) || []).length;
const nestingDepth = this.calculateNestingDepth(body);
const recursionDepth = this.detectRecursion(body);

return {
cyclomaticComplexity,
cognitiveComplexity,
functionCount: 0,
branchCount,
loopCount,
recursionDepth,
nestingDepth,
};
}

private calculateCognitiveComplexity(body: string): number {
let score = 0;
if (body.match(/\bif\b/g)) score += (body.match(/\bif\b/g) || []).length;
if (body.match(/\bmatch\b/g)) score += (body.match(/\bmatch\b/g) || []).length * 2;
if (body.match(/\bfor\b|\bwhile\b/g)) score += (body.match(/\bfor\b|\bwhile\b/g) || []).length * 2;
if (body.match(/\btry\b/g)) score += (body.match(/\btry\b/g) || []).length;
return score;
}

private calculateNestingDepth(body: string): number {
let maxDepth = 0;
let currentDepth = 0;
for (const char of body) {
if (char === '{') currentDepth++;
if (char === '}') currentDepth--;
maxDepth = Math.max(maxDepth, currentDepth);
}
return maxDepth;
}

private detectRecursion(body: string): number {
return 0;
}

private determineRiskLevel(complexity: number): FunctionComplexity['riskLevel'] {
if (complexity > this.config.thresholds.high) return 'critical';
if (complexity > this.config.thresholds.medium) return 'high';
if (complexity > this.config.thresholds.low) return 'medium';
return 'low';
}

private getRecommendation(name: string, metrics: ComplexityMetrics, riskLevel: string): string {
if (riskLevel === 'critical') {
return `Function "${name}" has critical complexity (${metrics.cyclomaticComplexity}). Consider splitting into smaller helper functions.`;
}
if (riskLevel === 'high') {
return `Function "${name}" has high complexity (${metrics.cyclomaticComplexity}). Review for potential simplification.`;
}
if (metrics.nestingDepth > 4) {
return `Function "${name}" has deep nesting (depth ${metrics.nestingDepth}). Consider early returns or guard clauses.`;
}
return 'Complexity is within acceptable range.';
}

private getLineNumber(offset: number): number {
return (this.source.substring(0, offset).match(/\n/g) || []).length + 1;
}

private generateSummary(
name: string,
fnCount: number,
totalComplexity: number,
avgComplexity: number,
riskBreakdown: { low: number; medium: number; high: number; critical: number },
): string {
const critical = riskBreakdown.critical;
const high = riskBreakdown.high;
return `Contract "${name}": ${fnCount} function(s), total complexity ${totalComplexity} (avg ${avgComplexity}). ` +
`${riskBreakdown.low} low, ${riskBreakdown.medium} medium, ${high} high, ${critical} critical.`;
}
}
2 changes: 2 additions & 0 deletions src/analysis/stellar/complexity/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { StellarComplexityAnalyzer } from './complexity-analyzer';
export type { ComplexityMetrics, FunctionComplexity, ComplexityReport, ComplexityConfig } from './types';
32 changes: 32 additions & 0 deletions src/analysis/stellar/complexity/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export interface ComplexityMetrics {
cyclomaticComplexity: number;
cognitiveComplexity: number;
functionCount: number;
branchCount: number;
loopCount: number;
recursionDepth: number;
nestingDepth: number;
}

export interface FunctionComplexity {
name: string;
line: number;
metrics: ComplexityMetrics;
riskLevel: 'low' | 'medium' | 'high' | 'critical';
recommendation: string;
}

export interface ComplexityReport {
contractName: string;
functions: FunctionComplexity[];
totalComplexity: number;
averageComplexity: number;
highestComplexity: FunctionComplexity | null;
riskBreakdown: { low: number; medium: number; high: number; critical: number };
summary: string;
}

export interface ComplexityConfig {
thresholds: { low: number; medium: number; high: number };
includePrivate: boolean;
}
52 changes: 52 additions & 0 deletions src/analysis/stellar/dependencies/dependency-scanner.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, expect, it } from '@jest/globals';
import { StellarDependencyScanner } from './dependency-scanner';

describe('StellarDependencyScanner', () => {
it('extracts dependencies from use statements', () => {
const source = `pub struct Token;
use soroban_sdk::Env;`;

const scanner = new StellarDependencyScanner(source, 'token.rs');
const result = scanner.scan();

expect(result.contractName).toBe('Token');
expect(result.nodes.length).toBe(0);
expect(result.directCount).toBe(0);
});

it('reports unused dependencies', () => {
const source = `pub struct Test;
use some_unused_crate::Something;
use soroban_sdk::Env;
pub fn hello(env: Env) {}`;

const scanner = new StellarDependencyScanner(source, 'test.rs');
const result = scanner.scan();

expect(result.nodes.some(n => n.name === 'some_unused_crate')).toBeTruthy();
expect(result.unusedDependencies).toContain('some_unused_crate');
});

it('detects no circular dependencies in simple chain', () => {
const source = `pub struct Linear;
use crate::a::A;
use soroban_sdk::Env;`;

const scanner = new StellarDependencyScanner(source, 'linear.rs');
const result = scanner.scan();

expect(result.circularDependencies.length).toBe(0);
});

it('summarises scan results', () => {
const source = `pub struct Summary;
use soroban_sdk::Env;
pub fn run(env: Env) {}`;

const scanner = new StellarDependencyScanner(source, 'summary.rs');
const result = scanner.scan();

expect(result.summary).toContain('Summary');
expect(result.directCount).toBeGreaterThanOrEqual(0);
});
});
Loading
Loading