@@ -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 = / t r a i t \s + ( [ a - z A - Z _ ] [ a - z A - Z 0 - 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
2439function 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 ;
0 commit comments