Skip to content

Commit 9dd8562

Browse files
author
Viktor Ryabinin
committed
added traits support for tact files
1 parent e1f8144 commit 9dd8562

File tree

3 files changed

+178
-20
lines changed

3 files changed

+178
-20
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "ton-graph",
33
"displayName": "TON Graph",
44
"description": "Visualize function calls for TON smart contracts",
5-
"version": "0.2.5",
5+
"version": "0.2.6",
66
"publisher": "positiveweb3",
77
"icon": "pic/logo.png",
88
"repository": {

src/parser/tactParser.ts

Lines changed: 175 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,21 @@ function removeCommentsFromCode(code: string): string {
2020
return result;
2121
}
2222

23+
// Function to extract trait names from Tact code
24+
function extractTraitNames(code: string): string[] {
25+
const traitNames: string[] = [];
26+
const traitPattern = /trait\s+([a-zA-Z_][a-zA-Z0-9_]*)/g;
27+
let match;
28+
29+
while ((match = traitPattern.exec(code)) !== null) {
30+
if (match[1]) {
31+
traitNames.push(match[1]);
32+
}
33+
}
34+
35+
return traitNames;
36+
}
37+
2338
// Function to extract all contract names from Tact code
2439
function extractContractNames(code: string): string[] {
2540
const contractNames: string[] = [];
@@ -35,8 +50,42 @@ function extractContractNames(code: string): string[] {
3550
return contractNames;
3651
}
3752

53+
// Map traits to contracts using them
54+
function mapTraitsToContracts(code: string, contractNames: string[], traitNames: string[]): Map<string, string[]> {
55+
const traitToContractMap = new Map<string, string[]>();
56+
57+
// Initialize map for each trait
58+
traitNames.forEach(trait => {
59+
traitToContractMap.set(trait, []);
60+
});
61+
62+
// For each contract, check which traits it uses
63+
for (const contractName of contractNames) {
64+
// Look for "contract ContractName with ... Trait1, Trait2"
65+
const contractWithPattern = new RegExp(`contract\\s+${contractName}\\s+with\\s+([^{]+)`, 'i');
66+
const match = contractWithPattern.exec(code);
67+
68+
if (match) {
69+
const withClause = match[1];
70+
71+
// Check each trait if it's used by this contract
72+
traitNames.forEach(trait => {
73+
// Match the trait name as a word (surrounded by non-word chars or string boundaries)
74+
const traitPattern = new RegExp(`\\b${trait}\\b`, 'i');
75+
if (traitPattern.test(withClause)) {
76+
const contracts = traitToContractMap.get(trait) || [];
77+
contracts.push(contractName);
78+
traitToContractMap.set(trait, contracts);
79+
}
80+
});
81+
}
82+
}
83+
84+
return traitToContractMap;
85+
}
86+
3887
// Find the contract name for a specific position in the code
39-
function findContractForPosition(code: string, position: number, contractNames: string[]): string {
88+
function findContractForPosition(code: string, position: number, contractNames: string[], traitNames: string[], traitToContractMap: Map<string, string[]>): string {
4089
const contractStartPositions: { name: string, startPos: number, endPos: number }[] = [];
4190

4291
// Find all contract start positions
@@ -61,13 +110,49 @@ function findContractForPosition(code: string, position: number, contractNames:
61110
}
62111
}
63112

64-
// Find which contract contains this position
113+
// Find all trait start positions
114+
const traitStartPositions: { name: string, startPos: number, endPos: number }[] = [];
115+
for (const name of traitNames) {
116+
const traitRegex = new RegExp(`trait\\s+${name}[\\s\\S]*?{`, 'g');
117+
let traitMatch;
118+
119+
while ((traitMatch = traitRegex.exec(code)) !== null) {
120+
const startPos = traitMatch.index;
121+
122+
// Find the end of this trait by counting braces
123+
let braceCount = 1;
124+
let endPos = startPos + traitMatch[0].length;
125+
126+
while (braceCount > 0 && endPos < code.length) {
127+
if (code[endPos] === '{') braceCount++;
128+
if (code[endPos] === '}') braceCount--;
129+
endPos++;
130+
}
131+
132+
traitStartPositions.push({ name, startPos, endPos });
133+
}
134+
}
135+
136+
// First check if the position is inside a contract
65137
for (const contract of contractStartPositions) {
66138
if (position > contract.startPos && position < contract.endPos) {
67139
return contract.name;
68140
}
69141
}
70142

143+
// Then check if the position is inside a trait
144+
for (const trait of traitStartPositions) {
145+
if (position > trait.startPos && position < trait.endPos) {
146+
// If this trait is used by only one contract, associate it with that contract
147+
const usingContracts = traitToContractMap.get(trait.name) || [];
148+
if (usingContracts.length === 1) {
149+
return usingContracts[0]; // Associate with the single contract using it
150+
}
151+
// Otherwise, we'll return the trait name and handle the multiple contracts later
152+
return trait.name;
153+
}
154+
}
155+
71156
// Default to first contract if we can't determine
72157
return contractNames.length > 0 ? contractNames[0] : "Unknown";
73158
}
@@ -82,8 +167,11 @@ export async function parseTactContract(code: string): Promise<ContractGraph> {
82167
const normalizedCode = code.replace(/\r\n/g, '\n');
83168
const cleanedCode = removeCommentsFromCode(normalizedCode);
84169

85-
// Extract all contract names
170+
// Extract all contract and trait names
86171
const contractNames = extractContractNames(cleanedCode);
172+
const traitNames = extractTraitNames(cleanedCode);
173+
const traitToContractMap = mapTraitsToContracts(cleanedCode, contractNames, traitNames);
174+
87175
const multipleContracts = contractNames.length > 1;
88176

89177
// Find all function declarations
@@ -92,7 +180,9 @@ export async function parseTactContract(code: string): Promise<ContractGraph> {
92180
params: string,
93181
body: string,
94182
type: string,
95-
contractName: string
183+
contractName: string,
184+
isTrait: boolean,
185+
traitName?: string
96186
}>();
97187

98188
// Find function declarations for each type
@@ -103,7 +193,20 @@ export async function parseTactContract(code: string): Promise<ContractGraph> {
103193
while ((initMatch = initPattern.exec(cleanedCode)) !== null) {
104194
const params = initMatch[1] || '';
105195
const bodyStartPos = initMatch.index + initMatch[0].length;
106-
const contractName = findContractForPosition(cleanedCode, initMatch.index, contractNames);
196+
const contractName = findContractForPosition(cleanedCode, initMatch.index, contractNames, traitNames, traitToContractMap);
197+
198+
// Determine if this function is from a trait
199+
const isTrait = traitNames.includes(contractName);
200+
const traitName = isTrait ? contractName : undefined;
201+
202+
// If it's a trait function, use the contracts that implement it
203+
let effectiveContractName = contractName;
204+
if (isTrait) {
205+
const implementingContracts = traitToContractMap.get(contractName) || [];
206+
if (implementingContracts.length === 1) {
207+
effectiveContractName = implementingContracts[0];
208+
}
209+
}
107210

108211
// Find the matching closing brace
109212
let braceCount = 1;
@@ -120,15 +223,17 @@ export async function parseTactContract(code: string): Promise<ContractGraph> {
120223
const body = cleanedCode.slice(bodyStartPos, bodyEndPos - 1);
121224

122225
// Create function ID with contract prefix if multiple contracts
123-
const functionId = multipleContracts ? `${contractName}::init` : 'init';
226+
const functionId = multipleContracts ? `${effectiveContractName}::init` : 'init';
124227

125228
// Add init function with a special name
126229
functions.set(functionId, {
127230
id: functionId,
128231
params,
129232
body,
130233
type: 'init',
131-
contractName
234+
contractName: effectiveContractName,
235+
isTrait,
236+
traitName
132237
});
133238
}
134239

@@ -138,7 +243,21 @@ export async function parseTactContract(code: string): Promise<ContractGraph> {
138243
while ((receiveMatch = receivePattern.exec(cleanedCode)) !== null) {
139244
// Extract the message type from the parameter
140245
const params = receiveMatch[1] || '';
141-
const contractName = findContractForPosition(cleanedCode, receiveMatch.index, contractNames);
246+
const contractName = findContractForPosition(cleanedCode, receiveMatch.index, contractNames, traitNames, traitToContractMap);
247+
248+
// Determine if this function is from a trait
249+
const isTrait = traitNames.includes(contractName);
250+
const traitName = isTrait ? contractName : undefined;
251+
252+
// If it's a trait function, use the contracts that implement it
253+
let effectiveContractName = contractName;
254+
if (isTrait) {
255+
const implementingContracts = traitToContractMap.get(contractName) || [];
256+
if (implementingContracts.length === 1) {
257+
effectiveContractName = implementingContracts[0];
258+
}
259+
}
260+
142261
let funcBaseName = 'receive';
143262

144263
// Check for string literal patterns: receive("StringLiteral")
@@ -162,7 +281,7 @@ export async function parseTactContract(code: string): Promise<ContractGraph> {
162281
}
163282

164283
// Create function ID with contract prefix if multiple contracts
165-
const functionId = multipleContracts ? `${contractName}::${funcBaseName}` : funcBaseName;
284+
const functionId = multipleContracts ? `${effectiveContractName}::${funcBaseName}` : funcBaseName;
166285

167286
const bodyStartPos = receiveMatch.index + receiveMatch[0].length;
168287

@@ -186,7 +305,9 @@ export async function parseTactContract(code: string): Promise<ContractGraph> {
186305
params,
187306
body,
188307
type: 'receive',
189-
contractName
308+
contractName: effectiveContractName,
309+
isTrait,
310+
traitName
190311
});
191312
}
192313

@@ -196,7 +317,20 @@ export async function parseTactContract(code: string): Promise<ContractGraph> {
196317
while ((getMatch = getPattern.exec(cleanedCode)) !== null) {
197318
const funcName = getMatch[1];
198319
const params = getMatch[2] || '';
199-
const contractName = findContractForPosition(cleanedCode, getMatch.index, contractNames);
320+
const contractName = findContractForPosition(cleanedCode, getMatch.index, contractNames, traitNames, traitToContractMap);
321+
322+
// Determine if this function is from a trait
323+
const isTrait = traitNames.includes(contractName);
324+
const traitName = isTrait ? contractName : undefined;
325+
326+
// If it's a trait function, use the contracts that implement it
327+
let effectiveContractName = contractName;
328+
if (isTrait) {
329+
const implementingContracts = traitToContractMap.get(contractName) || [];
330+
if (implementingContracts.length === 1) {
331+
effectiveContractName = implementingContracts[0];
332+
}
333+
}
200334

201335
// Skip built-in functions
202336
if (BUILT_IN_FUNCTIONS.has(funcName)) {
@@ -221,14 +355,16 @@ export async function parseTactContract(code: string): Promise<ContractGraph> {
221355

222356
// Add get function - ensure ID doesn't have spaces
223357
const getBaseId = `get_fun_${funcName}`;
224-
const functionId = multipleContracts ? `${contractName}::${getBaseId}` : getBaseId;
358+
const functionId = multipleContracts ? `${effectiveContractName}::${getBaseId}` : getBaseId;
225359

226360
functions.set(functionId, {
227361
id: functionId,
228362
params,
229363
body,
230364
type: 'get_fun',
231-
contractName
365+
contractName: effectiveContractName,
366+
isTrait,
367+
traitName
232368
});
233369
}
234370

@@ -238,7 +374,20 @@ export async function parseTactContract(code: string): Promise<ContractGraph> {
238374
while ((funMatch = funPattern.exec(cleanedCode)) !== null) {
239375
const funcName = funMatch[1];
240376
const params = funMatch[2] || '';
241-
const contractName = findContractForPosition(cleanedCode, funMatch.index, contractNames);
377+
const contractName = findContractForPosition(cleanedCode, funMatch.index, contractNames, traitNames, traitToContractMap);
378+
379+
// Determine if this function is from a trait
380+
const isTrait = traitNames.includes(contractName);
381+
const traitName = isTrait ? contractName : undefined;
382+
383+
// If it's a trait function, use the contracts that implement it
384+
let effectiveContractName = contractName;
385+
if (isTrait) {
386+
const implementingContracts = traitToContractMap.get(contractName) || [];
387+
if (implementingContracts.length === 1) {
388+
effectiveContractName = implementingContracts[0];
389+
}
390+
}
242391

243392
// Skip built-in functions
244393
if (BUILT_IN_FUNCTIONS.has(funcName)) {
@@ -262,14 +411,16 @@ export async function parseTactContract(code: string): Promise<ContractGraph> {
262411
const body = cleanedCode.slice(bodyStartPos, bodyEndPos - 1);
263412

264413
// Add regular function
265-
const functionId = multipleContracts ? `${contractName}::${funcName}` : funcName;
414+
const functionId = multipleContracts ? `${effectiveContractName}::${funcName}` : funcName;
266415

267416
functions.set(functionId, {
268417
id: functionId,
269418
params,
270419
body,
271420
type: 'fun',
272-
contractName
421+
contractName: effectiveContractName,
422+
isTrait,
423+
traitName
273424
});
274425
}
275426

@@ -302,7 +453,10 @@ export async function parseTactContract(code: string): Promise<ContractGraph> {
302453
type: 'function',
303454
contractName: func.contractName,
304455
parameters: func.params.split(',').map(p => p.trim()).filter(p => p),
305-
functionType: func.type.replace(/\s+/g, '_') as any
456+
functionType: func.type.replace(/\s+/g, '_') as any,
457+
// Add trait info to the node if needed
458+
isTrait: func.isTrait,
459+
traitName: func.traitName
306460
};
307461
graph.nodes.push(node);
308462
});
@@ -336,14 +490,16 @@ export async function parseTactContract(code: string): Promise<ContractGraph> {
336490
}
337491

338492
// Check for direct function calls (functionName())
339-
const pattern = new RegExp(`\\b${baseCalledFuncName}\\s*\\(`, 'g');
493+
const pattern = new RegExp(`\\b${baseCalledFuncName.replace(/_/g, '_')}\\s*\\(`, 'g');
340494
let match;
341495

342496
// Only add edge if the called function is in the same contract or explicitly qualified
497+
// Trait functions are considered part of the contract that implements them
343498
const isSameContract = currentContractName === calledContractName;
344499
const isExplicitCall = funcBody.includes(`${calledContractName}.${baseCalledFuncName}`);
500+
const isTraitFunction = calledFunc.isTrait;
345501

346-
if (isSameContract || isExplicitCall) {
502+
if (isSameContract || isExplicitCall || isTraitFunction) {
347503
while ((match = pattern.exec(funcBody)) !== null) {
348504
// Skip if it's an explicitly qualified call to another contract's function
349505
const matchPos = match.index;

src/types/graph.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export interface GraphNode {
55
contractName: string;
66
parameters?: string[];
77
functionType?: string;
8+
isTrait?: boolean;
9+
traitName?: string;
810
}
911

1012
export interface GraphEdge {

0 commit comments

Comments
 (0)