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
126 changes: 126 additions & 0 deletions packages/rules/soroban/src/collections/map-ops-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* Rule: Detect Inefficient Soroban Map Operations
*
* Repeated lookups and unnecessary map mutations increase contract resource
* consumption in Soroban. Each host-object map access is metered.
*
* Issue: #768
*/

export interface MapOpsWarning {
line: number;
column?: number;
patternType: 'repeated-lookup' | 'unnecessary-update' | 'avoidable-traversal';
key?: string;
message: string;
suggestion: string;
}

export class SorobanMapOpsCheckRule {
public static readonly RULE_ID = 'soroban-inefficient-map-ops';

/** Map get patterns – each lookup is a metered host call. */
private static readonly LOOKUP_PATTERNS = [
'.get(',
'.contains_key(',
'.try_get(',
];

/** Mutation patterns that write the same entry without guard. */
private static readonly UPDATE_PATTERNS = [
'.set(',
'.insert(',
'.put(',
];

/** Full traversal of a Map is expensive due to host-object iteration cost. */
private static readonly TRAVERSAL_PATTERNS = [
'.iter()',
'.keys()',
'.values()',
'.into_iter()',
];

public analyze(sourceCode: string): MapOpsWarning[] {
const warnings: MapOpsWarning[] = [];
const lines = sourceCode.split('\n');

// Track lookup keys per function to detect repeated lookups for the same key
const lookupKeysPerFunction: Map<string, Map<string, number>> = new Map();
let currentFunction = '<global>';

for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const lineNum = i + 1;

// Track function scope
const fnMatch = line.match(/\bfn\s+([a-zA-Z0-9_]+)\s*\(/);
if (fnMatch) {
currentFunction = fnMatch[1];
if (!lookupKeysPerFunction.has(currentFunction)) {
lookupKeysPerFunction.set(currentFunction, new Map());
}
}

// 1. Repeated lookup for the same key
for (const pattern of SorobanMapOpsCheckRule.LOOKUP_PATTERNS) {
if (line.includes(pattern)) {
// Extract the key argument (rough heuristic: text inside first parens)
const keyMatch = line.match(/\.(?:get|contains_key|try_get)\(([^)]+)\)/);
const key = keyMatch ? keyMatch[1].trim() : 'unknown';

const fnKeys = lookupKeysPerFunction.get(currentFunction) ?? new Map<string, number>();
const count = (fnKeys.get(key) ?? 0) + 1;
fnKeys.set(key, count);
lookupKeysPerFunction.set(currentFunction, fnKeys);

if (count > 1) {
warnings.push({
line: lineNum,
patternType: 'repeated-lookup',
key,
message: `Key '${key}' is looked up more than once in function '${currentFunction}'. Each map lookup is a metered host call.`,
suggestion: `Cache the result of the first lookup in a local variable and reuse it instead of calling .get(${key}) again.`,
});
}
break;
}
}

// 2. Unnecessary update – set() called without a prior guard/condition check
for (const pattern of SorobanMapOpsCheckRule.UPDATE_PATTERNS) {
if (line.includes(pattern)) {
// Warn if the set/insert is NOT preceded by a contains_key check on the same line
// or the immediately preceding lines (simple heuristic).
const prevLines = lines.slice(Math.max(0, i - 3), i).join('\n');
if (!prevLines.includes('contains_key') && !prevLines.includes('if ') && !prevLines.includes('match ')) {
warnings.push({
line: lineNum,
patternType: 'unnecessary-update',
message: `Unconditional map update ('${pattern.trim()}') without a prior existence check may overwrite existing values unnecessarily, wasting metered write budget.`,
suggestion:
'Guard the update with a .contains_key() check or use an entry-based pattern to avoid redundant writes.',
});
}
break;
}
}

// 3. Avoidable full traversal
for (const pattern of SorobanMapOpsCheckRule.TRAVERSAL_PATTERNS) {
if (line.includes(pattern)) {
warnings.push({
line: lineNum,
patternType: 'avoidable-traversal',
message: `Full map traversal ('${pattern.trim()}') in function '${currentFunction}' is expensive. Iterating over a Soroban Map enumerates all host objects.`,
suggestion:
'Access only the specific keys you need via direct .get() calls, or restructure data to avoid full traversal.',
});
break;
}
}
}

return warnings;
}
}
123 changes: 123 additions & 0 deletions packages/rules/soroban/src/collections/vector-ops-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
* Rule: Detect Inefficient Soroban Vector Operations
*
* Repeated vector traversal, front insertion/removal, and unnecessary copying
* increase metered CPU and memory budget in Soroban contracts.
*
* Issue: #767
*/

export interface VectorOpsWarning {
line: number;
column?: number;
patternType: 'repeated-traversal' | 'inefficient-insertion' | 'unnecessary-copy';
symbol?: string;
message: string;
suggestion: string;
}

export class SorobanVectorOpsCheckRule {
public static readonly RULE_ID = 'soroban-inefficient-vector-ops';

/**
* Patterns that indicate a full vector traversal (O(n) scan).
* In Soroban, each host-object access is metered.
*/
private static readonly TRAVERSAL_PATTERNS = [
'.iter()',
'.iter_mut()',
'.into_iter()',
'.for_each(',
'for ',
];

/**
* Insertion/removal at the front is O(n) for Vec – push_back / push_front
* differ in cost on Soroban host vectors.
*/
private static readonly INEFFICIENT_INSERT_PATTERNS = [
'.push_front(',
'.insert(0,',
'.remove(0)',
];

/** Unnecessary clone / copy of an entire vector. */
private static readonly COPY_PATTERNS = [
'.clone()',
'Vec::from(',
'.to_vec()',
];

public analyze(sourceCode: string): VectorOpsWarning[] {
const warnings: VectorOpsWarning[] = [];
const lines = sourceCode.split('\n');

// Track traversal counts per function to detect repeated traversals
const traversalCountPerFunction: Map<string, number> = new Map();
let currentFunction = '<global>';

for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const lineNum = i + 1;

// Detect function boundaries (rough heuristic)
const fnMatch = line.match(/\bfn\s+([a-zA-Z0-9_]+)\s*\(/);
if (fnMatch) {
currentFunction = fnMatch[1];
if (!traversalCountPerFunction.has(currentFunction)) {
traversalCountPerFunction.set(currentFunction, 0);
}
}

// 1. Repeated vector traversal
for (const pattern of SorobanVectorOpsCheckRule.TRAVERSAL_PATTERNS) {
if (line.includes(pattern)) {
const count = (traversalCountPerFunction.get(currentFunction) ?? 0) + 1;
traversalCountPerFunction.set(currentFunction, count);

if (count > 1) {
warnings.push({
line: lineNum,
patternType: 'repeated-traversal',
symbol: currentFunction,
message: `Function '${currentFunction}' performs multiple vector traversals. Each traversal consumes metered Soroban CPU budget.`,
suggestion:
'Combine traversals into a single pass or cache intermediate results in a local variable.',
});
}
break;
}
}

// 2. Inefficient insertion / removal
for (const pattern of SorobanVectorOpsCheckRule.INEFFICIENT_INSERT_PATTERNS) {
if (line.includes(pattern)) {
warnings.push({
line: lineNum,
patternType: 'inefficient-insertion',
message: `Front insertion/removal ('${pattern.trim()}') on a vector is O(n) and shifts all elements, wasting metered CPU.`,
suggestion:
'Use push_back() for appending, or consider a different data structure (e.g., a Map) if random access is needed.',
});
break;
}
}

// 3. Unnecessary copy
for (const pattern of SorobanVectorOpsCheckRule.COPY_PATTERNS) {
if (line.includes(pattern) && line.includes('Vec')) {
warnings.push({
line: lineNum,
patternType: 'unnecessary-copy',
message: `Unnecessary vector copy detected ('${pattern.trim()}'). Cloning a Soroban Vec allocates a new host object and doubles memory budget consumption.`,
suggestion:
'Pass a reference or slice instead of cloning the entire vector where ownership is not strictly required.',
});
break;
}
}
}

return warnings;
}
}
2 changes: 2 additions & 0 deletions packages/rules/soroban/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export * from './storage-rent-check';
export * from './analyzer/wasm-inspector';
export * from './collections/vector-ops-check';
export * from './collections/map-ops-check';
export * from './analyzer/callgraph-analyzer';
export * from './analyzer/serialization-analyzer';
export * from './calls';
Expand Down
Loading
Loading