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
49 changes: 49 additions & 0 deletions src/extractors/docs/stellar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export interface DocSummary {
description: string;
annotations: Record<string, string>;
methods: Record<string, string>;
}

export class DocumentationExtractor {
/**
* Parses comments and annotations from Soroban contract source code.
* Generates documentation summaries and exports structured outputs.
*/
extract(sourceCode: string): DocSummary {
const annotations: Record<string, string> = {};
const methods: Record<string, string> = {};

// Simple parsing for rust doc comments (///)
const lines = sourceCode.split('\n');
let currentDoc: string[] = [];

for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('///')) {
currentDoc.push(trimmed.substring(3).trim());
} else if (trimmed.startsWith('#[')) {
// Annotation
const match = trimmed.match(/#\[(.*?)\]/);
if (match) {
annotations[match[1]] = currentDoc.join(' ');
currentDoc = [];
}
} else if (trimmed.startsWith('pub fn')) {
// Method
const match = trimmed.match(/pub fn ([a-zA-Z0-9_]+)/);
if (match) {
methods[match[1]] = currentDoc.join(' ');
currentDoc = [];
}
} else if (trimmed !== '') {
currentDoc = [];
}
}

return {
description: "Extracted Soroban documentation",
annotations,
methods
};
}
}
41 changes: 41 additions & 0 deletions src/mapping/stellar/source-maps/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export interface SourceLocation {
line: number;
column: number;
file?: string;
}

export interface Finding {
id: string;
description: string;
location?: SourceLocation;
}

export class SourceMappingEngine {
private sourceMap = new Map<string, SourceLocation>();
private findings = new Map<string, Finding>();

/**
* Resolves source locations from AST nodes or elements.
*/
resolveLocation(nodeId: string, line: number, column: number, file?: string): SourceLocation {
const loc = { line, column, file };
this.sourceMap.set(nodeId, loc);
return loc;
}

/**
* Links findings directly to code coordinates.
*/
linkFinding(finding: Finding, nodeId: string): Finding {
const loc = this.sourceMap.get(nodeId);
if (loc) {
finding.location = loc;
}
this.findings.set(finding.id, finding);
return finding;
}

getFinding(findingId: string): Finding | undefined {
return this.findings.get(findingId);
}
}
53 changes: 53 additions & 0 deletions src/rules/registry/stellar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
export interface RuleMetadata {
id: string;
name: string;
description: string;
severity: 'high' | 'medium' | 'low';
}

export class AutoRegistry {
private rules = new Map<string, RuleMetadata>();

/**
* Automatically discovers rules from a given module or directory.
*/
discover(modules: Record<string, any>): void {
for (const [key, exported] of Object.entries(modules)) {
if (exported && typeof exported === 'object' && 'metadata' in exported) {
this.register(exported.metadata);
}
}
}

/**
* Validates rule metadata to ensure it meets requirements.
*/
validateMetadata(metadata: any): metadata is RuleMetadata {
return (
metadata &&
typeof metadata.id === 'string' &&
typeof metadata.name === 'string' &&
typeof metadata.description === 'string' &&
['high', 'medium', 'low'].includes(metadata.severity)
);
}

/**
* Registers rules dynamically.
*/
register(metadata: any): boolean {
if (this.validateMetadata(metadata)) {
this.rules.set(metadata.id, metadata);
return true;
}
return false;
}

getRule(id: string): RuleMetadata | undefined {
return this.rules.get(id);
}

getAllRules(): RuleMetadata[] {
return Array.from(this.rules.values());
}
}
47 changes: 47 additions & 0 deletions src/similarity/stellar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
export interface SimilarityResult {
score: number;
isNearDuplicate: boolean;
}

export class SimilarityEngine {
/**
* Compares contract structures and generates similarity scores.
* Detects near-duplicates based on a threshold.
*/
compare(contractA: string, contractB: string): SimilarityResult {
// Simple tokenization by whitespace and non-alphanumeric chars
const tokensA = new Set(contractA.split(/[\s\W]+/).filter(t => t.length > 0));
const tokensB = new Set(contractB.split(/[\s\W]+/).filter(t => t.length > 0));

let intersection = 0;
for (const t of tokensA) {
if (tokensB.has(t)) {
intersection++;
}
}

const union = tokensA.size + tokensB.size - intersection;
const score = union === 0 ? 1 : intersection / union;

return {
score,
isNearDuplicate: score > 0.85
};
}

detectNearDuplicates(contracts: Record<string, string>): Array<[string, string]> {
const duplicates: Array<[string, string]> = [];
const keys = Object.keys(contracts);

for (let i = 0; i < keys.length; i++) {
for (let j = i + 1; j < keys.length; j++) {
const result = this.compare(contracts[keys[i]], contracts[keys[j]]);
if (result.isNearDuplicate) {
duplicates.push([keys[i], keys[j]]);
}
}
}

return duplicates;
}
}
Loading