diff --git a/src/extractors/docs/stellar/index.ts b/src/extractors/docs/stellar/index.ts new file mode 100644 index 0000000..04bd10c --- /dev/null +++ b/src/extractors/docs/stellar/index.ts @@ -0,0 +1,49 @@ +export interface DocSummary { + description: string; + annotations: Record; + methods: Record; +} + +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 = {}; + const methods: Record = {}; + + // 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 + }; + } +} diff --git a/src/mapping/stellar/source-maps/index.ts b/src/mapping/stellar/source-maps/index.ts new file mode 100644 index 0000000..8487d5d --- /dev/null +++ b/src/mapping/stellar/source-maps/index.ts @@ -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(); + private findings = new Map(); + + /** + * 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); + } +} diff --git a/src/rules/registry/stellar/index.ts b/src/rules/registry/stellar/index.ts new file mode 100644 index 0000000..4bcb1de --- /dev/null +++ b/src/rules/registry/stellar/index.ts @@ -0,0 +1,53 @@ +export interface RuleMetadata { + id: string; + name: string; + description: string; + severity: 'high' | 'medium' | 'low'; +} + +export class AutoRegistry { + private rules = new Map(); + + /** + * Automatically discovers rules from a given module or directory. + */ + discover(modules: Record): 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()); + } +} diff --git a/src/similarity/stellar/index.ts b/src/similarity/stellar/index.ts new file mode 100644 index 0000000..d1d26d8 --- /dev/null +++ b/src/similarity/stellar/index.ts @@ -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): 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; + } +}