extraction.test.ts 479 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460746174627463746474657466746774687469747074717472747374747475747674777478747974807481748274837484748574867487748874897490749174927493749474957496749774987499750075017502750375047505750675077508750975107511751275137514751575167517751875197520752175227523752475257526752775287529753075317532753375347535753675377538753975407541754275437544754575467547754875497550755175527553755475557556755775587559756075617562756375647565756675677568756975707571757275737574757575767577757875797580758175827583758475857586758775887589759075917592759375947595759675977598759976007601760276037604760576067607760876097610761176127613761476157616761776187619762076217622762376247625762676277628762976307631763276337634763576367637763876397640764176427643764476457646764776487649765076517652765376547655765676577658765976607661766276637664766576667667766876697670767176727673767476757676767776787679768076817682768376847685768676877688768976907691769276937694769576967697769876997700770177027703770477057706770777087709771077117712771377147715771677177718771977207721772277237724772577267727772877297730773177327733773477357736773777387739774077417742774377447745774677477748774977507751775277537754775577567757775877597760776177627763776477657766776777687769777077717772777377747775777677777778777977807781778277837784778577867787778877897790779177927793779477957796779777987799780078017802780378047805780678077808780978107811781278137814781578167817781878197820782178227823782478257826782778287829783078317832783378347835783678377838783978407841784278437844784578467847784878497850785178527853785478557856785778587859786078617862786378647865786678677868786978707871787278737874787578767877787878797880788178827883788478857886788778887889789078917892789378947895789678977898789979007901790279037904790579067907790879097910791179127913791479157916791779187919792079217922792379247925792679277928792979307931793279337934793579367937793879397940794179427943794479457946794779487949795079517952795379547955795679577958795979607961796279637964796579667967796879697970797179727973797479757976797779787979798079817982798379847985798679877988798979907991799279937994799579967997799879998000800180028003800480058006800780088009801080118012801380148015801680178018801980208021802280238024802580268027802880298030803180328033803480358036803780388039804080418042804380448045804680478048804980508051805280538054805580568057805880598060806180628063806480658066806780688069807080718072807380748075807680778078807980808081808280838084808580868087808880898090809180928093809480958096809780988099810081018102810381048105810681078108810981108111811281138114811581168117811881198120812181228123812481258126812781288129813081318132813381348135813681378138813981408141814281438144814581468147814881498150815181528153815481558156815781588159816081618162816381648165816681678168816981708171817281738174817581768177817881798180818181828183818481858186818781888189819081918192819381948195819681978198819982008201820282038204820582068207820882098210821182128213821482158216821782188219822082218222822382248225822682278228822982308231823282338234823582368237823882398240824182428243824482458246824782488249825082518252825382548255825682578258825982608261826282638264826582668267826882698270827182728273827482758276827782788279828082818282828382848285828682878288828982908291829282938294829582968297829882998300830183028303830483058306830783088309831083118312831383148315831683178318831983208321832283238324832583268327832883298330833183328333833483358336833783388339834083418342834383448345834683478348834983508351835283538354835583568357835883598360836183628363836483658366836783688369837083718372837383748375837683778378837983808381838283838384838583868387838883898390839183928393839483958396839783988399840084018402840384048405840684078408840984108411841284138414841584168417841884198420842184228423842484258426842784288429843084318432843384348435843684378438843984408441844284438444844584468447844884498450845184528453845484558456845784588459846084618462846384648465846684678468846984708471847284738474847584768477847884798480848184828483848484858486848784888489849084918492849384948495849684978498849985008501850285038504850585068507850885098510851185128513851485158516851785188519852085218522852385248525852685278528852985308531853285338534853585368537853885398540854185428543854485458546854785488549855085518552855385548555855685578558855985608561856285638564856585668567856885698570857185728573857485758576857785788579858085818582858385848585858685878588858985908591859285938594859585968597859885998600860186028603860486058606860786088609861086118612861386148615861686178618861986208621862286238624862586268627862886298630863186328633863486358636863786388639864086418642864386448645864686478648864986508651865286538654865586568657865886598660866186628663866486658666866786688669867086718672867386748675867686778678867986808681868286838684868586868687868886898690869186928693869486958696869786988699870087018702870387048705870687078708870987108711871287138714871587168717871887198720872187228723872487258726872787288729873087318732873387348735873687378738873987408741874287438744874587468747874887498750875187528753875487558756875787588759876087618762876387648765876687678768876987708771877287738774877587768777877887798780878187828783878487858786878787888789879087918792879387948795879687978798879988008801880288038804880588068807880888098810881188128813881488158816881788188819882088218822882388248825882688278828882988308831883288338834883588368837883888398840884188428843884488458846884788488849885088518852885388548855885688578858885988608861886288638864886588668867886888698870887188728873887488758876887788788879888088818882888388848885888688878888888988908891889288938894889588968897889888998900890189028903890489058906890789088909891089118912891389148915891689178918891989208921892289238924892589268927892889298930893189328933893489358936893789388939894089418942894389448945894689478948894989508951895289538954895589568957895889598960896189628963896489658966896789688969897089718972897389748975897689778978897989808981898289838984898589868987898889898990899189928993899489958996899789988999900090019002900390049005900690079008900990109011901290139014901590169017901890199020902190229023902490259026902790289029903090319032903390349035903690379038903990409041904290439044904590469047904890499050905190529053905490559056905790589059906090619062906390649065906690679068906990709071907290739074907590769077907890799080908190829083908490859086908790889089909090919092909390949095909690979098909991009101910291039104910591069107910891099110911191129113911491159116911791189119912091219122912391249125912691279128912991309131913291339134913591369137913891399140914191429143914491459146914791489149915091519152915391549155915691579158915991609161916291639164916591669167916891699170917191729173917491759176917791789179918091819182918391849185918691879188918991909191919291939194919591969197919891999200920192029203920492059206920792089209921092119212921392149215921692179218921992209221922292239224922592269227922892299230923192329233923492359236923792389239924092419242924392449245924692479248924992509251925292539254925592569257925892599260926192629263926492659266926792689269927092719272927392749275927692779278927992809281928292839284928592869287928892899290929192929293929492959296929792989299930093019302930393049305930693079308930993109311931293139314931593169317931893199320932193229323932493259326932793289329933093319332933393349335933693379338933993409341934293439344934593469347934893499350935193529353935493559356935793589359936093619362936393649365936693679368936993709371937293739374937593769377937893799380938193829383938493859386938793889389939093919392939393949395939693979398939994009401940294039404940594069407940894099410941194129413941494159416941794189419942094219422942394249425942694279428942994309431943294339434943594369437943894399440944194429443944494459446944794489449945094519452945394549455945694579458945994609461946294639464946594669467946894699470947194729473947494759476947794789479948094819482948394849485948694879488948994909491949294939494949594969497949894999500950195029503950495059506950795089509951095119512951395149515951695179518951995209521952295239524952595269527952895299530953195329533953495359536953795389539954095419542954395449545954695479548954995509551955295539554955595569557955895599560956195629563956495659566956795689569957095719572957395749575957695779578957995809581958295839584958595869587958895899590959195929593959495959596959795989599960096019602960396049605960696079608960996109611961296139614961596169617961896199620962196229623962496259626962796289629963096319632963396349635963696379638963996409641964296439644964596469647964896499650965196529653965496559656965796589659966096619662966396649665966696679668966996709671967296739674967596769677967896799680968196829683968496859686968796889689969096919692969396949695969696979698969997009701970297039704970597069707970897099710971197129713971497159716971797189719972097219722972397249725972697279728972997309731973297339734973597369737973897399740974197429743974497459746974797489749975097519752975397549755975697579758975997609761976297639764976597669767976897699770977197729773977497759776977797789779978097819782978397849785978697879788978997909791979297939794979597969797979897999800980198029803980498059806980798089809981098119812981398149815981698179818981998209821982298239824982598269827982898299830983198329833983498359836983798389839984098419842984398449845984698479848984998509851985298539854985598569857985898599860986198629863986498659866986798689869987098719872987398749875987698779878987998809881988298839884988598869887988898899890989198929893989498959896989798989899990099019902990399049905990699079908990999109911991299139914991599169917991899199920992199229923992499259926992799289929993099319932993399349935993699379938993999409941994299439944994599469947994899499950995199529953995499559956995799589959996099619962996399649965996699679968996999709971997299739974997599769977997899799980998199829983998499859986998799889989999099919992999399949995999699979998999910000100011000210003100041000510006100071000810009100101001110012100131001410015100161001710018100191002010021100221002310024100251002610027100281002910030100311003210033100341003510036100371003810039100401004110042100431004410045100461004710048100491005010051100521005310054100551005610057100581005910060100611006210063100641006510066100671006810069100701007110072100731007410075100761007710078100791008010081100821008310084100851008610087100881008910090100911009210093100941009510096100971009810099101001010110102101031010410105101061010710108101091011010111101121011310114101151011610117101181011910120101211012210123101241012510126101271012810129101301013110132101331013410135101361013710138101391014010141101421014310144101451014610147101481014910150101511015210153101541015510156101571015810159101601016110162101631016410165101661016710168101691017010171101721017310174101751017610177101781017910180101811018210183101841018510186101871018810189101901019110192101931019410195101961019710198101991020010201102021020310204102051020610207102081020910210102111021210213102141021510216102171021810219102201022110222102231022410225102261022710228102291023010231102321023310234102351023610237102381023910240102411024210243102441024510246102471024810249102501025110252102531025410255102561025710258102591026010261102621026310264102651026610267102681026910270102711027210273102741027510276102771027810279102801028110282102831028410285102861028710288102891029010291102921029310294102951029610297102981029910300103011030210303103041030510306103071030810309103101031110312103131031410315103161031710318103191032010321103221032310324103251032610327103281032910330103311033210333103341033510336103371033810339103401034110342103431034410345103461034710348103491035010351103521035310354103551035610357103581035910360103611036210363103641036510366103671036810369103701037110372103731037410375103761037710378103791038010381103821038310384103851038610387103881038910390103911039210393103941039510396103971039810399104001040110402104031040410405104061040710408104091041010411104121041310414104151041610417104181041910420104211042210423104241042510426104271042810429104301043110432104331043410435104361043710438104391044010441104421044310444104451044610447104481044910450104511045210453104541045510456104571045810459104601046110462104631046410465104661046710468104691047010471104721047310474104751047610477104781047910480104811048210483104841048510486104871048810489104901049110492104931049410495104961049710498104991050010501105021050310504105051050610507105081050910510105111051210513105141051510516105171051810519105201052110522105231052410525105261052710528105291053010531105321053310534105351053610537105381053910540105411054210543105441054510546105471054810549105501055110552105531055410555105561055710558105591056010561105621056310564105651056610567105681056910570105711057210573105741057510576105771057810579105801058110582105831058410585105861058710588105891059010591105921059310594105951059610597105981059910600106011060210603106041060510606106071060810609106101061110612106131061410615106161061710618106191062010621106221062310624106251062610627106281062910630106311063210633106341063510636106371063810639106401064110642106431064410645106461064710648106491065010651106521065310654106551065610657106581065910660106611066210663106641066510666106671066810669106701067110672106731067410675106761067710678106791068010681106821068310684106851068610687106881068910690106911069210693106941069510696106971069810699107001070110702107031070410705107061070710708107091071010711107121071310714107151071610717107181071910720107211072210723107241072510726107271072810729107301073110732107331073410735107361073710738107391074010741107421074310744107451074610747107481074910750107511075210753107541075510756107571075810759107601076110762107631076410765107661076710768107691077010771107721077310774107751077610777107781077910780107811078210783107841078510786107871078810789107901079110792107931079410795107961079710798107991080010801108021080310804108051080610807108081080910810108111081210813108141081510816108171081810819108201082110822108231082410825108261082710828108291083010831108321083310834108351083610837108381083910840108411084210843108441084510846108471084810849108501085110852108531085410855108561085710858108591086010861108621086310864108651086610867108681086910870108711087210873108741087510876108771087810879108801088110882108831088410885108861088710888108891089010891108921089310894108951089610897108981089910900109011090210903109041090510906109071090810909109101091110912109131091410915109161091710918109191092010921109221092310924109251092610927109281092910930109311093210933109341093510936109371093810939109401094110942109431094410945109461094710948109491095010951109521095310954109551095610957109581095910960109611096210963109641096510966109671096810969109701097110972109731097410975109761097710978109791098010981109821098310984109851098610987109881098910990109911099210993109941099510996109971099810999110001100111002110031100411005110061100711008110091101011011110121101311014110151101611017110181101911020110211102211023110241102511026110271102811029110301103111032110331103411035110361103711038110391104011041110421104311044110451104611047110481104911050110511105211053110541105511056110571105811059110601106111062110631106411065110661106711068110691107011071110721107311074110751107611077110781107911080110811108211083110841108511086110871108811089110901109111092110931109411095110961109711098110991110011101111021110311104111051110611107111081110911110111111111211113111141111511116111171111811119111201112111122111231112411125111261112711128111291113011131111321113311134111351113611137111381113911140111411114211143111441114511146111471114811149111501115111152111531115411155111561115711158111591116011161111621116311164111651116611167111681116911170111711117211173111741117511176111771117811179111801118111182111831118411185111861118711188111891119011191111921119311194111951119611197111981119911200112011120211203112041120511206112071120811209112101121111212112131121411215112161121711218112191122011221112221122311224112251122611227112281122911230112311123211233112341123511236112371123811239112401124111242112431124411245112461124711248112491125011251112521125311254112551125611257112581125911260112611126211263112641126511266112671126811269112701127111272112731127411275112761127711278112791128011281112821128311284112851128611287112881128911290112911129211293112941129511296112971129811299113001130111302113031130411305113061130711308113091131011311113121131311314113151131611317113181131911320113211132211323113241132511326113271132811329113301133111332113331133411335113361133711338113391134011341113421134311344113451134611347113481134911350113511135211353113541135511356113571135811359113601136111362113631136411365113661136711368113691137011371113721137311374113751137611377113781137911380113811138211383113841138511386113871138811389113901139111392113931139411395113961139711398113991140011401114021140311404114051140611407114081140911410114111141211413114141141511416114171141811419114201142111422114231142411425114261142711428114291143011431114321143311434114351143611437114381143911440114411144211443114441144511446114471144811449114501145111452114531145411455114561145711458114591146011461114621146311464114651146611467114681146911470114711147211473114741147511476114771147811479114801148111482114831148411485114861148711488114891149011491114921149311494114951149611497114981149911500115011150211503115041150511506115071150811509115101151111512115131151411515115161151711518115191152011521115221152311524115251152611527115281152911530115311153211533115341153511536115371153811539115401154111542115431154411545115461154711548115491155011551115521155311554115551155611557115581155911560115611156211563115641156511566115671156811569115701157111572115731157411575115761157711578115791158011581115821158311584115851158611587115881158911590115911159211593115941159511596115971159811599116001160111602116031160411605116061160711608116091161011611116121161311614116151161611617116181161911620116211162211623116241162511626116271162811629116301163111632116331163411635116361163711638116391164011641116421164311644116451164611647116481164911650116511165211653116541165511656116571165811659116601166111662116631166411665116661166711668116691167011671116721167311674116751167611677116781167911680116811168211683116841168511686116871168811689116901169111692116931169411695116961169711698116991170011701117021170311704117051170611707117081170911710117111171211713117141171511716117171171811719117201172111722117231172411725117261172711728117291173011731117321173311734117351173611737117381173911740117411174211743117441174511746117471174811749117501175111752117531175411755117561175711758117591176011761117621176311764117651176611767117681176911770117711177211773117741177511776117771177811779117801178111782117831178411785117861178711788117891179011791117921179311794117951179611797117981179911800118011180211803118041180511806118071180811809118101181111812118131181411815118161181711818118191182011821118221182311824118251182611827118281182911830118311183211833118341183511836118371183811839118401184111842118431184411845118461184711848118491185011851118521185311854118551185611857118581185911860118611186211863118641186511866118671186811869118701187111872118731187411875118761187711878118791188011881118821188311884118851188611887118881188911890118911189211893118941189511896118971189811899119001190111902119031190411905119061190711908119091191011911119121191311914119151191611917119181191911920119211192211923119241192511926119271192811929119301193111932119331193411935119361193711938119391194011941119421194311944119451194611947119481194911950119511195211953119541195511956119571195811959119601196111962119631196411965119661196711968119691197011971119721197311974119751197611977119781197911980119811198211983119841198511986119871198811989119901199111992119931199411995119961199711998119991200012001120021200312004120051200612007120081200912010120111201212013120141201512016120171201812019120201202112022120231202412025120261202712028120291203012031120321203312034120351203612037120381203912040120411204212043120441204512046120471204812049120501205112052120531205412055120561205712058120591206012061120621206312064120651206612067120681206912070120711207212073120741207512076120771207812079120801208112082120831208412085120861208712088120891209012091120921209312094120951209612097120981209912100121011210212103121041210512106121071210812109121101211112112121131211412115121161211712118121191212012121121221212312124121251212612127121281212912130121311213212133121341213512136121371213812139121401214112142121431214412145121461214712148121491215012151121521215312154121551215612157121581215912160121611216212163121641216512166121671216812169121701217112172121731217412175121761217712178121791218012181121821218312184121851218612187121881218912190121911219212193121941219512196121971219812199122001220112202122031220412205122061220712208122091221012211122121221312214122151221612217122181221912220122211222212223122241222512226122271222812229122301223112232122331223412235122361223712238122391224012241122421224312244122451224612247122481224912250122511225212253122541225512256122571225812259122601226112262122631226412265122661226712268122691227012271122721227312274122751227612277122781227912280122811228212283122841228512286122871228812289122901229112292122931229412295122961229712298122991230012301123021230312304123051230612307123081230912310123111231212313123141231512316123171231812319123201232112322123231232412325123261232712328123291233012331123321233312334123351233612337123381233912340123411234212343123441234512346123471234812349123501235112352123531235412355123561235712358123591236012361123621236312364123651236612367123681236912370123711237212373123741237512376123771237812379123801238112382123831238412385123861238712388123891239012391123921239312394123951239612397123981239912400124011240212403124041240512406124071240812409124101241112412124131241412415124161241712418124191242012421124221242312424124251242612427124281242912430124311243212433124341243512436124371243812439124401244112442124431244412445124461244712448124491245012451124521245312454124551245612457
  1. /**
  2. * Extraction Tests
  3. *
  4. * Tests for the tree-sitter extraction system.
  5. */
  6. import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
  7. import * as fs from 'fs';
  8. import * as path from 'path';
  9. import * as os from 'os';
  10. import { CodeGraph } from '../src';
  11. import { extractFromSource, scanDirectory, buildDefaultIgnore, discoverEmbeddedRepoRoots, buildScopeIgnore } from '../src/extraction';
  12. import { detectLanguage, isLanguageSupported, getSupportedLanguages, initGrammars, loadAllGrammars, isSourceFile } from '../src/extraction/grammars';
  13. import { stripCppTemplateArgs, blankCppExportMacros, blankCppInlineMacros, blankMetalAttributes, blankCudaConstructs, blankCppAnnotationMacroCalls, blankCppApiPrefixMacros, blankCppInlineAnnotationMacros, blankCLeadingAttrMacros, recoverMangledCppName } from '../src/extraction/languages/c-cpp';
  14. import { normalizePath } from '../src/utils';
  15. beforeAll(async () => {
  16. await initGrammars();
  17. await loadAllGrammars();
  18. });
  19. // Create a temporary directory for each test
  20. function createTempDir(): string {
  21. return fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-test-'));
  22. }
  23. // Clean up temporary directory
  24. function cleanupTempDir(dir: string): void {
  25. if (fs.existsSync(dir)) {
  26. fs.rmSync(dir, { recursive: true, force: true });
  27. }
  28. }
  29. describe('Language Detection', () => {
  30. it('should detect TypeScript files', () => {
  31. expect(detectLanguage('src/index.ts')).toBe('typescript');
  32. expect(detectLanguage('components/Button.tsx')).toBe('tsx');
  33. });
  34. it('should detect JavaScript files', () => {
  35. expect(detectLanguage('index.js')).toBe('javascript');
  36. expect(detectLanguage('App.jsx')).toBe('jsx');
  37. expect(detectLanguage('config.mjs')).toBe('javascript');
  38. });
  39. it('should detect Python files', () => {
  40. expect(detectLanguage('main.py')).toBe('python');
  41. });
  42. it('should detect Go files', () => {
  43. expect(detectLanguage('main.go')).toBe('go');
  44. });
  45. it('should detect Rust files', () => {
  46. expect(detectLanguage('lib.rs')).toBe('rust');
  47. });
  48. it('should detect Java files', () => {
  49. expect(detectLanguage('Main.java')).toBe('java');
  50. });
  51. it('should detect C files', () => {
  52. expect(detectLanguage('main.c')).toBe('c');
  53. expect(detectLanguage('utils.h')).toBe('c');
  54. });
  55. it('should detect C++ files', () => {
  56. expect(detectLanguage('main.cpp')).toBe('cpp');
  57. expect(detectLanguage('class.hpp')).toBe('cpp');
  58. });
  59. it('should detect C# files', () => {
  60. expect(detectLanguage('Program.cs')).toBe('csharp');
  61. });
  62. it('should detect PHP files', () => {
  63. expect(detectLanguage('index.php')).toBe('php');
  64. });
  65. it('should detect Ruby files', () => {
  66. expect(detectLanguage('app.rb')).toBe('ruby');
  67. });
  68. it('should detect Swift files', () => {
  69. expect(detectLanguage('ViewController.swift')).toBe('swift');
  70. });
  71. it('should detect Kotlin files', () => {
  72. expect(detectLanguage('MainActivity.kt')).toBe('kotlin');
  73. expect(detectLanguage('build.gradle.kts')).toBe('kotlin');
  74. });
  75. it('should detect Dart files', () => {
  76. expect(detectLanguage('main.dart')).toBe('dart');
  77. });
  78. it('should detect Objective-C files', () => {
  79. expect(detectLanguage('AppDelegate.m')).toBe('objc');
  80. expect(detectLanguage('ViewController.mm')).toBe('objc');
  81. const objcHeader = '@interface Foo : NSObject\n@end\n';
  82. expect(detectLanguage('Foo.h', objcHeader)).toBe('objc');
  83. expect(detectLanguage('stdio.h', '#ifndef STDIO_H\nvoid printf();\n#endif\n')).toBe('c');
  84. });
  85. it('should detect Metal shader files as C++ (#1121)', () => {
  86. expect(detectLanguage('Shaders.metal')).toBe('cpp');
  87. expect(isSourceFile('Renderer/Shaders.metal')).toBe(true);
  88. });
  89. it('should detect CUDA files as C++ (#387)', () => {
  90. expect(detectLanguage('kernels/scan.cu')).toBe('cpp');
  91. expect(detectLanguage('include/reduce.cuh')).toBe('cpp');
  92. expect(isSourceFile('csrc/flash_attn/softmax.cu')).toBe(true);
  93. expect(isSourceFile('include/block_reduce.cuh')).toBe(true);
  94. });
  95. it('should detect Erlang files', () => {
  96. expect(detectLanguage('src/my_server.erl')).toBe('erlang');
  97. expect(detectLanguage('include/records.hrl')).toBe('erlang');
  98. expect(detectLanguage('bin/release_tool.escript')).toBe('erlang');
  99. // OTP app resource files route by full suffix — `.src` alone is too generic.
  100. expect(detectLanguage('src/myapp.app.src')).toBe('erlang');
  101. expect(detectLanguage('ebin/myapp.app')).toBe('erlang');
  102. expect(detectLanguage('legacy/module.src')).toBe('unknown');
  103. expect(isSourceFile('src/myapp.app.src')).toBe(true);
  104. expect(isSourceFile('ebin/myapp.app')).toBe(true);
  105. expect(isSourceFile('legacy/module.src')).toBe(false);
  106. });
  107. it('should detect Solidity files', () => {
  108. expect(detectLanguage('contracts/Vault.sol')).toBe('solidity');
  109. });
  110. it('should detect Terraform files', () => {
  111. expect(detectLanguage('main.tf')).toBe('terraform');
  112. expect(detectLanguage('variables.tf')).toBe('terraform');
  113. expect(detectLanguage('terraform.tfvars')).toBe('terraform');
  114. expect(detectLanguage('versions.tofu')).toBe('terraform');
  115. });
  116. it('should detect ArkTS files', () => {
  117. expect(detectLanguage('entry/src/main/ets/pages/Index.ets')).toBe('arkts');
  118. // Plain `.ts` in a HarmonyOS project is still TypeScript.
  119. expect(detectLanguage('entry/src/main/ets/common/utils.ts')).toBe('typescript');
  120. });
  121. it('should detect Nix files', () => {
  122. expect(detectLanguage('default.nix')).toBe('nix');
  123. expect(detectLanguage('pkgs/development/tools/misc/codegraph/default.nix')).toBe('nix');
  124. expect(isSourceFile('default.nix')).toBe(true);
  125. });
  126. it('should detect a .h whose only C++ signal is an export-macro class as cpp', () => {
  127. // Lean Unreal-Engine style header: the class is annotated with an export
  128. // macro and carries no explicit `public:`/`virtual`/`namespace`/`template`,
  129. // so the macro-blind `class\s+\w+\s*[:{]` branch alone can't see it. It must
  130. // still detect as C++ — otherwise the C extractor (classTypes: []) drops the
  131. // class definition entirely. (#1093 follow-up)
  132. const macroClassHeader = `#pragma once
  133. #include "CoreMinimal.h"
  134. UCLASS()
  135. class ENGINE_API UNetConnectionRepControl : public UObject
  136. {
  137. \tGENERATED_BODY()
  138. \tbool IsRepControlEnable() const;
  139. };
  140. `;
  141. expect(detectLanguage('NetConnectionRepControl.h', macroClassHeader)).toBe('cpp');
  142. // Macro class with no base clause, brace on the next line, still C++.
  143. expect(detectLanguage('Foo.h', 'MYMODULE_API_DECL\nclass MYMODULE_API FFoo\n{\n\tint X;\n};\n')).toBe('cpp');
  144. // Export-macro struct with inheritance is likewise C++-only.
  145. expect(detectLanguage('Bar.h', 'struct ENGINE_API FBar : public FBase {};\n')).toBe('cpp');
  146. // Guard: a genuine C header must NOT be dragged to C++ by the new branch.
  147. expect(detectLanguage('cfoo.h', '#ifndef CFOO_H\nstruct Point { int x; int y; };\nvoid f(struct Point p);\n#endif\n')).toBe('c');
  148. });
  149. it('should detect a .h whose only C++ signal is a plain base clause as cpp (#1592)', () => {
  150. // No export macro, no `class` keyword, no access section, no `virtual`:
  151. // the derived struct's base clause is the only C++ construct, and the
  152. // #1159 branch only knows the macro-annotated form. Misdetected as C, the
  153. // C extractor drops `Derived` and mints a phantom `function Base`.
  154. expect(detectLanguage('min.h', 'struct Base {};\nstruct Derived : Base {};\n')).toBe('cpp');
  155. expect(detectLanguage('pub.h', 'struct Derived : public Base {};\n')).toBe('cpp');
  156. expect(detectLanguage('scoped.h', 'struct Derived : ns::Base {};\n')).toBe('cpp');
  157. expect(detectLanguage('tmpl.h', 'struct Derived : Base<int, Foo<T>> {};\n')).toBe('cpp');
  158. expect(detectLanguage('final.h', 'struct Derived final : Base {};\n')).toBe('cpp');
  159. expect(detectLanguage('multi.h', 'class Derived : public A, private B\n{\n};\n')).toBe('cpp');
  160. expect(detectLanguage('virt.h', 'struct Derived : virtual Base {};\n')).toBe('cpp');
  161. // The base clause sits PAST the 8 KB sample, behind a long C-compatible
  162. // preamble (guards, defines, plain typedefs) — the second pass must scan
  163. // the whole file, not just the sample.
  164. const preamble = '#ifndef BIG_H\n#define BIG_H\n' + '#define VALUE_0 0\n'.repeat(700);
  165. expect(preamble.length).toBeGreaterThan(8192);
  166. expect(detectLanguage('big.h', `${preamble}struct Base {};\nstruct Derived : Base {};\n#endif\n`)).toBe('cpp');
  167. // Controls — all genuine C, none may flip to C++:
  168. // a bit-field (`:` after a member name inside the body),
  169. expect(detectLanguage('bits.h', 'struct S { unsigned int a : 3; unsigned int b : 5; };\n')).toBe('c');
  170. // a ternary whose `:` follows a `sizeof(struct …)` / cast,
  171. expect(detectLanguage('tern.h', 'static inline int sz(int x) { return x ? sizeof(struct foo) : 0; }\n#define P(a,b) ((a) ? (struct foo *)(a) : (b))\n')).toBe('c');
  172. // a label / identifier that merely starts with `struct`,
  173. expect(detectLanguage('label.h', 'static void g(void) {\nstruct_end:\n return;\n}\nint struct_a, struct_b;\n')).toBe('c');
  174. // a doc comment whose prose reads like a base clause,
  175. expect(detectLanguage('doc.h', '/* struct timeval: seconds, microseconds */\nstruct timeval { long tv_sec; long tv_usec; };\n// struct foo: x, y\n')).toBe('c');
  176. // and the two existing controls.
  177. expect(detectLanguage('cfoo.h', '#ifndef CFOO_H\nstruct Point { int x; int y; };\nvoid f(struct Point p);\n#endif\n')).toBe('c');
  178. expect(detectLanguage('stdio.h', '#ifndef STDIO_H\nvoid printf();\n#endif\n')).toBe('c');
  179. });
  180. it('should extract a derived struct from a plain base-clause .h, with no phantom function (#1592)', () => {
  181. const result = extractFromSource('src/min.h', 'struct Base {};\nstruct Derived : Base {};\n');
  182. const derived = result.nodes.find((n) => n.name === 'Derived');
  183. expect(derived).toBeDefined();
  184. expect(derived?.kind).toBe('struct');
  185. expect(derived?.language).toBe('cpp');
  186. // The C mis-route read `Derived : Base {}` as a K&R-ish function `Base`
  187. // returning `Derived` — that phantom must be gone.
  188. expect(result.nodes.some((n) => n.name === 'Base' && n.kind === 'function')).toBe(false);
  189. expect(result.nodes.filter((n) => n.name === 'Base')).toHaveLength(1);
  190. expect(result.nodes.find((n) => n.name === 'Base')?.kind).toBe('struct');
  191. });
  192. it('should return unknown for unsupported extensions', () => {
  193. expect(detectLanguage('styles.css')).toBe('unknown');
  194. expect(detectLanguage('data.json')).toBe('unknown');
  195. });
  196. });
  197. describe('Language Support', () => {
  198. it('should report supported languages', () => {
  199. expect(isLanguageSupported('typescript')).toBe(true);
  200. expect(isLanguageSupported('python')).toBe(true);
  201. expect(isLanguageSupported('go')).toBe(true);
  202. expect(isLanguageSupported('unknown')).toBe(false);
  203. });
  204. it('should list all supported languages', () => {
  205. const languages = getSupportedLanguages();
  206. expect(languages).toContain('typescript');
  207. expect(languages).toContain('javascript');
  208. expect(languages).toContain('python');
  209. expect(languages).toContain('go');
  210. expect(languages).toContain('rust');
  211. expect(languages).toContain('java');
  212. expect(languages).toContain('csharp');
  213. expect(languages).toContain('php');
  214. expect(languages).toContain('ruby');
  215. expect(languages).toContain('swift');
  216. expect(languages).toContain('kotlin');
  217. expect(languages).toContain('dart');
  218. expect(languages).toContain('solidity');
  219. expect(languages).toContain('nix');
  220. });
  221. });
  222. describe('Nix Extraction', () => {
  223. it('should distinguish Nix variable and function bindings', () => {
  224. const code = `
  225. let
  226. plainValue = 10;
  227. simpleFn = arg: arg + 1;
  228. destructuredFn = { lib, stdenv }: lib.getName stdenv;
  229. curriedFn = a: b: builtins.toString (a + b);
  230. in
  231. {
  232. exportedValue = plainValue;
  233. exportedFn = curriedFn;
  234. }
  235. `;
  236. const result = extractFromSource('default.nix', code);
  237. expect(result.nodes.find((n) => n.kind === 'variable' && n.name === 'plainValue')).toBeDefined();
  238. expect(result.nodes.find((n) => n.kind === 'variable' && n.name === 'exportedValue')).toBeDefined();
  239. const simpleFn = result.nodes.find((n) => n.kind === 'function' && n.name === 'simpleFn');
  240. const destructuredFn = result.nodes.find((n) => n.kind === 'function' && n.name === 'destructuredFn');
  241. const curriedFn = result.nodes.find((n) => n.kind === 'function' && n.name === 'curriedFn');
  242. expect(simpleFn?.signature).toBe('(arg)');
  243. expect(destructuredFn?.signature).toBe('{ lib, stdenv }');
  244. expect(curriedFn?.signature).toBe('a : b');
  245. const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName);
  246. expect(calls).toContain('lib.getName');
  247. expect(calls.filter((name) => name === 'builtins.toString')).toHaveLength(1);
  248. });
  249. it('should extract inherited Nix attributes as variables', () => {
  250. const code = `
  251. let
  252. inherit lib;
  253. inherit (pkgs) stdenv writeShellScriptBin;
  254. in
  255. stdenv.mkDerivation {}
  256. `;
  257. const result = extractFromSource('default.nix', code);
  258. const variables = result.nodes.filter((n) => n.kind === 'variable').map((n) => n.name);
  259. expect(variables).toContain('lib');
  260. expect(variables).toContain('stdenv');
  261. expect(variables).toContain('writeShellScriptBin');
  262. });
  263. it('should emit only static project path imports for Nix import calls', () => {
  264. const code = `
  265. let
  266. local = import ./x.nix;
  267. defaultFile = builtins.import ./dir;
  268. packageSet = import <nixpkgs> {};
  269. fromSources = import sources.nixpkgs {};
  270. dynamic = import selectedPath;
  271. in
  272. local
  273. `;
  274. const result = extractFromSource('default.nix', code);
  275. const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name);
  276. const importRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'imports').map((r) => r.referenceName);
  277. expect(imports).toEqual(['./x.nix', './dir']);
  278. expect(importRefs).toEqual(['./x.nix', './dir']);
  279. });
  280. it('should emit file imports for NixOS module imports/modules lists (literal paths only)', () => {
  281. const code = `
  282. { config, lib, ... }:
  283. {
  284. imports = [ ./hardware.nix ../common inputs.foo.nixosModules.bar ];
  285. home-manager.users.demo.imports = [ ./home.nix ];
  286. flake.modules = [ ./configuration.nix ];
  287. notAModuleList = [ ./ignored.nix ];
  288. }
  289. `;
  290. const result = extractFromSource('configuration.nix', code);
  291. const importRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'imports').map((r) => r.referenceName);
  292. expect(importRefs).toEqual(['./hardware.nix', '../common', './home.nix', './configuration.nix']);
  293. // The dynamic entry (inputs.foo.nixosModules.bar) must not create a ref.
  294. expect(importRefs).not.toContain('inputs.foo.nixosModules.bar');
  295. });
  296. it('should emit file imports for callPackage with a literal path and skip dynamic ones', () => {
  297. const code = `
  298. { pkgs, newScope }:
  299. let
  300. hello = pkgs.callPackage ./pkgs/hello { };
  301. tools = pkgs.callPackages ../tools/all.nix { };
  302. dynamic = pkgs.callPackage pkgPath { };
  303. in
  304. {
  305. inherit hello tools dynamic;
  306. }
  307. `;
  308. const result = extractFromSource('overlay.nix', code);
  309. const importRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'imports').map((r) => r.referenceName);
  310. const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName);
  311. expect(importRefs).toEqual(['./pkgs/hello', '../tools/all.nix']);
  312. // The call edge to callPackage itself is still recorded.
  313. expect(calls).toContain('pkgs.callPackage');
  314. });
  315. it('should mark returned top-level Nix attrset members exported and keep let or nested attrs private', () => {
  316. const code = `
  317. { lib, stdenv }:
  318. let
  319. localValue = 10;
  320. in
  321. {
  322. exported = localValue;
  323. package = { name }: stdenv.mkDerivation { inherit name; };
  324. nested = {
  325. privateNested = true;
  326. };
  327. inherit (lib) licenses;
  328. }
  329. `;
  330. const result = extractFromSource('default.nix', code);
  331. const node = (name: string) => result.nodes.find((n) => n.name === name);
  332. expect(node('localValue')?.isExported).toBe(false);
  333. expect(node('exported')?.isExported).toBe(true);
  334. expect(node('package')?.kind).toBe('function');
  335. expect(node('package')?.isExported).toBe(true);
  336. expect(node('privateNested')?.isExported).toBe(false);
  337. expect(node('licenses')?.isExported).toBe(true);
  338. });
  339. });
  340. describe('TypeScript Extraction', () => {
  341. it('should extract function declarations', () => {
  342. const code = `
  343. export function processPayment(amount: number): Promise<Receipt> {
  344. return stripe.charge(amount);
  345. }
  346. `;
  347. const result = extractFromSource('payment.ts', code);
  348. // File node + function node
  349. const fileNode = result.nodes.find((n) => n.kind === 'file');
  350. expect(fileNode).toBeDefined();
  351. expect(fileNode?.name).toBe('payment.ts');
  352. const funcNode = result.nodes.find((n) => n.kind === 'function');
  353. expect(funcNode).toMatchObject({
  354. kind: 'function',
  355. name: 'processPayment',
  356. language: 'typescript',
  357. isExported: true,
  358. });
  359. expect(funcNode?.signature).toContain('amount: number');
  360. });
  361. it('should extract class declarations', () => {
  362. const code = `
  363. export class PaymentService {
  364. private stripe: StripeClient;
  365. constructor(apiKey: string) {
  366. this.stripe = new StripeClient(apiKey);
  367. }
  368. async charge(amount: number): Promise<Receipt> {
  369. return this.stripe.charge(amount);
  370. }
  371. }
  372. `;
  373. const result = extractFromSource('service.ts', code);
  374. const classNode = result.nodes.find((n) => n.kind === 'class');
  375. const methodNodes = result.nodes.filter((n) => n.kind === 'method');
  376. expect(classNode).toBeDefined();
  377. expect(classNode?.name).toBe('PaymentService');
  378. expect(classNode?.isExported).toBe(true);
  379. expect(methodNodes.length).toBeGreaterThanOrEqual(1);
  380. const chargeMethod = methodNodes.find((m) => m.name === 'charge');
  381. expect(chargeMethod).toBeDefined();
  382. });
  383. it('captures docstrings for export- and const-wrapped declarations (#780)', () => {
  384. const code = `
  385. // plain class control
  386. class Ledger {}
  387. // exported class
  388. export class Invoice {}
  389. // export default
  390. export default function settle() { return true; }
  391. // exported arrow const
  392. export const refund = (amount: number) => amount;
  393. // non-export arrow const
  394. const audit = (amount: number) => amount;
  395. `;
  396. const byName = new Map(extractFromSource('doc.ts', code).nodes.map((n) => [n.name, n]));
  397. expect(byName.get('Ledger')?.docstring).toBe('plain class control'); // control still works
  398. expect(byName.get('Invoice')?.docstring).toBe('exported class');
  399. expect(byName.get('settle')?.docstring).toBe('export default');
  400. expect(byName.get('refund')?.docstring).toBe('exported arrow const');
  401. expect(byName.get('audit')?.docstring).toBe('non-export arrow const');
  402. });
  403. it('does not mis-attribute a class comment to an uncommented member (#780)', () => {
  404. const code = `
  405. // Comment for Box
  406. export class Box {
  407. noComment() {}
  408. // own comment
  409. withComment() {}
  410. }
  411. `;
  412. const byName = new Map(extractFromSource('box.ts', code).nodes.map((n) => [n.name, n]));
  413. expect(byName.get('Box')?.docstring).toBe('Comment for Box');
  414. expect(byName.get('noComment')?.docstring ?? null).toBeNull(); // no over-walk
  415. expect(byName.get('withComment')?.docstring).toBe('own comment');
  416. });
  417. it('captures docstrings for decorated Python declarations, stripping `#` (#780)', () => {
  418. const code = [
  419. '# decorated function',
  420. '@app.route("/x")',
  421. 'def py_handler():',
  422. ' return 1',
  423. '',
  424. '',
  425. '# plain function control',
  426. 'def py_plain():',
  427. ' return 1',
  428. '',
  429. '',
  430. '# decorated class',
  431. '@dataclass',
  432. 'class PyModel:',
  433. ' pass',
  434. '',
  435. ].join('\n');
  436. const byName = new Map(extractFromSource('mod.py', code).nodes.map((n) => [n.name, n]));
  437. expect(byName.get('py_handler')?.docstring).toBe('decorated function');
  438. expect(byName.get('py_plain')?.docstring).toBe('plain function control'); // `#` stripped
  439. expect(byName.get('PyModel')?.docstring).toBe('decorated class');
  440. });
  441. it('cleans comment markers across language styles (#780)', () => {
  442. const doc = (file: string, code: string, name: string) =>
  443. new Map(extractFromSource(file, code).nodes.map((n) => [n.name, n])).get(name)?.docstring;
  444. // Rust doc lines (`///`, `//!`) — the trailing slash used to leak through.
  445. expect(doc('m.rs', '/// rust doc line\nfn rs_fn() {}', 'rs_fn')).toBe('rust doc line');
  446. // Lua line + long-bracket comments.
  447. expect(doc('m.lua', '-- lua line\nfunction lua_fn() end', 'lua_fn')).toBe('lua line');
  448. expect(doc('b.lua', '--[[ lua block ]]\nfunction lua_b() end', 'lua_b')).toBe('lua block');
  449. // Pascal brace and paren-star comments.
  450. const pasUnit = (c: string) =>
  451. `unit U;\ninterface\n${c}\nprocedure P;\nimplementation\nprocedure P;\nbegin\nend;\nend.\n`;
  452. expect(doc('a.pas', pasUnit('{ pascal brace }'), 'P')).toBe('pascal brace');
  453. expect(doc('c.pas', pasUnit('(* pascal paren *)'), 'P')).toBe('pascal paren');
  454. // C block comment still clean (no regression).
  455. expect(doc('m.c', '/* c block */\nvoid c_fn(void) {}', 'c_fn')).toBe('c block');
  456. });
  457. it('should extract interfaces', () => {
  458. const code = `
  459. export interface User {
  460. id: string;
  461. name: string;
  462. email: string;
  463. }
  464. `;
  465. const result = extractFromSource('types.ts', code);
  466. const fileNode = result.nodes.find((n) => n.kind === 'file');
  467. expect(fileNode).toBeDefined();
  468. const ifaceNode = result.nodes.find((n) => n.kind === 'interface');
  469. expect(ifaceNode).toMatchObject({
  470. kind: 'interface',
  471. name: 'User',
  472. isExported: true,
  473. });
  474. });
  475. it('should extract type references from interface property signatures', () => {
  476. const code = `
  477. import type { IPage } from '../PromoterList';
  478. import type { IOrderField } from '../types';
  479. interface Hprops {
  480. value?: Partial<IPage> & Partial<IOrderField>;
  481. }
  482. `;
  483. const result = extractFromSource('HeaderFilter.ts', code);
  484. const refs = result.unresolvedReferences.filter((r) => r.referenceKind === 'references');
  485. expect(refs.some((r) => r.referenceName === 'IPage')).toBe(true);
  486. expect(refs.some((r) => r.referenceName === 'IOrderField')).toBe(true);
  487. });
  488. it('indexes interface members, not just the interface itself', () => {
  489. // tree-sitter-typescript spells interface members `method_signature` /
  490. // `property_signature`, distinct from the class-member types the extractor
  491. // listed, so they were never captured (#1638). Java/C# are unaffected —
  492. // their grammars reuse `method_declaration`, already in their methodTypes.
  493. // The cost lands on `.d.ts` platform APIs: with no declaration node, call
  494. // sites through the interface have nothing to attach an edge to.
  495. const code = `
  496. export interface PlatformApi {
  497. fetchPage(id: string): Promise<string>;
  498. version: string;
  499. }
  500. `;
  501. const result = extractFromSource('api.d.ts', code);
  502. const iface = result.nodes.find((n) => n.kind === 'interface' && n.name === 'PlatformApi');
  503. const method = result.nodes.find((n) => n.kind === 'method' && n.name === 'fetchPage');
  504. const prop = result.nodes.find((n) => n.kind === 'property' && n.name === 'version');
  505. expect(iface).toBeDefined();
  506. expect(method).toBeDefined();
  507. expect(prop).toBeDefined();
  508. // Attached to the interface, not merely present. A member the graph holds
  509. // but hangs off the file is not a declaration a call edge can be resolved
  510. // through, which is the whole point of extracting it.
  511. const contained = result.edges
  512. .filter((e) => e.kind === 'contains' && e.source === iface!.id)
  513. .map((e) => e.target);
  514. expect(contained).toContain(method!.id);
  515. expect(contained).toContain(prop!.id);
  516. });
  517. it('does not mint a top-level function from a type literal method signature', () => {
  518. // The failure mode the class-like guard on `method_signature` exists for
  519. // (#1638). `extractMethod` treats a method node with no class-like parent
  520. // as a free function — right for `method_definition`, wrong for a bodiless
  521. // signature, whose only home outside an interface is a type literal. Those
  522. // members are already extracted onto the alias (#359), so without the guard
  523. // the file gains a phantom `function stop` beside the real `Handle::stop`.
  524. const result = extractFromSource('t.ts', `
  525. export type Handle = { stop(): void; label: string };
  526. `);
  527. const alias = result.nodes.find((n) => n.kind === 'type_alias' && n.name === 'Handle');
  528. expect(alias).toBeDefined();
  529. expect(result.nodes.find((n) => n.kind === 'method' && n.name === 'stop')).toBeDefined();
  530. expect(result.nodes.filter((n) => n.kind === 'function' && n.name === 'stop')).toEqual([]);
  531. });
  532. it('should extract type references from interface method signatures', () => {
  533. const code = `
  534. import type { IPage } from '../PromoterList';
  535. import type { IOrderField } from '../types';
  536. interface MethodForm {
  537. fetchPage(arg: IPage): IOrderField;
  538. }
  539. `;
  540. const result = extractFromSource('MethodForm.ts', code);
  541. const refs = result.unresolvedReferences.filter((r) => r.referenceKind === 'references');
  542. expect(refs.some((r) => r.referenceName === 'IPage')).toBe(true);
  543. expect(refs.some((r) => r.referenceName === 'IOrderField')).toBe(true);
  544. });
  545. it('extracts type references from in-body local variable annotations', () => {
  546. // A function that uses a type ONLY in its body — `const items: Foo[] = []` —
  547. // still depends on Foo. The body walker used to capture calls but never type
  548. // annotations, so impact / `affected` missed the dependency. Must cover
  549. // function, class-method, and object-literal-method bodies — and must NOT
  550. // turn the locals themselves into graph nodes (that would explode the graph).
  551. const code = `
  552. import { Foo } from './types';
  553. export function build(): void {
  554. const items: Foo[] = [];
  555. void items;
  556. }
  557. export class K {
  558. run(): void {
  559. const a: Foo = { x: 1 };
  560. void a;
  561. }
  562. }
  563. export const handler = {
  564. handle(): void {
  565. const b: Foo = { x: 1 };
  566. void b;
  567. },
  568. };
  569. `;
  570. const result = extractFromSource('inbody.ts', code);
  571. const fooRefs = result.unresolvedReferences.filter(
  572. (r) => r.referenceKind === 'references' && r.referenceName === 'Foo'
  573. );
  574. // One per body scope: build(), K.run(), handler.handle().
  575. expect(fooRefs.length).toBeGreaterThanOrEqual(3);
  576. // Each reference is attributed to its enclosing function/method node — never
  577. // to a local-variable node, because locals are intentionally not extracted.
  578. const byId = new Map(result.nodes.map((n) => [n.id, n]));
  579. for (const ref of fooRefs) {
  580. const owner = byId.get(ref.fromNodeId);
  581. expect(owner).toBeDefined();
  582. expect(['function', 'method']).toContain(owner!.kind);
  583. }
  584. // The locals (items/a/b) must not leak in as symbols.
  585. expect(result.nodes.some((n) => ['items', 'a', 'b'].includes(n.name))).toBe(false);
  586. });
  587. it('should track function calls', () => {
  588. const code = `
  589. function main() {
  590. const result = processData();
  591. console.log(result);
  592. }
  593. `;
  594. const result = extractFromSource('main.ts', code);
  595. expect(result.unresolvedReferences.length).toBeGreaterThan(0);
  596. const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls');
  597. expect(calls.some((c) => c.referenceName === 'processData')).toBe(true);
  598. });
  599. });
  600. describe('Arrow Function Export Extraction', () => {
  601. it('should extract exported arrow functions assigned to const', () => {
  602. const code = `
  603. export const useAuth = (): AuthContextValue => {
  604. return useContext(AuthContext);
  605. };
  606. `;
  607. const result = extractFromSource('hooks.ts', code);
  608. const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'useAuth');
  609. expect(funcNode).toBeDefined();
  610. expect(funcNode).toMatchObject({
  611. kind: 'function',
  612. name: 'useAuth',
  613. isExported: true,
  614. });
  615. });
  616. it('should extract exported function expressions assigned to const', () => {
  617. const code = `
  618. export const processData = function(input: string): string {
  619. return input.trim();
  620. };
  621. `;
  622. const result = extractFromSource('utils.ts', code);
  623. const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'processData');
  624. expect(funcNode).toBeDefined();
  625. expect(funcNode).toMatchObject({
  626. kind: 'function',
  627. name: 'processData',
  628. isExported: true,
  629. });
  630. });
  631. it('should not extract non-exported arrow functions as exported', () => {
  632. const code = `
  633. const internalHelper = () => {
  634. return 42;
  635. };
  636. `;
  637. const result = extractFromSource('internal.ts', code);
  638. const helperNode = result.nodes.find((n) => n.name === 'internalHelper');
  639. expect(helperNode).toBeDefined();
  640. expect(helperNode?.isExported).toBeFalsy();
  641. });
  642. it('should still skip truly anonymous arrow functions', () => {
  643. const code = `
  644. const items = [1, 2, 3].map((x) => x * 2);
  645. `;
  646. const result = extractFromSource('anon.ts', code);
  647. // The inline arrow function passed to .map() has no variable_declarator parent
  648. // and should remain anonymous (skipped)
  649. const anonFunctions = result.nodes.filter(
  650. (n) => n.kind === 'function' && n.name === '<anonymous>'
  651. );
  652. expect(anonFunctions).toHaveLength(0);
  653. });
  654. it('should extract multiple exported arrow functions from the same file', () => {
  655. const code = `
  656. export const add = (a: number, b: number): number => a + b;
  657. export const subtract = (a: number, b: number): number => a - b;
  658. const internal = () => 'not exported';
  659. `;
  660. const result = extractFromSource('math.ts', code);
  661. const exported = result.nodes.filter((n) => n.kind === 'function' && n.isExported);
  662. expect(exported).toHaveLength(2);
  663. expect(exported.map((n) => n.name).sort()).toEqual(['add', 'subtract']);
  664. const internalNode = result.nodes.find((n) => n.name === 'internal');
  665. expect(internalNode).toBeDefined();
  666. expect(internalNode?.isExported).toBeFalsy();
  667. });
  668. it('should extract arrow functions in JavaScript files', () => {
  669. const code = `
  670. export const fetchData = async () => {
  671. const response = await fetch('/api/data');
  672. return response.json();
  673. };
  674. `;
  675. const result = extractFromSource('api.js', code);
  676. const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'fetchData');
  677. expect(funcNode).toBeDefined();
  678. expect(funcNode).toMatchObject({
  679. kind: 'function',
  680. name: 'fetchData',
  681. isExported: true,
  682. });
  683. });
  684. });
  685. describe('Generator Function Extraction (#1741)', () => {
  686. const functionNames = (file: string, code: string) =>
  687. extractFromSource(file, code)
  688. .nodes.filter((n) => n.kind === 'function')
  689. .map((n) => n.name)
  690. .sort();
  691. it('extracts function* and async function* declarations in TypeScript', () => {
  692. process.env.CODEGRAPH_KERNEL = '0';
  693. const code = `
  694. function plain() { return 1; }
  695. function* gen() { yield 2; }
  696. async function asyncFn() { return 3; }
  697. async function* asyncGen() { yield 4; }
  698. `;
  699. expect(functionNames('gens.ts', code)).toEqual(['asyncFn', 'asyncGen', 'gen', 'plain']);
  700. });
  701. it('extracts function* and async function* declarations in JavaScript', () => {
  702. process.env.CODEGRAPH_KERNEL = '0';
  703. const code = `
  704. function plain() { return 1; }
  705. function* gen() { yield 2; }
  706. async function asyncFn() { return 3; }
  707. async function* asyncGen() { yield 4; }
  708. `;
  709. expect(functionNames('gens.js', code)).toEqual(['asyncFn', 'asyncGen', 'gen', 'plain']);
  710. });
  711. it('extracts const-assigned generator and async generator expressions (TS)', () => {
  712. process.env.CODEGRAPH_KERNEL = '0';
  713. const code = `
  714. const g = function* () { yield 1; };
  715. const ag = async function* () { yield 2; };
  716. export const exportedGen = function* () { yield 3; };
  717. `;
  718. const result = extractFromSource('gen-expr.ts', code);
  719. const names = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name).sort();
  720. expect(names).toEqual(['ag', 'exportedGen', 'g']);
  721. expect(result.nodes.find((n) => n.name === 'exportedGen')?.isExported).toBe(true);
  722. expect(result.nodes.find((n) => n.name === 'g')?.isExported).toBeFalsy();
  723. });
  724. it('extracts const-assigned generator and async generator expressions (JS)', () => {
  725. process.env.CODEGRAPH_KERNEL = '0';
  726. const code = `
  727. const g = function* () { yield 1; };
  728. const ag = async function* () { yield 2; };
  729. export const exportedGen = function* () { yield 3; };
  730. `;
  731. const result = extractFromSource('gen-expr.js', code);
  732. const names = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name).sort();
  733. expect(names).toEqual(['ag', 'exportedGen', 'g']);
  734. expect(result.nodes.find((n) => n.name === 'exportedGen')?.isExported).toBe(true);
  735. });
  736. });
  737. describe('Type Alias Extraction', () => {
  738. it('should extract exported type aliases in TypeScript', () => {
  739. const code = `
  740. export type AuthContextValue = {
  741. user: User | null;
  742. login: () => void;
  743. logout: () => void;
  744. };
  745. `;
  746. const result = extractFromSource('types.ts', code);
  747. const typeNode = result.nodes.find((n) => n.kind === 'type_alias');
  748. expect(typeNode).toMatchObject({
  749. kind: 'type_alias',
  750. name: 'AuthContextValue',
  751. isExported: true,
  752. });
  753. });
  754. it('should extract non-exported type aliases', () => {
  755. const code = `
  756. type InternalState = {
  757. loading: boolean;
  758. error: string | null;
  759. };
  760. `;
  761. const result = extractFromSource('internal.ts', code);
  762. const typeNode = result.nodes.find((n) => n.kind === 'type_alias');
  763. expect(typeNode).toMatchObject({
  764. kind: 'type_alias',
  765. name: 'InternalState',
  766. isExported: false,
  767. });
  768. });
  769. it('should extract multiple type aliases from the same file', () => {
  770. const code = `
  771. export type UnitSystem = 'metric' | 'imperial';
  772. export type DateFormat = 'ISO' | 'US' | 'EU';
  773. type Internal = string;
  774. `;
  775. const result = extractFromSource('config.ts', code);
  776. const typeAliases = result.nodes.filter((n) => n.kind === 'type_alias');
  777. expect(typeAliases).toHaveLength(3);
  778. const exported = typeAliases.filter((n) => n.isExported);
  779. expect(exported).toHaveLength(2);
  780. expect(exported.map((n) => n.name).sort()).toEqual(['DateFormat', 'UnitSystem']);
  781. });
  782. // A service/contract registry written as a tuple of generic instantiations —
  783. // the names are string-literal type arguments, not declarations, so static
  784. // extraction otherwise never indexes them (issue #634).
  785. it('extracts string-literal contract names from a generic tuple type alias (#634)', () => {
  786. const code = `
  787. interface Service<Name extends string, Req, Resp> { name: Name; }
  788. export type MyServiceList = [
  789. Service<'query_apply_record', { pageNo: number }, { ok: boolean }>,
  790. Service<'apply_confirm', { code: string }, { ok: boolean }>
  791. ];
  792. `;
  793. const result = extractFromSource('services/api.ts', code);
  794. const names = result.nodes.filter(
  795. (n) => n.kind === 'method' && n.qualifiedName.startsWith('MyServiceList::')
  796. );
  797. expect(names.map((n) => n.name).sort()).toEqual(['apply_confirm', 'query_apply_record']);
  798. const queryNode = names.find((n) => n.name === 'query_apply_record');
  799. expect(queryNode?.qualifiedName).toBe('MyServiceList::query_apply_record');
  800. // Signature carries the full contract entry so search results show context.
  801. expect(queryNode?.signature).toContain("Service<'query_apply_record'");
  802. // The string-literal name is contained by the type alias.
  803. const alias = result.nodes.find((n) => n.kind === 'type_alias' && n.name === 'MyServiceList');
  804. const containsEdge = result.edges.find(
  805. (e) => e.kind === 'contains' && e.source === alias?.id && e.target === queryNode?.id
  806. );
  807. expect(containsEdge).toBeDefined();
  808. });
  809. it('does not extract string literals from utility types or nested generics (#634)', () => {
  810. const code = `
  811. interface User { id: string; name: string; }
  812. interface Service<Name extends string, Req, Resp> { name: Name; }
  813. export type Picked = Pick<User, 'id' | 'name'>;
  814. export type Rec = Record<'foo' | 'bar', number>;
  815. // Tuple entry, but the name is a non-identifier route path; the nested Pick's
  816. // 'id' must also stay out (only DIRECT literal args of a tuple's generic count).
  817. export type Routes = [Service<'/api/users', Pick<User, 'id'>, {}>];
  818. // Bare string-literal tuple — not generic type arguments.
  819. export type Names = ['alpha', 'beta'];
  820. `;
  821. const result = extractFromSource('noise.ts', code);
  822. // Since #1638 the fixture's own interfaces legitimately declare `id` / `name`
  823. // (`User::id`, `User::name`, `Service::name`), so membership in the name list
  824. // no longer implies a leak. What #634 guards is the *source*: a node minted
  825. // from a string literal in `Pick<User, 'id'>` or a tuple has no declaring
  826. // interface, so exclude anything a `contains` edge ties to one.
  827. const ifaceIds = new Set(result.nodes.filter((n) => n.kind === 'interface').map((n) => n.id));
  828. const declaredInInterface = new Set(
  829. result.edges.filter((e) => e.kind === 'contains' && ifaceIds.has(e.source)).map((e) => e.target)
  830. );
  831. const leaked = result.nodes.filter(
  832. (n) =>
  833. (n.kind === 'method' || n.kind === 'property') &&
  834. ['id', 'name', 'foo', 'bar', 'alpha', 'beta'].includes(n.name) &&
  835. !declaredInInterface.has(n.id)
  836. );
  837. expect(leaked).toEqual([]);
  838. });
  839. });
  840. describe('Exported Variable Extraction', () => {
  841. it('should extract exported const with call expression (Zustand store)', () => {
  842. const code = `
  843. export const useUIStore = create<UIState>((set) => ({
  844. isOpen: false,
  845. toggle: () => set((s) => ({ isOpen: !s.isOpen })),
  846. }));
  847. `;
  848. const result = extractFromSource('store.ts', code);
  849. const varNode = result.nodes.find((n) => n.kind === 'constant' && n.name === 'useUIStore');
  850. expect(varNode).toBeDefined();
  851. expect(varNode?.isExported).toBe(true);
  852. });
  853. it('should extract exported const with object literal', () => {
  854. const code = `
  855. export const config = {
  856. apiUrl: 'https://api.example.com',
  857. timeout: 5000,
  858. };
  859. `;
  860. const result = extractFromSource('config.ts', code);
  861. const varNode = result.nodes.find((n) => n.kind === 'constant' && n.name === 'config');
  862. expect(varNode).toBeDefined();
  863. expect(varNode?.isExported).toBe(true);
  864. });
  865. it('should extract exported const with array literal', () => {
  866. const code = `
  867. export const SCREEN_NAMES = ['home', 'settings', 'profile'] as const;
  868. `;
  869. const result = extractFromSource('constants.ts', code);
  870. const varNode = result.nodes.find((n) => n.kind === 'constant' && n.name === 'SCREEN_NAMES');
  871. expect(varNode).toBeDefined();
  872. expect(varNode?.isExported).toBe(true);
  873. });
  874. it('should extract exported const with primitive value', () => {
  875. const code = `
  876. export const MAX_RETRIES = 3;
  877. export const API_VERSION = "v2";
  878. `;
  879. const result = extractFromSource('constants.ts', code);
  880. const variables = result.nodes.filter((n) => n.kind === 'constant');
  881. expect(variables).toHaveLength(2);
  882. expect(variables.map((n) => n.name).sort()).toEqual(['API_VERSION', 'MAX_RETRIES']);
  883. });
  884. it('should NOT duplicate arrow functions as both function and variable', () => {
  885. const code = `
  886. export const useAuth = () => {
  887. return useContext(AuthContext);
  888. };
  889. `;
  890. const result = extractFromSource('hooks.ts', code);
  891. // Should be extracted as function (from arrow function handler), NOT as variable
  892. const funcNodes = result.nodes.filter((n) => n.kind === 'function' && n.name === 'useAuth');
  893. const varNodes = result.nodes.filter((n) => n.kind === 'variable' && n.name === 'useAuth');
  894. expect(funcNodes).toHaveLength(1);
  895. expect(varNodes).toHaveLength(0);
  896. });
  897. it('should extract non-exported const as non-exported variable', () => {
  898. const code = `
  899. const internalConfig = {
  900. debug: true,
  901. };
  902. `;
  903. const result = extractFromSource('internal.ts', code);
  904. // Non-exported const at file level should be extracted as a constant (not exported)
  905. const varNodes = result.nodes.filter((n) => (n.kind === 'variable' || n.kind === 'constant') && n.name === 'internalConfig');
  906. expect(varNodes).toHaveLength(1);
  907. expect(varNodes[0]?.isExported).toBeFalsy();
  908. });
  909. it('should extract Zod schema exports', () => {
  910. const code = `
  911. export const userSchema = z.object({
  912. id: z.string(),
  913. name: z.string(),
  914. email: z.string().email(),
  915. });
  916. `;
  917. const result = extractFromSource('schemas.ts', code);
  918. const varNode = result.nodes.find((n) => n.kind === 'constant' && n.name === 'userSchema');
  919. expect(varNode).toBeDefined();
  920. expect(varNode?.isExported).toBe(true);
  921. });
  922. it('should extract XState machine exports', () => {
  923. const code = `
  924. export const authMachine = createMachine({
  925. id: "auth",
  926. initial: "idle",
  927. states: {
  928. idle: {},
  929. authenticated: {},
  930. },
  931. });
  932. `;
  933. const result = extractFromSource('machine.ts', code);
  934. const varNode = result.nodes.find((n) => n.kind === 'constant' && n.name === 'authMachine');
  935. expect(varNode).toBeDefined();
  936. expect(varNode?.isExported).toBe(true);
  937. });
  938. it('should extract calls from a top-level variable initializer (issue #425)', () => {
  939. const code = `
  940. import { getTokenMp } from './api/upload';
  941. const token = getTokenMp();
  942. `;
  943. const result = extractFromSource('app.ts', code);
  944. const call = result.unresolvedReferences.find(
  945. (ref) => ref.referenceKind === 'calls' && ref.referenceName === 'getTokenMp'
  946. );
  947. expect(call).toBeDefined();
  948. });
  949. });
  950. describe('File Node Extraction', () => {
  951. it('should create a file-kind node for each parsed file', () => {
  952. const code = `
  953. export function greet(name: string): string {
  954. return "Hello " + name;
  955. }
  956. `;
  957. const result = extractFromSource('greeter.ts', code);
  958. const fileNode = result.nodes.find((n) => n.kind === 'file');
  959. expect(fileNode).toBeDefined();
  960. expect(fileNode?.name).toBe('greeter.ts');
  961. expect(fileNode?.filePath).toBe('greeter.ts');
  962. expect(fileNode?.language).toBe('typescript');
  963. expect(fileNode?.startLine).toBe(1);
  964. });
  965. it('should create file nodes for Python files', () => {
  966. const code = `
  967. def main():
  968. pass
  969. `;
  970. const result = extractFromSource('main.py', code);
  971. const fileNode = result.nodes.find((n) => n.kind === 'file');
  972. expect(fileNode).toBeDefined();
  973. expect(fileNode?.name).toBe('main.py');
  974. expect(fileNode?.language).toBe('python');
  975. });
  976. it('should create containment edges from file node to top-level declarations', () => {
  977. const code = `
  978. export function foo() {}
  979. export function bar() {}
  980. `;
  981. const result = extractFromSource('fns.ts', code);
  982. const fileNode = result.nodes.find((n) => n.kind === 'file');
  983. expect(fileNode).toBeDefined();
  984. // There should be contains edges from the file node to each function
  985. const containsEdges = result.edges.filter(
  986. (e) => e.source === fileNode?.id && e.kind === 'contains'
  987. );
  988. expect(containsEdges.length).toBeGreaterThanOrEqual(2);
  989. });
  990. });
  991. describe('Python Extraction', () => {
  992. it('should extract function definitions', () => {
  993. const code = `
  994. def calculate_total(items: list, tax_rate: float) -> float:
  995. """Calculate total with tax."""
  996. subtotal = sum(item.price for item in items)
  997. return subtotal * (1 + tax_rate)
  998. `;
  999. const result = extractFromSource('calc.py', code);
  1000. const fileNode = result.nodes.find((n) => n.kind === 'file');
  1001. expect(fileNode).toBeDefined();
  1002. const funcNode = result.nodes.find((n) => n.kind === 'function');
  1003. expect(funcNode).toMatchObject({
  1004. kind: 'function',
  1005. name: 'calculate_total',
  1006. language: 'python',
  1007. });
  1008. });
  1009. it('should extract class definitions', () => {
  1010. const code = `
  1011. class UserService:
  1012. """Service for managing users."""
  1013. def __init__(self, db):
  1014. self.db = db
  1015. def get_user(self, user_id: str) -> User:
  1016. return self.db.find_user(user_id)
  1017. `;
  1018. const result = extractFromSource('service.py', code);
  1019. const classNode = result.nodes.find((n) => n.kind === 'class');
  1020. expect(classNode).toBeDefined();
  1021. expect(classNode?.name).toBe('UserService');
  1022. });
  1023. });
  1024. describe('Go Extraction', () => {
  1025. it('should extract function declarations', () => {
  1026. const code = `
  1027. package main
  1028. func ProcessOrder(order Order) (Receipt, error) {
  1029. // Process the order
  1030. return Receipt{}, nil
  1031. }
  1032. `;
  1033. const result = extractFromSource('main.go', code);
  1034. const funcNode = result.nodes.find((n) => n.kind === 'function');
  1035. expect(funcNode).toBeDefined();
  1036. expect(funcNode?.name).toBe('ProcessOrder');
  1037. });
  1038. it('should extract method declarations', () => {
  1039. const code = `
  1040. package main
  1041. type Service struct {
  1042. db *Database
  1043. }
  1044. func (s *Service) GetUser(id string) (*User, error) {
  1045. return s.db.FindUser(id)
  1046. }
  1047. `;
  1048. const result = extractFromSource('service.go', code);
  1049. const methodNode = result.nodes.find((n) => n.kind === 'method');
  1050. expect(methodNode).toBeDefined();
  1051. expect(methodNode?.name).toBe('GetUser');
  1052. });
  1053. });
  1054. describe('Rust Extraction', () => {
  1055. it('should extract function declarations', () => {
  1056. const code = `
  1057. pub fn process_data(input: &str) -> Result<Output, Error> {
  1058. // Process data
  1059. Ok(Output::new())
  1060. }
  1061. `;
  1062. const result = extractFromSource('lib.rs', code);
  1063. const funcNode = result.nodes.find((n) => n.kind === 'function');
  1064. expect(funcNode).toBeDefined();
  1065. expect(funcNode?.name).toBe('process_data');
  1066. expect(funcNode?.visibility).toBe('public');
  1067. });
  1068. it('should extract struct declarations', () => {
  1069. const code = `
  1070. pub struct User {
  1071. pub id: String,
  1072. pub name: String,
  1073. email: String,
  1074. }
  1075. `;
  1076. const result = extractFromSource('models.rs', code);
  1077. const structNode = result.nodes.find((n) => n.kind === 'struct');
  1078. expect(structNode).toBeDefined();
  1079. expect(structNode?.name).toBe('User');
  1080. });
  1081. it('should extract unit and tuple structs, not just brace structs', () => {
  1082. // A unit struct has no body field, but it IS a complete definition —
  1083. // Rust has no forward declarations. Skipping it dropped the type and
  1084. // every `impl Trait for UnitStruct` edge with it.
  1085. const code = `
  1086. pub struct Unit;
  1087. pub struct Tuple(pub u32);
  1088. pub struct Brace { pub x: u32 }
  1089. `;
  1090. const result = extractFromSource('shapes.rs', code);
  1091. const structs = result.nodes.filter((n) => n.kind === 'struct').map((n) => n.name).sort();
  1092. expect(structs).toEqual(['Brace', 'Tuple', 'Unit']);
  1093. });
  1094. it('should link impl Trait for a unit struct', () => {
  1095. const code = `
  1096. pub struct Unit;
  1097. pub trait Greet { fn hi(&self) -> String; }
  1098. impl Greet for Unit { fn hi(&self) -> String { "unit".into() } }
  1099. `;
  1100. const result = extractFromSource('greet.rs', code);
  1101. const unit = result.nodes.find((n) => n.kind === 'struct' && n.name === 'Unit');
  1102. expect(unit).toBeDefined();
  1103. const trait = result.nodes.find((n) => n.kind === 'trait' && n.name === 'Greet');
  1104. expect(trait).toBeDefined();
  1105. });
  1106. it('should extract trait declarations', () => {
  1107. const code = `
  1108. pub trait Repository {
  1109. fn find(&self, id: &str) -> Option<Entity>;
  1110. fn save(&mut self, entity: Entity) -> Result<(), Error>;
  1111. }
  1112. `;
  1113. const result = extractFromSource('traits.rs', code);
  1114. const traitNode = result.nodes.find((n) => n.kind === 'trait');
  1115. expect(traitNode).toBeDefined();
  1116. expect(traitNode?.name).toBe('Repository');
  1117. });
  1118. it('should extract impl Trait for Type as implements edges', () => {
  1119. const code = `
  1120. pub struct MyCache {}
  1121. pub trait Cache {
  1122. fn get(&self, key: &str) -> Option<String>;
  1123. }
  1124. impl Cache for MyCache {
  1125. fn get(&self, key: &str) -> Option<String> {
  1126. None
  1127. }
  1128. }
  1129. `;
  1130. const result = extractFromSource('cache.rs', code);
  1131. // Should have an unresolved reference for implements
  1132. const implRef = result.unresolvedReferences.find(
  1133. (r) => r.referenceKind === 'implements' && r.referenceName === 'Cache'
  1134. );
  1135. expect(implRef).toBeDefined();
  1136. // The struct MyCache should be the source
  1137. const myCacheNode = result.nodes.find((n) => n.name === 'MyCache' && n.kind === 'struct');
  1138. expect(myCacheNode).toBeDefined();
  1139. expect(implRef?.fromNodeId).toBe(myCacheNode?.id);
  1140. });
  1141. it('qualifies methods of a generic or lifetime impl by the implementing type, not the trait (#1588)', () => {
  1142. const code = `
  1143. pub trait Source {
  1144. fn read(&mut self) -> usize;
  1145. }
  1146. pub struct FileSource { pub n: usize }
  1147. impl Source for FileSource {
  1148. fn read(&mut self) -> usize { self.n }
  1149. }
  1150. pub struct BufSource<T> { pub inner: T }
  1151. impl<T> Source for BufSource<T> {
  1152. fn read(&mut self) -> usize { 0 }
  1153. }
  1154. pub struct Parents<'a> { cur: &'a u32 }
  1155. impl<'a> Iterator for Parents<'a> {
  1156. type Item = u32;
  1157. fn next(&mut self) -> Option<u32> { None }
  1158. }
  1159. pub struct Wrapper { pub n: usize }
  1160. impl Source for &Wrapper {
  1161. fn read(&mut self) -> usize { 1 }
  1162. }
  1163. pub mod m { pub struct Scoped { pub n: usize } }
  1164. impl Source for m::Scoped {
  1165. fn read(&mut self) -> usize { 2 }
  1166. }
  1167. pub struct Own { pub n: usize }
  1168. impl From<u32> for Own {
  1169. fn from(n: u32) -> Self { Own { n: n as usize } }
  1170. }
  1171. `;
  1172. const result = extractFromSource('src.rs', code);
  1173. // Every impl method is qualified by the IMPLEMENTING type. Before, a
  1174. // parameterized implementing type (`BufSource<T>`, `Parents<'a>`, `&Wrapper`)
  1175. // left the trait's identifier as the only bare type_identifier child of the
  1176. // impl, so those methods were recorded as `Source::read` / `Iterator::next`.
  1177. const methodQns = result.nodes
  1178. .filter((n) => n.kind === 'method')
  1179. .map((n) => n.qualifiedName)
  1180. .sort();
  1181. expect(methodQns).toEqual([
  1182. 'BufSource::read',
  1183. 'FileSource::read',
  1184. 'Own::from',
  1185. 'Parents::next',
  1186. 'Scoped::read',
  1187. 'Source::read',
  1188. 'Wrapper::read',
  1189. ]);
  1190. // The trait's qualified name now names exactly one node: its declaration.
  1191. const traitRead = result.nodes.filter((n) => n.qualifiedName === 'Source::read');
  1192. expect(traitRead).toHaveLength(1);
  1193. expect(traitRead[0]!.startLine).toBe(3);
  1194. // The implements back-reference comes FROM the implementing type's node
  1195. // for every impl shape, named by the trait's full text.
  1196. const implementsFrom = (typeName: string): string[] => {
  1197. const typeNode = result.nodes.find((n) => n.name === typeName && n.kind === 'struct');
  1198. expect(typeNode, typeName).toBeDefined();
  1199. return result.unresolvedReferences
  1200. .filter((r) => r.referenceKind === 'implements' && r.fromNodeId === typeNode!.id)
  1201. .map((r) => r.referenceName);
  1202. };
  1203. expect(implementsFrom('FileSource')).toEqual(['Source']);
  1204. expect(implementsFrom('BufSource')).toEqual(['Source']);
  1205. expect(implementsFrom('Parents')).toEqual(['Iterator']);
  1206. expect(implementsFrom('Wrapper')).toEqual(['Source']);
  1207. expect(implementsFrom('Scoped')).toEqual(['Source']);
  1208. expect(implementsFrom('Own')).toEqual(['From<u32>']);
  1209. // …and the owner `contains` edge lands on the implementing type too.
  1210. const buf = result.nodes.find((n) => n.name === 'BufSource' && n.kind === 'struct')!;
  1211. const bufRead = result.nodes.find((n) => n.qualifiedName === 'BufSource::read')!;
  1212. expect(
  1213. result.edges.some((e) => e.kind === 'contains' && e.source === buf.id && e.target === bufRead.id)
  1214. ).toBe(true);
  1215. });
  1216. it('keeps the owner-field shape for `self.<field>.<method>()` and collapses every other receiver (#1585)', () => {
  1217. const code = `
  1218. pub struct Outer { pub inner: Inner, pub deep: Deep }
  1219. impl Outer {
  1220. pub fn run(&mut self) {
  1221. self.inner.run();
  1222. self.deep.inner.run();
  1223. self.make().run();
  1224. (self.inner).run();
  1225. self.run();
  1226. let local = Inner { n: 0 };
  1227. local.run();
  1228. }
  1229. }
  1230. `;
  1231. const result = extractFromSource('outer.rs', code);
  1232. const calls = result.unresolvedReferences
  1233. .filter((r) => r.referenceKind === 'calls')
  1234. .map((r) => r.referenceName);
  1235. // Exactly one call keeps the `self.<field>` prefix — the single-hop field
  1236. // receiver whose type the resolver can read off the owner struct.
  1237. expect(calls.filter((c) => c.startsWith('self.'))).toEqual(['self.inner.run']);
  1238. // A local receiver keeps its name as before…
  1239. expect(calls).toContain('local.run');
  1240. // …and the deeper chain, the call receiver, the parenthesized receiver and
  1241. // the bare `self` receiver all still collapse to the method name.
  1242. expect(calls.filter((c) => c === 'run')).toHaveLength(4);
  1243. expect(calls).toContain('make');
  1244. const outerRun = result.nodes.find((n) => n.qualifiedName === 'Outer::run');
  1245. expect(outerRun).toBeDefined();
  1246. const fieldRef = result.unresolvedReferences.find((r) => r.referenceName === 'self.inner.run');
  1247. expect(fieldRef?.fromNodeId).toBe(outerRun!.id);
  1248. expect(fieldRef?.line).toBe(5);
  1249. });
  1250. it('gives no receiver to an impl whose target names no single type', () => {
  1251. // A tuple / `dyn Trait` / primitive implementing type has no struct to
  1252. // hang the methods off, so they are extracted as plain functions — the
  1253. // pre-#1588 behavior for these shapes, minus the trait mis-qualification.
  1254. const code = `
  1255. pub trait Base { fn id(&self) -> u32; }
  1256. impl Base for (u32, u32) {
  1257. fn id(&self) -> u32 { 0 }
  1258. }
  1259. impl Base for dyn Base {
  1260. fn id(&self) -> u32 { 1 }
  1261. }
  1262. `;
  1263. const result = extractFromSource('src.rs', code);
  1264. const ids = result.nodes.filter((n) => n.name === 'id');
  1265. expect(ids.map((n) => n.qualifiedName).sort()).toEqual(['Base::id', 'id', 'id']);
  1266. expect(ids.filter((n) => n.kind === 'function')).toHaveLength(2);
  1267. expect(result.unresolvedReferences.filter((r) => r.referenceKind === 'implements')).toHaveLength(0);
  1268. });
  1269. it('should extract trait supertraits as extends references', () => {
  1270. const code = `
  1271. pub trait Display {}
  1272. pub trait Error: Display {
  1273. fn description(&self) -> &str;
  1274. }
  1275. `;
  1276. const result = extractFromSource('error.rs', code);
  1277. const extendsRef = result.unresolvedReferences.find(
  1278. (r) => r.referenceKind === 'extends' && r.referenceName === 'Display'
  1279. );
  1280. expect(extendsRef).toBeDefined();
  1281. const errorTrait = result.nodes.find((n) => n.name === 'Error' && n.kind === 'trait');
  1282. expect(errorTrait).toBeDefined();
  1283. expect(extendsRef?.fromNodeId).toBe(errorTrait?.id);
  1284. });
  1285. it('should not create implements edges for plain impl blocks', () => {
  1286. const code = `
  1287. pub struct Counter {
  1288. count: u32,
  1289. }
  1290. impl Counter {
  1291. pub fn new() -> Counter {
  1292. Counter { count: 0 }
  1293. }
  1294. pub fn increment(&mut self) {
  1295. self.count += 1;
  1296. }
  1297. }
  1298. `;
  1299. const result = extractFromSource('counter.rs', code);
  1300. // Should have no implements references (no trait involved)
  1301. const implRefs = result.unresolvedReferences.filter(
  1302. (r) => r.referenceKind === 'implements'
  1303. );
  1304. expect(implRefs).toHaveLength(0);
  1305. });
  1306. it('should extract union declarations and their impl edges', () => {
  1307. const code = `
  1308. pub union Reg {
  1309. pub raw: u32,
  1310. pub halves: [u16; 2],
  1311. }
  1312. pub trait Describe {
  1313. fn describe(&self) -> u32;
  1314. }
  1315. impl Describe for Reg {
  1316. fn describe(&self) -> u32 {
  1317. unsafe { self.raw }
  1318. }
  1319. }
  1320. `;
  1321. const result = extractFromSource('reg.rs', code);
  1322. // A union is a first-class type definition, not an alias — it must be a
  1323. // node, or the impl below has no source endpoint to hang off.
  1324. const reg = result.nodes.find((n) => n.name === 'Reg');
  1325. expect(reg).toBeDefined();
  1326. expect(reg?.kind).toBe('union');
  1327. const implRef = result.unresolvedReferences.find(
  1328. (r) => r.referenceKind === 'implements' && r.referenceName === 'Describe'
  1329. );
  1330. expect(implRef).toBeDefined();
  1331. expect(implRef?.fromNodeId).toBe(reg?.id);
  1332. // The impl's method attaches to the union, not to the file — without a Reg
  1333. // node it was an orphan whose qualifiedName pointed at a type that did not
  1334. // exist in the graph.
  1335. const implMethod = result.nodes.find(
  1336. (n) => n.kind === 'method' && n.qualifiedName?.includes('Reg')
  1337. );
  1338. expect(implMethod).toBeDefined();
  1339. expect(
  1340. result.edges.some(
  1341. (e) => e.kind === 'contains' && e.source === reg?.id && e.target === implMethod?.id
  1342. )
  1343. ).toBe(true);
  1344. });
  1345. });
  1346. describe('Java Extraction', () => {
  1347. it('should extract class declarations', () => {
  1348. const code = `
  1349. public class UserService {
  1350. private final UserRepository repository;
  1351. public UserService(UserRepository repository) {
  1352. this.repository = repository;
  1353. }
  1354. public User getUser(String id) {
  1355. return repository.findById(id);
  1356. }
  1357. }
  1358. `;
  1359. const result = extractFromSource('UserService.java', code);
  1360. const classNode = result.nodes.find((n) => n.kind === 'class');
  1361. expect(classNode).toBeDefined();
  1362. expect(classNode?.name).toBe('UserService');
  1363. expect(classNode?.visibility).toBe('public');
  1364. });
  1365. it('should extract method declarations', () => {
  1366. const code = `
  1367. public class Calculator {
  1368. public static int add(int a, int b) {
  1369. return a + b;
  1370. }
  1371. }
  1372. `;
  1373. const result = extractFromSource('Calculator.java', code);
  1374. const methodNode = result.nodes.find((n) => n.kind === 'method' && n.name === 'add');
  1375. expect(methodNode).toBeDefined();
  1376. expect(methodNode?.isStatic).toBe(true);
  1377. });
  1378. it('wraps top-level declarations in a namespace from package_declaration', () => {
  1379. const code = `
  1380. package com.example.foo;
  1381. public class Bar {
  1382. public String greet() { return "hi"; }
  1383. }
  1384. `;
  1385. const result = extractFromSource('Bar.java', code);
  1386. const ns = result.nodes.find((n) => n.kind === 'namespace');
  1387. expect(ns?.name).toBe('com.example.foo');
  1388. const cls = result.nodes.find((n) => n.kind === 'class' && n.name === 'Bar');
  1389. expect(cls?.qualifiedName).toBe('com.example.foo::Bar');
  1390. const greet = result.nodes.find((n) => n.kind === 'method' && n.name === 'greet');
  1391. expect(greet?.qualifiedName).toBe('com.example.foo::Bar::greet');
  1392. });
  1393. it('does not wrap when no package is declared', () => {
  1394. const code = `
  1395. public class Bar {
  1396. public String greet() { return "hi"; }
  1397. }
  1398. `;
  1399. const result = extractFromSource('Bar.java', code);
  1400. expect(result.nodes.find((n) => n.kind === 'namespace')).toBeUndefined();
  1401. const cls = result.nodes.find((n) => n.kind === 'class' && n.name === 'Bar');
  1402. expect(cls?.qualifiedName).toBe('Bar');
  1403. });
  1404. it('extracts anonymous-class overrides from `new T() { ... }`', () => {
  1405. // The pattern that breaks the trace through `strategy.foo()` in
  1406. // libraries like guava's Splitter: the lambda-returned anonymous
  1407. // class overrides abstract methods on the base, but without
  1408. // extracting those overrides the interface→impl synthesizer has
  1409. // nothing to bridge.
  1410. const code = `
  1411. package com.example;
  1412. abstract class Base {
  1413. abstract int compute(int x);
  1414. }
  1415. public class Factory {
  1416. public Base make() {
  1417. return new Base() {
  1418. @Override
  1419. int compute(int x) { return x + 1; }
  1420. };
  1421. }
  1422. }
  1423. `;
  1424. const result = extractFromSource('Factory.java', code);
  1425. const anon = result.nodes.find((n) => n.kind === 'class' && /Base\$anon@/.test(n.name));
  1426. expect(anon, 'anonymous Base subclass should be extracted as a class').toBeDefined();
  1427. const compute = result.nodes.find(
  1428. (n) => n.kind === 'method' && n.name === 'compute' && n.qualifiedName.includes('$anon@')
  1429. );
  1430. expect(compute, 'override method should be a method on the anon class').toBeDefined();
  1431. expect(compute!.qualifiedName).toContain('Factory::make::<Base$anon@');
  1432. expect(compute!.qualifiedName.endsWith('::compute')).toBe(true);
  1433. // Anon class must extend Base so Phase 5.5 (interface-impl) can bridge.
  1434. const extendsRef = result.unresolvedReferences.find(
  1435. (r) => r.referenceKind === 'extends' && r.referenceName === 'Base' && r.fromNodeId === anon!.id
  1436. );
  1437. expect(extendsRef, 'anon class should carry an `extends Base` reference').toBeDefined();
  1438. // The enclosing `make` method still emits an instantiates edge to Base —
  1439. // anon extraction must not swallow that signal.
  1440. const instantiatesRef = result.unresolvedReferences.find(
  1441. (r) => r.referenceKind === 'instantiates' && r.referenceName === 'Base'
  1442. );
  1443. expect(instantiatesRef, 'enclosing method should still instantiate Base').toBeDefined();
  1444. });
  1445. it('extracts anonymous-class overrides inside a lambda body', () => {
  1446. // The exact guava pattern: a lambda is passed to a constructor, and the
  1447. // lambda body returns `new T() { @Override ... }`. The anon class must
  1448. // still surface even though it sits inside a lambda_expression node.
  1449. const code = `
  1450. package com.example;
  1451. interface Strategy {
  1452. java.util.Iterator<String> iterator(String s);
  1453. }
  1454. abstract class BaseIter implements java.util.Iterator<String> {
  1455. abstract int separatorStart(int start);
  1456. }
  1457. public class Splitter {
  1458. private final Strategy strategy;
  1459. public Splitter(Strategy s) { this.strategy = s; }
  1460. public static Splitter on(char c) {
  1461. return new Splitter((seq) ->
  1462. new BaseIter() {
  1463. @Override
  1464. int separatorStart(int start) { return start + 1; }
  1465. @Override public boolean hasNext() { return false; }
  1466. @Override public String next() { return null; }
  1467. });
  1468. }
  1469. }
  1470. `;
  1471. const result = extractFromSource('Splitter.java', code);
  1472. const anon = result.nodes.find((n) => n.kind === 'class' && /BaseIter\$anon@/.test(n.name));
  1473. expect(anon, 'anon BaseIter inside the lambda body should be extracted').toBeDefined();
  1474. const sepStart = result.nodes.find(
  1475. (n) =>
  1476. n.kind === 'method' &&
  1477. n.name === 'separatorStart' &&
  1478. n.qualifiedName.includes('$anon@')
  1479. );
  1480. expect(sepStart, 'override inside the lambda-returned anon class should be a method node').toBeDefined();
  1481. });
  1482. });
  1483. describe('C# Extraction', () => {
  1484. it('should extract class declarations', () => {
  1485. const code = `
  1486. public class OrderService
  1487. {
  1488. private readonly IOrderRepository _repository;
  1489. public OrderService(IOrderRepository repository)
  1490. {
  1491. _repository = repository;
  1492. }
  1493. public async Task<Order> GetOrderAsync(string id)
  1494. {
  1495. return await _repository.FindByIdAsync(id);
  1496. }
  1497. }
  1498. `;
  1499. const result = extractFromSource('OrderService.cs', code);
  1500. const classNode = result.nodes.find((n) => n.kind === 'class');
  1501. expect(classNode).toBeDefined();
  1502. expect(classNode?.name).toBe('OrderService');
  1503. expect(classNode?.visibility).toBe('public');
  1504. });
  1505. it('indexes every record form with the right kind (#831)', () => {
  1506. // The grammar parses ALL record forms as record_declaration — there is no
  1507. // record_struct_declaration node — so the value-type forms are told apart
  1508. // by their `struct` keyword child. Positional one-liners have no body
  1509. // block and must still index (the no-body gate is for C/C++ forward
  1510. // declarations, not records).
  1511. const code = `
  1512. namespace Fixture;
  1513. public record SimplePositional(int A);
  1514. public record WithBody(int A) { public int DoubleIt() => A * 2; }
  1515. public record class ExplicitClassRec(string Name);
  1516. public record struct ValueRec(int X);
  1517. public readonly record struct ReadonlyRec(int X, int Y);
  1518. public record DerivedRec(int A, string B) : SimplePositional(A);
  1519. public record GenericRec<T>(T Value);
  1520. public partial record PartialRec(int A);
  1521. `;
  1522. const result = extractFromSource('Records.cs', code);
  1523. const kindOf = (name: string) => result.nodes.find((n) => n.name === name)?.kind;
  1524. expect(kindOf('SimplePositional')).toBe('class');
  1525. expect(kindOf('WithBody')).toBe('class');
  1526. expect(kindOf('ExplicitClassRec')).toBe('class');
  1527. expect(kindOf('DerivedRec')).toBe('class');
  1528. expect(kindOf('GenericRec')).toBe('class');
  1529. expect(kindOf('PartialRec')).toBe('class');
  1530. // Value-type records are structs, not classes.
  1531. expect(kindOf('ValueRec')).toBe('struct');
  1532. expect(kindOf('ReadonlyRec')).toBe('struct');
  1533. // Members of a bodied record still extract.
  1534. expect(kindOf('DoubleIt')).toBe('method');
  1535. });
  1536. it('indexes primary-constructor classes, including keyed-DI attribute params (#237)', () => {
  1537. // C# 12 primary constructors (`class Foo(IDep dep) { … }`) are parsed
  1538. // natively by the vendored tree-sitter-c-sharp 0.23.x grammar. The worst
  1539. // shape under the previous (older) grammar — an attribute-with-args on a
  1540. // ctor param (`[FromKeyedServices("primary")] …`, the ASP.NET keyed-DI
  1541. // pattern) — used to parse as an ERROR that swallowed the whole class, so
  1542. // the class and all its methods vanished. They now index in every case.
  1543. const code = `
  1544. public class DataService(IMemoryCache cache)
  1545. {
  1546. public void Warm() { }
  1547. }
  1548. public class InstanceService(InstanceManager m, ProfileManager p)
  1549. {
  1550. public void DeployAndLaunchAsync() { }
  1551. public void Deploy() { }
  1552. }
  1553. public partial class UpdateService(int x) : ILifetimeService
  1554. {
  1555. public void Run() { }
  1556. }
  1557. public class K1KeyedDi([FromKeyedServices("primary")] IMemoryCache cache)
  1558. {
  1559. public void Warm() { }
  1560. }
  1561. public record CatalogBrand(int Id, string Name);
  1562. `;
  1563. const result = extractFromSource('Services.cs', code);
  1564. const classNames = result.nodes.filter((n) => n.kind === 'class').map((n) => n.name);
  1565. expect(classNames).toContain('DataService');
  1566. expect(classNames).toContain('InstanceService');
  1567. expect(classNames).toContain('UpdateService'); // partial + base list
  1568. expect(classNames).toContain('K1KeyedDi'); // attribute-arg ctor param — used to vanish entirely
  1569. expect(classNames).toContain('CatalogBrand'); // record
  1570. const methods = result.nodes.filter((n) => n.kind === 'method').map((n) => n.name);
  1571. expect(methods).toContain('DeployAndLaunchAsync');
  1572. expect(methods).toContain('Deploy');
  1573. expect(methods).toContain('Run');
  1574. });
  1575. it('keeps a class indexable when a nested enum has #if-guarded members (#237)', () => {
  1576. // A `#if` directive inside an enum member list (the multi-targeting pattern
  1577. // in libraries like Newtonsoft.Json) makes the grammar emit an ERROR that,
  1578. // for a nested enum, detaches the enclosing class's member list — dropping
  1579. // most of the class's methods. A pre-parse pass blanks the directive lines
  1580. // (keeping both branches), so the class and all its methods still index.
  1581. const code = `
  1582. public class Reader
  1583. {
  1584. private enum ReadType
  1585. {
  1586. #if HAVE_DATE_TIME_OFFSET
  1587. ReadAsDateTimeOffset,
  1588. #endif
  1589. ReadAsDouble,
  1590. ReadAsString,
  1591. }
  1592. public void Open() { }
  1593. public void Close() { }
  1594. public int ReadInt() { return 0; }
  1595. }
  1596. `;
  1597. const result = extractFromSource('Reader.cs', code);
  1598. const methods = result.nodes.filter((n) => n.kind === 'method').map((n) => n.name);
  1599. // All three methods after the #if-bearing enum must survive.
  1600. expect(methods).toContain('Open');
  1601. expect(methods).toContain('Close');
  1602. expect(methods).toContain('ReadInt');
  1603. // Both enum branches are kept.
  1604. const enumMembers = result.nodes.filter((n) => n.kind === 'enum_member').map((n) => n.name);
  1605. expect(enumMembers).toContain('ReadAsDateTimeOffset');
  1606. expect(enumMembers).toContain('ReadAsDouble');
  1607. });
  1608. });
  1609. describe('PHP Extraction', () => {
  1610. it('should extract class declarations', () => {
  1611. const code = `<?php
  1612. class UserController
  1613. {
  1614. private UserService $userService;
  1615. public function __construct(UserService $userService)
  1616. {
  1617. $this->userService = $userService;
  1618. }
  1619. public function show(string $id): User
  1620. {
  1621. return $this->userService->find($id);
  1622. }
  1623. }
  1624. `;
  1625. const result = extractFromSource('UserController.php', code);
  1626. const classNode = result.nodes.find((n) => n.kind === 'class');
  1627. expect(classNode).toBeDefined();
  1628. expect(classNode?.name).toBe('UserController');
  1629. });
  1630. it('should extract class inheritance (extends) and interface implementation', () => {
  1631. const code = `<?php
  1632. class ChildController extends BaseController implements Serializable, JsonSerializable
  1633. {
  1634. public function serialize(): string
  1635. {
  1636. return json_encode($this);
  1637. }
  1638. }
  1639. `;
  1640. const result = extractFromSource('ChildController.php', code);
  1641. const classNode = result.nodes.find((n) => n.kind === 'class');
  1642. expect(classNode).toBeDefined();
  1643. expect(classNode?.name).toBe('ChildController');
  1644. const extendsRef = result.unresolvedReferences.find(
  1645. (r) => r.referenceKind === 'extends'
  1646. );
  1647. expect(extendsRef).toBeDefined();
  1648. expect(extendsRef?.referenceName).toBe('BaseController');
  1649. const implementsRefs = result.unresolvedReferences.filter(
  1650. (r) => r.referenceKind === 'implements'
  1651. );
  1652. expect(implementsRefs.length).toBe(2);
  1653. expect(implementsRefs.map((r) => r.referenceName)).toContain('Serializable');
  1654. expect(implementsRefs.map((r) => r.referenceName)).toContain('JsonSerializable');
  1655. });
  1656. });
  1657. describe('Swift Extraction', () => {
  1658. it('should extract class declarations', () => {
  1659. const code = `
  1660. public class NetworkManager {
  1661. private let session: URLSession
  1662. public init(session: URLSession = .shared) {
  1663. self.session = session
  1664. }
  1665. public func fetchData(from url: URL) async throws -> Data {
  1666. let (data, _) = try await session.data(from: url)
  1667. return data
  1668. }
  1669. }
  1670. `;
  1671. const result = extractFromSource('NetworkManager.swift', code);
  1672. const classNode = result.nodes.find((n) => n.kind === 'class');
  1673. expect(classNode).toBeDefined();
  1674. expect(classNode?.name).toBe('NetworkManager');
  1675. });
  1676. it('should extract function declarations', () => {
  1677. const code = `
  1678. func calculateSum(_ numbers: [Int]) -> Int {
  1679. return numbers.reduce(0, +)
  1680. }
  1681. public func formatCurrency(amount: Double) -> String {
  1682. return String(format: "$%.2f", amount)
  1683. }
  1684. `;
  1685. const result = extractFromSource('utils.swift', code);
  1686. const functions = result.nodes.filter((n) => n.kind === 'function');
  1687. expect(functions.length).toBeGreaterThanOrEqual(1);
  1688. });
  1689. it('should extract struct declarations', () => {
  1690. const code = `
  1691. public struct User {
  1692. let id: UUID
  1693. var name: String
  1694. var email: String
  1695. func displayName() -> String {
  1696. return name
  1697. }
  1698. }
  1699. `;
  1700. const result = extractFromSource('User.swift', code);
  1701. const structNode = result.nodes.find((n) => n.kind === 'struct');
  1702. expect(structNode).toBeDefined();
  1703. expect(structNode?.name).toBe('User');
  1704. });
  1705. it('should extract protocol declarations', () => {
  1706. const code = `
  1707. public protocol Repository {
  1708. associatedtype Entity
  1709. func find(id: String) async throws -> Entity?
  1710. func save(_ entity: Entity) async throws
  1711. }
  1712. `;
  1713. const result = extractFromSource('Repository.swift', code);
  1714. const protocolNode = result.nodes.find((n) => n.kind === 'interface');
  1715. expect(protocolNode).toBeDefined();
  1716. expect(protocolNode?.name).toBe('Repository');
  1717. });
  1718. it('should extract class inheritance and protocol conformance', () => {
  1719. const code = `
  1720. class DataRequest: Request {
  1721. func validate() {}
  1722. }
  1723. class UploadRequest: DataRequest, Sendable {
  1724. func upload() {}
  1725. }
  1726. enum AFError: Error {
  1727. case invalidURL
  1728. }
  1729. struct HTTPMethod: RawRepresentable {
  1730. let rawValue: String
  1731. }
  1732. protocol UploadConvertible: URLRequestConvertible {
  1733. func asURLRequest() throws -> URLRequest
  1734. }
  1735. `;
  1736. const result = extractFromSource('Inheritance.swift', code);
  1737. const extendsRefs = result.unresolvedReferences.filter(
  1738. (r) => r.referenceKind === 'extends'
  1739. );
  1740. // DataRequest extends Request
  1741. expect(extendsRefs.find((r) => r.referenceName === 'Request')).toBeDefined();
  1742. // UploadRequest extends DataRequest and Sendable
  1743. expect(extendsRefs.find((r) => r.referenceName === 'DataRequest')).toBeDefined();
  1744. expect(extendsRefs.find((r) => r.referenceName === 'Sendable')).toBeDefined();
  1745. // AFError extends Error
  1746. expect(extendsRefs.find((r) => r.referenceName === 'Error')).toBeDefined();
  1747. // HTTPMethod extends RawRepresentable
  1748. expect(extendsRefs.find((r) => r.referenceName === 'RawRepresentable')).toBeDefined();
  1749. // UploadConvertible extends URLRequestConvertible
  1750. expect(extendsRefs.find((r) => r.referenceName === 'URLRequestConvertible')).toBeDefined();
  1751. });
  1752. it('indexes Swift properties so they are findable: computed → property, stored → field, static → constant/variable (#1020)', () => {
  1753. const code = `
  1754. struct ReproConfig {
  1755. let reproStoredValue: Int
  1756. var reproComputedFlag: Bool {
  1757. reproStoredValue > 0
  1758. }
  1759. static let sharedLimit = 10
  1760. static var sharedCount = 0
  1761. func reproControlMethod() -> Bool {
  1762. reproComputedFlag
  1763. }
  1764. }
  1765. final class ReproService {
  1766. private let reproClassStored: String = "x"
  1767. var reproClassComputed: Int { reproClassStored.count }
  1768. }
  1769. `;
  1770. const result = extractFromSource('Repro.swift', code);
  1771. const byName = (name: string) => result.nodes.find((n) => n.name === name);
  1772. // Computed properties are the regression this fix targets: before #1020 they
  1773. // were dropped entirely, so search/explore returned nothing for them.
  1774. expect(byName('reproComputedFlag')?.kind).toBe('property');
  1775. expect(byName('reproClassComputed')?.kind).toBe('property');
  1776. // Stored instance properties stay `field` (fixed earlier in #708 — guard it).
  1777. expect(byName('reproStoredValue')?.kind).toBe('field');
  1778. expect(byName('reproClassStored')?.kind).toBe('field');
  1779. // `static let`/`static var` members remain shared constant/variable nodes.
  1780. expect(byName('sharedLimit')?.kind).toBe('constant');
  1781. expect(byName('sharedCount')?.kind).toBe('variable');
  1782. // The control method is unaffected.
  1783. expect(byName('reproControlMethod')?.kind).toBe('method');
  1784. });
  1785. it("attributes a computed property's getter calls to the property, not the type (SwiftUI body flow) (#1020)", () => {
  1786. const code = `
  1787. struct GreetingView {
  1788. let name: String
  1789. var body: some View {
  1790. let prefix = "Hi"
  1791. return VStack {
  1792. Text(greeting(prefix))
  1793. }
  1794. }
  1795. func greeting(_ p: String) -> String { p }
  1796. }
  1797. `;
  1798. const result = extractFromSource('View.swift', code);
  1799. const body = result.nodes.find((n) => n.kind === 'property' && n.name === 'body');
  1800. expect(body).toBeDefined();
  1801. // The getter's call to greeting() must originate from `body` (so a SwiftUI
  1802. // view's render flow is reachable through the property), not flatten onto the
  1803. // enclosing struct.
  1804. const callsFromBody = result.unresolvedReferences.filter(
  1805. (r) => r.fromNodeId === body!.id && r.referenceKind === 'calls'
  1806. );
  1807. expect(callsFromBody.some((r) => r.referenceName === 'greeting')).toBe(true);
  1808. // The getter is walked as a body, so a local declared inside it is NOT
  1809. // node-ified (locals are the data-flow frontier we leave uncovered). Before
  1810. // this fix the generic walker treated such a local as a struct `field`.
  1811. expect(result.nodes.find((n) => n.name === 'prefix')).toBeUndefined();
  1812. });
  1813. it('indexes a Swift protocol property requirement as a findable property (#1020)', () => {
  1814. const code = `
  1815. protocol Themable {
  1816. var accentColor: Color { get }
  1817. var title: String { get set }
  1818. }
  1819. `;
  1820. const result = extractFromSource('Themable.swift', code);
  1821. expect(result.nodes.find((n) => n.name === 'accentColor')?.kind).toBe('property');
  1822. expect(result.nodes.find((n) => n.name === 'title')?.kind).toBe('property');
  1823. });
  1824. });
  1825. describe('Kotlin Extraction', () => {
  1826. it('should extract class declarations', () => {
  1827. const code = `
  1828. class UserRepository(private val database: Database) {
  1829. fun findById(id: String): User? {
  1830. return database.query("SELECT * FROM users WHERE id = ?", id)
  1831. }
  1832. suspend fun save(user: User) {
  1833. database.insert(user)
  1834. }
  1835. }
  1836. `;
  1837. const result = extractFromSource('UserRepository.kt', code);
  1838. const classNode = result.nodes.find((n) => n.kind === 'class');
  1839. expect(classNode).toBeDefined();
  1840. expect(classNode?.name).toBe('UserRepository');
  1841. });
  1842. it('should extract function declarations', () => {
  1843. const code = `
  1844. fun calculateTotal(items: List<Item>): Double {
  1845. return items.sumOf { it.price }
  1846. }
  1847. suspend fun fetchUserData(userId: String): User {
  1848. return api.getUser(userId)
  1849. }
  1850. `;
  1851. const result = extractFromSource('utils.kt', code);
  1852. const functions = result.nodes.filter((n) => n.kind === 'function');
  1853. expect(functions.length).toBeGreaterThanOrEqual(1);
  1854. });
  1855. it('should detect suspend functions as async', () => {
  1856. const code = `
  1857. suspend fun loadData(): List<String> {
  1858. delay(1000)
  1859. return listOf("a", "b", "c")
  1860. }
  1861. `;
  1862. const result = extractFromSource('loader.kt', code);
  1863. const funcNode = result.nodes.find((n) => n.kind === 'function');
  1864. expect(funcNode).toBeDefined();
  1865. expect(funcNode?.isAsync).toBe(true);
  1866. });
  1867. it('should extract fun interface declarations', () => {
  1868. const code = `
  1869. fun interface OnObjectRetainedListener {
  1870. fun onObjectRetained()
  1871. }
  1872. `;
  1873. const result = extractFromSource('listener.kt', code);
  1874. const ifaceNode = result.nodes.find((n) => n.kind === 'interface');
  1875. expect(ifaceNode).toBeDefined();
  1876. expect(ifaceNode?.name).toBe('OnObjectRetainedListener');
  1877. const methodNode = result.nodes.find((n) => n.kind === 'method');
  1878. expect(methodNode).toBeDefined();
  1879. expect(methodNode?.name).toBe('onObjectRetained');
  1880. expect(methodNode?.qualifiedName).toBe('OnObjectRetainedListener::onObjectRetained');
  1881. });
  1882. it('should extract complex fun interface with nested classes', () => {
  1883. const code = `
  1884. fun interface EventListener {
  1885. fun onEvent(event: Event)
  1886. sealed class Event {
  1887. class DumpingHeap : Event()
  1888. }
  1889. }
  1890. `;
  1891. const result = extractFromSource('events.kt', code);
  1892. const ifaceNode = result.nodes.find((n) => n.kind === 'interface');
  1893. expect(ifaceNode).toBeDefined();
  1894. expect(ifaceNode?.name).toBe('EventListener');
  1895. // Nested sealed class should still be extracted (as sibling due to grammar limitations)
  1896. const eventClass = result.nodes.find((n) => n.kind === 'class' && n.name === 'Event');
  1897. expect(eventClass).toBeDefined();
  1898. const dumpingHeap = result.nodes.find((n) => n.kind === 'class' && n.name === 'DumpingHeap');
  1899. expect(dumpingHeap).toBeDefined();
  1900. });
  1901. it('should not affect regular function declarations', () => {
  1902. const code = `
  1903. fun interface MyCallback {
  1904. fun invoke(value: Int)
  1905. }
  1906. fun regularFunction(): String {
  1907. return "hello"
  1908. }
  1909. `;
  1910. const result = extractFromSource('mixed.kt', code);
  1911. const ifaceNode = result.nodes.find((n) => n.kind === 'interface');
  1912. expect(ifaceNode).toBeDefined();
  1913. expect(ifaceNode?.name).toBe('MyCallback');
  1914. const funcNode = result.nodes.find((n) => n.kind === 'function');
  1915. expect(funcNode).toBeDefined();
  1916. expect(funcNode?.name).toBe('regularFunction');
  1917. });
  1918. it('should extract fun interface with annotation on method (Pattern 2b)', () => {
  1919. // When the SAM method has annotations like @Throws, tree-sitter produces a different
  1920. // misparse: function_declaration > ERROR("interface Name {") instead of
  1921. // function_declaration > user_type("interface"). This is the OkHttp Interceptor pattern.
  1922. const code = `
  1923. import java.io.IOException
  1924. fun interface Interceptor {
  1925. @Throws(IOException::class)
  1926. fun intercept(chain: Chain): Response
  1927. }
  1928. `;
  1929. const result = extractFromSource('interceptor.kt', code);
  1930. const ifaceNode = result.nodes.find((n) => n.kind === 'interface');
  1931. expect(ifaceNode).toBeDefined();
  1932. expect(ifaceNode?.name).toBe('Interceptor');
  1933. });
  1934. it('should extract methods from interface with nested fun interface', () => {
  1935. // When an interface contains a nested `fun interface`, tree-sitter misparsed
  1936. // the parent body as ERROR. Methods inside should still be extracted.
  1937. const code = `
  1938. interface WebSocket {
  1939. fun request(): Request
  1940. fun send(text: String): Boolean
  1941. fun cancel()
  1942. fun interface Factory {
  1943. fun newWebSocket(request: Request): WebSocket
  1944. }
  1945. }
  1946. `;
  1947. const result = extractFromSource('websocket.kt', code);
  1948. const wsIface = result.nodes.find((n) => n.kind === 'interface' && n.name === 'WebSocket');
  1949. expect(wsIface).toBeDefined();
  1950. const methods = result.nodes.filter((n) => n.kind === 'method' && n.qualifiedName?.startsWith('WebSocket::'));
  1951. const methodNames = methods.map((m) => m.name);
  1952. expect(methodNames).toContain('request');
  1953. expect(methodNames).toContain('send');
  1954. expect(methodNames).toContain('cancel');
  1955. });
  1956. it('wraps top-level declarations in a namespace from package_header', () => {
  1957. const code = `
  1958. package com.example.foo
  1959. class Bar {
  1960. fun greet(): String = "hi"
  1961. }
  1962. fun util(): Int = 42
  1963. `;
  1964. const result = extractFromSource('Bar.kt', code);
  1965. const ns = result.nodes.find((n) => n.kind === 'namespace');
  1966. expect(ns?.name).toBe('com.example.foo');
  1967. const cls = result.nodes.find((n) => n.kind === 'class' && n.name === 'Bar');
  1968. expect(cls?.qualifiedName).toBe('com.example.foo::Bar');
  1969. const greet = result.nodes.find((n) => n.kind === 'method' && n.name === 'greet');
  1970. expect(greet?.qualifiedName).toBe('com.example.foo::Bar::greet');
  1971. const util = result.nodes.find((n) => n.kind === 'function' && n.name === 'util');
  1972. expect(util?.qualifiedName).toBe('com.example.foo::util');
  1973. });
  1974. it('handles a single-segment package', () => {
  1975. const code = `
  1976. package foo
  1977. class Bar
  1978. `;
  1979. const result = extractFromSource('Bar.kt', code);
  1980. const cls = result.nodes.find((n) => n.kind === 'class' && n.name === 'Bar');
  1981. expect(cls?.qualifiedName).toBe('foo::Bar');
  1982. });
  1983. it('does not wrap when no package is declared', () => {
  1984. const code = `
  1985. class Bar {
  1986. fun greet() = "hi"
  1987. }
  1988. `;
  1989. const result = extractFromSource('Bar.kt', code);
  1990. expect(result.nodes.find((n) => n.kind === 'namespace')).toBeUndefined();
  1991. const cls = result.nodes.find((n) => n.kind === 'class' && n.name === 'Bar');
  1992. expect(cls?.qualifiedName).toBe('Bar');
  1993. });
  1994. });
  1995. describe('Dart Extraction', () => {
  1996. it('should extract class declarations', () => {
  1997. const code = `
  1998. class UserService {
  1999. final Database _db;
  2000. Future<User> findById(String id) async {
  2001. return await _db.query(id);
  2002. }
  2003. void _privateMethod() {}
  2004. }
  2005. `;
  2006. const result = extractFromSource('service.dart', code);
  2007. const classNode = result.nodes.find((n) => n.kind === 'class');
  2008. expect(classNode).toBeDefined();
  2009. expect(classNode?.name).toBe('UserService');
  2010. expect(classNode?.visibility).toBe('public');
  2011. const methodNodes = result.nodes.filter((n) => n.kind === 'method');
  2012. expect(methodNodes.length).toBeGreaterThanOrEqual(2);
  2013. const findById = methodNodes.find((m) => m.name === 'findById');
  2014. expect(findById).toBeDefined();
  2015. expect(findById?.isAsync).toBe(true);
  2016. const privateMethod = methodNodes.find((m) => m.name === '_privateMethod');
  2017. expect(privateMethod).toBeDefined();
  2018. expect(privateMethod?.visibility).toBe('private');
  2019. // Dart models a method body as a SIBLING of the signature, so the method
  2020. // node must be extended to span its body (not just the signature line) —
  2021. // required for body-level analysis (callees, the callback synthesizer).
  2022. expect(findById!.endLine).toBeGreaterThan(findById!.startLine);
  2023. });
  2024. it('should extract top-level function declarations', () => {
  2025. const code = `
  2026. void topLevelFunction(String name) {
  2027. print(name);
  2028. }
  2029. `;
  2030. const result = extractFromSource('utils.dart', code);
  2031. const funcNode = result.nodes.find((n) => n.kind === 'function');
  2032. expect(funcNode).toBeDefined();
  2033. expect(funcNode?.name).toBe('topLevelFunction');
  2034. expect(funcNode?.language).toBe('dart');
  2035. });
  2036. it('should extract enum declarations', () => {
  2037. const code = `
  2038. enum Status { active, inactive, pending }
  2039. `;
  2040. const result = extractFromSource('models.dart', code);
  2041. const enumNode = result.nodes.find((n) => n.kind === 'enum');
  2042. expect(enumNode).toBeDefined();
  2043. expect(enumNode?.name).toBe('Status');
  2044. });
  2045. it('should extract mixin declarations', () => {
  2046. const code = `
  2047. mixin LoggerMixin {
  2048. void log(String message) {}
  2049. }
  2050. `;
  2051. const result = extractFromSource('mixins.dart', code);
  2052. const classNode = result.nodes.find((n) => n.kind === 'class');
  2053. expect(classNode).toBeDefined();
  2054. expect(classNode?.name).toBe('LoggerMixin');
  2055. const methodNode = result.nodes.find((n) => n.kind === 'method');
  2056. expect(methodNode).toBeDefined();
  2057. expect(methodNode?.name).toBe('log');
  2058. });
  2059. it('should extract extension declarations', () => {
  2060. const code = `
  2061. extension StringExt on String {
  2062. bool get isBlank => trim().isEmpty;
  2063. }
  2064. `;
  2065. const result = extractFromSource('extensions.dart', code);
  2066. const classNode = result.nodes.find((n) => n.kind === 'class');
  2067. expect(classNode).toBeDefined();
  2068. expect(classNode?.name).toBe('StringExt');
  2069. });
  2070. it('should detect static methods', () => {
  2071. const code = `
  2072. class Utils {
  2073. static void doWork() {}
  2074. }
  2075. `;
  2076. const result = extractFromSource('utils.dart', code);
  2077. const methodNode = result.nodes.find((n) => n.kind === 'method');
  2078. expect(methodNode).toBeDefined();
  2079. expect(methodNode?.name).toBe('doWork');
  2080. expect(methodNode?.isStatic).toBe(true);
  2081. });
  2082. it('should detect async functions', () => {
  2083. const code = `
  2084. Future<String> fetchData() async {
  2085. return await http.get('/data');
  2086. }
  2087. `;
  2088. const result = extractFromSource('api.dart', code);
  2089. const funcNode = result.nodes.find((n) => n.kind === 'function');
  2090. expect(funcNode).toBeDefined();
  2091. expect(funcNode?.name).toBe('fetchData');
  2092. expect(funcNode?.isAsync).toBe(true);
  2093. });
  2094. it('should detect private visibility via underscore convention', () => {
  2095. const code = `
  2096. void _privateHelper() {}
  2097. void publicFunction() {}
  2098. `;
  2099. const result = extractFromSource('helpers.dart', code);
  2100. const functions = result.nodes.filter((n) => n.kind === 'function');
  2101. const privateFunc = functions.find((f) => f.name === '_privateHelper');
  2102. const publicFunc = functions.find((f) => f.name === 'publicFunction');
  2103. expect(privateFunc?.visibility).toBe('private');
  2104. expect(publicFunc?.visibility).toBe('public');
  2105. });
  2106. });
  2107. describe('Import Extraction', () => {
  2108. describe('TypeScript/JavaScript imports', () => {
  2109. it('should extract default imports', () => {
  2110. const code = `import React from 'react';`;
  2111. const result = extractFromSource('app.tsx', code);
  2112. const importNode = result.nodes.find((n) => n.kind === 'import');
  2113. expect(importNode).toBeDefined();
  2114. expect(importNode?.name).toBe('react');
  2115. expect(importNode?.signature).toBe("import React from 'react';");
  2116. });
  2117. it('should extract named imports', () => {
  2118. const code = `import { Bug, Database } from '@phosphor-icons/react';`;
  2119. const result = extractFromSource('icons.tsx', code);
  2120. const importNode = result.nodes.find((n) => n.kind === 'import');
  2121. expect(importNode).toBeDefined();
  2122. expect(importNode?.name).toBe('@phosphor-icons/react');
  2123. expect(importNode?.signature).toContain('Bug');
  2124. expect(importNode?.signature).toContain('Database');
  2125. });
  2126. it('should extract namespace imports', () => {
  2127. const code = `import * as Icons from '@phosphor-icons/react';`;
  2128. const result = extractFromSource('icons.tsx', code);
  2129. const importNode = result.nodes.find((n) => n.kind === 'import');
  2130. expect(importNode).toBeDefined();
  2131. expect(importNode?.name).toBe('@phosphor-icons/react');
  2132. expect(importNode?.signature).toContain('* as Icons');
  2133. });
  2134. it('should extract side-effect imports', () => {
  2135. const code = `import './styles.css';`;
  2136. const result = extractFromSource('app.tsx', code);
  2137. const importNode = result.nodes.find((n) => n.kind === 'import');
  2138. expect(importNode).toBeDefined();
  2139. expect(importNode?.name).toBe('./styles.css');
  2140. });
  2141. it('should extract mixed imports (default + named)', () => {
  2142. const code = `import React, { useState, useEffect } from 'react';`;
  2143. const result = extractFromSource('app.tsx', code);
  2144. const importNode = result.nodes.find((n) => n.kind === 'import');
  2145. expect(importNode).toBeDefined();
  2146. expect(importNode?.name).toBe('react');
  2147. expect(importNode?.signature).toContain('React');
  2148. expect(importNode?.signature).toContain('useState');
  2149. expect(importNode?.signature).toContain('useEffect');
  2150. });
  2151. it('should extract multiple import statements', () => {
  2152. const code = `
  2153. import React from 'react';
  2154. import { Button } from './components';
  2155. import './styles.css';
  2156. `;
  2157. const result = extractFromSource('app.tsx', code);
  2158. const importNodes = result.nodes.filter((n) => n.kind === 'import');
  2159. expect(importNodes.length).toBe(3);
  2160. const names = importNodes.map((n) => n.name);
  2161. expect(names).toContain('react');
  2162. expect(names).toContain('./components');
  2163. expect(names).toContain('./styles.css');
  2164. });
  2165. it('should extract type imports', () => {
  2166. const code = `import type { FC, ReactNode } from 'react';`;
  2167. const result = extractFromSource('types.ts', code);
  2168. const importNode = result.nodes.find((n) => n.kind === 'import');
  2169. expect(importNode).toBeDefined();
  2170. expect(importNode?.name).toBe('react');
  2171. expect(importNode?.signature).toContain('type');
  2172. expect(importNode?.signature).toContain('FC');
  2173. });
  2174. it('should extract aliased named imports', () => {
  2175. const code = `import { useState as useStateAlias } from 'react';`;
  2176. const result = extractFromSource('hooks.ts', code);
  2177. const importNode = result.nodes.find((n) => n.kind === 'import');
  2178. expect(importNode).toBeDefined();
  2179. expect(importNode?.name).toBe('react');
  2180. expect(importNode?.signature).toContain('useState');
  2181. expect(importNode?.signature).toContain('useStateAlias');
  2182. });
  2183. it('should extract relative path imports', () => {
  2184. const code = `import { helper } from '../utils/helper';`;
  2185. const result = extractFromSource('components/Button.tsx', code);
  2186. const importNode = result.nodes.find((n) => n.kind === 'import');
  2187. expect(importNode).toBeDefined();
  2188. expect(importNode?.name).toBe('../utils/helper');
  2189. expect(importNode?.signature).toContain('helper');
  2190. });
  2191. });
  2192. describe('Python imports', () => {
  2193. it('should extract simple import statement', () => {
  2194. const code = `import json`;
  2195. const result = extractFromSource('utils.py', code);
  2196. const importNode = result.nodes.find((n) => n.kind === 'import');
  2197. expect(importNode).toBeDefined();
  2198. expect(importNode?.name).toBe('json');
  2199. });
  2200. it('should extract from import statement', () => {
  2201. const code = `from os import path`;
  2202. const result = extractFromSource('utils.py', code);
  2203. const importNode = result.nodes.find((n) => n.kind === 'import');
  2204. expect(importNode).toBeDefined();
  2205. expect(importNode?.name).toBe('os');
  2206. expect(importNode?.signature).toContain('path');
  2207. });
  2208. it('should extract multiple imports from same module', () => {
  2209. const code = `from typing import List, Dict, Optional`;
  2210. const result = extractFromSource('types.py', code);
  2211. const importNode = result.nodes.find((n) => n.kind === 'import');
  2212. expect(importNode).toBeDefined();
  2213. expect(importNode?.name).toBe('typing');
  2214. expect(importNode?.signature).toContain('List');
  2215. expect(importNode?.signature).toContain('Dict');
  2216. });
  2217. it('should extract multiple import statements', () => {
  2218. const code = `
  2219. import os
  2220. import sys
  2221. `;
  2222. const result = extractFromSource('main.py', code);
  2223. const importNodes = result.nodes.filter((n) => n.kind === 'import');
  2224. expect(importNodes.length).toBe(2);
  2225. const names = importNodes.map((n) => n.name);
  2226. expect(names).toContain('os');
  2227. expect(names).toContain('sys');
  2228. });
  2229. it('should extract aliased import', () => {
  2230. const code = `import numpy as np`;
  2231. const result = extractFromSource('data.py', code);
  2232. const importNode = result.nodes.find((n) => n.kind === 'import');
  2233. expect(importNode).toBeDefined();
  2234. expect(importNode?.name).toBe('numpy');
  2235. expect(importNode?.signature).toContain('as np');
  2236. });
  2237. it('should extract relative import', () => {
  2238. const code = `from .utils import helper`;
  2239. const result = extractFromSource('module.py', code);
  2240. const importNode = result.nodes.find((n) => n.kind === 'import');
  2241. expect(importNode).toBeDefined();
  2242. expect(importNode?.name).toBe('.utils');
  2243. expect(importNode?.signature).toContain('helper');
  2244. });
  2245. it('should extract wildcard import', () => {
  2246. const code = `from typing import *`;
  2247. const result = extractFromSource('types.py', code);
  2248. const importNode = result.nodes.find((n) => n.kind === 'import');
  2249. expect(importNode).toBeDefined();
  2250. expect(importNode?.name).toBe('typing');
  2251. expect(importNode?.signature).toContain('*');
  2252. });
  2253. });
  2254. describe('Rust imports', () => {
  2255. it('should extract simple use declaration', () => {
  2256. const code = `use std::io;`;
  2257. const result = extractFromSource('main.rs', code);
  2258. const importNode = result.nodes.find((n) => n.kind === 'import');
  2259. expect(importNode).toBeDefined();
  2260. expect(importNode?.name).toBe('std');
  2261. expect(importNode?.signature).toBe('use std::io;');
  2262. });
  2263. it('should extract scoped use list', () => {
  2264. const code = `use std::{ffi::OsStr, io, path::Path};`;
  2265. const result = extractFromSource('main.rs', code);
  2266. const importNode = result.nodes.find((n) => n.kind === 'import');
  2267. expect(importNode).toBeDefined();
  2268. expect(importNode?.name).toBe('std');
  2269. expect(importNode?.signature).toContain('ffi::OsStr');
  2270. expect(importNode?.signature).toContain('path::Path');
  2271. });
  2272. it('should extract crate imports', () => {
  2273. const code = `use crate::error::Error;`;
  2274. const result = extractFromSource('lib.rs', code);
  2275. const importNode = result.nodes.find((n) => n.kind === 'import');
  2276. expect(importNode).toBeDefined();
  2277. expect(importNode?.name).toBe('crate');
  2278. });
  2279. it('should extract super imports', () => {
  2280. const code = `use super::utils;`;
  2281. const result = extractFromSource('submod.rs', code);
  2282. const importNode = result.nodes.find((n) => n.kind === 'import');
  2283. expect(importNode).toBeDefined();
  2284. expect(importNode?.name).toBe('super');
  2285. });
  2286. it('should extract external crate imports', () => {
  2287. const code = `use serde::{Serialize, Deserialize};`;
  2288. const result = extractFromSource('types.rs', code);
  2289. const importNode = result.nodes.find((n) => n.kind === 'import');
  2290. expect(importNode).toBeDefined();
  2291. expect(importNode?.name).toBe('serde');
  2292. expect(importNode?.signature).toContain('Serialize');
  2293. expect(importNode?.signature).toContain('Deserialize');
  2294. });
  2295. });
  2296. describe('Go imports', () => {
  2297. it('should extract single import', () => {
  2298. const code = `
  2299. package main
  2300. import "fmt"
  2301. `;
  2302. const result = extractFromSource('main.go', code);
  2303. const importNode = result.nodes.find((n) => n.kind === 'import');
  2304. expect(importNode).toBeDefined();
  2305. expect(importNode?.name).toBe('fmt');
  2306. });
  2307. it('should extract grouped imports', () => {
  2308. const code = `
  2309. package main
  2310. import (
  2311. "fmt"
  2312. "os"
  2313. "encoding/json"
  2314. )
  2315. `;
  2316. const result = extractFromSource('main.go', code);
  2317. const importNodes = result.nodes.filter((n) => n.kind === 'import');
  2318. expect(importNodes.length).toBe(3);
  2319. const names = importNodes.map((n) => n.name);
  2320. expect(names).toContain('fmt');
  2321. expect(names).toContain('os');
  2322. expect(names).toContain('encoding/json');
  2323. });
  2324. it('should extract aliased import', () => {
  2325. const code = `
  2326. package main
  2327. import f "fmt"
  2328. `;
  2329. const result = extractFromSource('main.go', code);
  2330. const importNode = result.nodes.find((n) => n.kind === 'import');
  2331. expect(importNode).toBeDefined();
  2332. expect(importNode?.name).toBe('fmt');
  2333. expect(importNode?.signature).toContain('f');
  2334. });
  2335. it('should extract dot import', () => {
  2336. const code = `
  2337. package main
  2338. import . "math"
  2339. `;
  2340. const result = extractFromSource('main.go', code);
  2341. const importNode = result.nodes.find((n) => n.kind === 'import');
  2342. expect(importNode).toBeDefined();
  2343. expect(importNode?.name).toBe('math');
  2344. expect(importNode?.signature).toContain('.');
  2345. });
  2346. it('should extract blank import', () => {
  2347. const code = `
  2348. package main
  2349. import _ "github.com/go-sql-driver/mysql"
  2350. `;
  2351. const result = extractFromSource('main.go', code);
  2352. const importNode = result.nodes.find((n) => n.kind === 'import');
  2353. expect(importNode).toBeDefined();
  2354. expect(importNode?.name).toBe('github.com/go-sql-driver/mysql');
  2355. expect(importNode?.signature).toContain('_');
  2356. });
  2357. });
  2358. describe('Swift imports', () => {
  2359. it('should extract simple import', () => {
  2360. const code = `import Foundation`;
  2361. const result = extractFromSource('main.swift', code);
  2362. const importNode = result.nodes.find((n) => n.kind === 'import');
  2363. expect(importNode).toBeDefined();
  2364. expect(importNode?.name).toBe('Foundation');
  2365. expect(importNode?.signature).toBe('import Foundation');
  2366. });
  2367. it('should extract @testable import', () => {
  2368. const code = `@testable import Alamofire`;
  2369. const result = extractFromSource('Tests.swift', code);
  2370. const importNode = result.nodes.find((n) => n.kind === 'import');
  2371. expect(importNode).toBeDefined();
  2372. expect(importNode?.name).toBe('Alamofire');
  2373. expect(importNode?.signature).toContain('@testable');
  2374. });
  2375. it('should extract @preconcurrency import', () => {
  2376. const code = `@preconcurrency import Security`;
  2377. const result = extractFromSource('Auth.swift', code);
  2378. const importNode = result.nodes.find((n) => n.kind === 'import');
  2379. expect(importNode).toBeDefined();
  2380. expect(importNode?.name).toBe('Security');
  2381. });
  2382. it('should extract multiple imports', () => {
  2383. const code = `
  2384. import Foundation
  2385. import UIKit
  2386. import Alamofire
  2387. `;
  2388. const result = extractFromSource('App.swift', code);
  2389. const importNodes = result.nodes.filter((n) => n.kind === 'import');
  2390. expect(importNodes.length).toBe(3);
  2391. const names = importNodes.map((n) => n.name);
  2392. expect(names).toContain('Foundation');
  2393. expect(names).toContain('UIKit');
  2394. expect(names).toContain('Alamofire');
  2395. });
  2396. });
  2397. describe('Kotlin imports', () => {
  2398. it('should extract simple import', () => {
  2399. const code = `import java.io.IOException`;
  2400. const result = extractFromSource('Main.kt', code);
  2401. const importNode = result.nodes.find((n) => n.kind === 'import');
  2402. expect(importNode).toBeDefined();
  2403. expect(importNode?.name).toBe('java.io.IOException');
  2404. expect(importNode?.signature).toBe('import java.io.IOException');
  2405. });
  2406. it('should extract aliased import', () => {
  2407. const code = `import okhttp3.Request.Builder as RequestBuilder`;
  2408. const result = extractFromSource('Utils.kt', code);
  2409. const importNode = result.nodes.find((n) => n.kind === 'import');
  2410. expect(importNode).toBeDefined();
  2411. expect(importNode?.name).toBe('okhttp3.Request.Builder');
  2412. expect(importNode?.signature).toContain('as RequestBuilder');
  2413. });
  2414. it('should extract wildcard import', () => {
  2415. const code = `import java.util.concurrent.TimeUnit.*`;
  2416. const result = extractFromSource('Time.kt', code);
  2417. const importNode = result.nodes.find((n) => n.kind === 'import');
  2418. expect(importNode).toBeDefined();
  2419. expect(importNode?.name).toBe('java.util.concurrent.TimeUnit');
  2420. expect(importNode?.signature).toContain('.*');
  2421. });
  2422. it('should extract multiple imports', () => {
  2423. const code = `
  2424. import java.io.IOException
  2425. import kotlin.test.assertFailsWith
  2426. import okhttp3.OkHttpClient
  2427. `;
  2428. const result = extractFromSource('Test.kt', code);
  2429. const importNodes = result.nodes.filter((n) => n.kind === 'import');
  2430. expect(importNodes.length).toBe(3);
  2431. const names = importNodes.map((n) => n.name);
  2432. expect(names).toContain('java.io.IOException');
  2433. expect(names).toContain('kotlin.test.assertFailsWith');
  2434. expect(names).toContain('okhttp3.OkHttpClient');
  2435. });
  2436. });
  2437. describe('Java imports', () => {
  2438. it('should extract simple import', () => {
  2439. const code = `import java.util.List;`;
  2440. const result = extractFromSource('Main.java', code);
  2441. const importNode = result.nodes.find((n) => n.kind === 'import');
  2442. expect(importNode).toBeDefined();
  2443. expect(importNode?.name).toBe('java.util.List');
  2444. expect(importNode?.signature).toBe('import java.util.List;');
  2445. });
  2446. it('should extract static import', () => {
  2447. const code = `import static java.util.Collections.emptyList;`;
  2448. const result = extractFromSource('Utils.java', code);
  2449. const importNode = result.nodes.find((n) => n.kind === 'import');
  2450. expect(importNode).toBeDefined();
  2451. expect(importNode?.name).toBe('java.util.Collections.emptyList');
  2452. expect(importNode?.signature).toContain('static');
  2453. });
  2454. it('should extract wildcard import', () => {
  2455. const code = `import java.util.*;`;
  2456. const result = extractFromSource('App.java', code);
  2457. const importNode = result.nodes.find((n) => n.kind === 'import');
  2458. expect(importNode).toBeDefined();
  2459. expect(importNode?.name).toBe('java.util');
  2460. expect(importNode?.signature).toContain('.*');
  2461. });
  2462. it('should extract nested class import', () => {
  2463. const code = `import java.util.Map.Entry;`;
  2464. const result = extractFromSource('MapUtil.java', code);
  2465. const importNode = result.nodes.find((n) => n.kind === 'import');
  2466. expect(importNode).toBeDefined();
  2467. expect(importNode?.name).toBe('java.util.Map.Entry');
  2468. });
  2469. it('should extract multiple imports', () => {
  2470. const code = `
  2471. import java.util.List;
  2472. import java.util.Map;
  2473. import java.io.IOException;
  2474. `;
  2475. const result = extractFromSource('Service.java', code);
  2476. const importNodes = result.nodes.filter((n) => n.kind === 'import');
  2477. expect(importNodes.length).toBe(3);
  2478. const names = importNodes.map((n) => n.name);
  2479. expect(names).toContain('java.util.List');
  2480. expect(names).toContain('java.util.Map');
  2481. expect(names).toContain('java.io.IOException');
  2482. });
  2483. });
  2484. describe('C# imports', () => {
  2485. it('should extract simple using', () => {
  2486. const code = `using System;`;
  2487. const result = extractFromSource('Program.cs', code);
  2488. const importNode = result.nodes.find((n) => n.kind === 'import');
  2489. expect(importNode).toBeDefined();
  2490. expect(importNode?.name).toBe('System');
  2491. expect(importNode?.signature).toBe('using System;');
  2492. });
  2493. it('should extract qualified using', () => {
  2494. const code = `using System.Collections.Generic;`;
  2495. const result = extractFromSource('Utils.cs', code);
  2496. const importNode = result.nodes.find((n) => n.kind === 'import');
  2497. expect(importNode).toBeDefined();
  2498. expect(importNode?.name).toBe('System.Collections.Generic');
  2499. });
  2500. it('should extract static using', () => {
  2501. const code = `using static System.Console;`;
  2502. const result = extractFromSource('App.cs', code);
  2503. const importNode = result.nodes.find((n) => n.kind === 'import');
  2504. expect(importNode).toBeDefined();
  2505. expect(importNode?.name).toBe('System.Console');
  2506. expect(importNode?.signature).toContain('static');
  2507. });
  2508. it('should extract alias using', () => {
  2509. const code = `using MyList = System.Collections.Generic.List<int>;`;
  2510. const result = extractFromSource('Types.cs', code);
  2511. const importNode = result.nodes.find((n) => n.kind === 'import');
  2512. expect(importNode).toBeDefined();
  2513. expect(importNode?.name).toBe('System.Collections.Generic.List<int>');
  2514. expect(importNode?.signature).toContain('MyList =');
  2515. });
  2516. it('should extract multiple usings', () => {
  2517. const code = `
  2518. using System;
  2519. using System.Threading.Tasks;
  2520. using Microsoft.Extensions.DependencyInjection;
  2521. `;
  2522. const result = extractFromSource('Service.cs', code);
  2523. const importNodes = result.nodes.filter((n) => n.kind === 'import');
  2524. expect(importNodes.length).toBe(3);
  2525. const names = importNodes.map((n) => n.name);
  2526. expect(names).toContain('System');
  2527. expect(names).toContain('System.Threading.Tasks');
  2528. expect(names).toContain('Microsoft.Extensions.DependencyInjection');
  2529. });
  2530. });
  2531. describe('PHP imports', () => {
  2532. it('should extract simple use', () => {
  2533. const code = `<?php use PHPUnit\\Framework\\TestCase;`;
  2534. const result = extractFromSource('Test.php', code);
  2535. const importNode = result.nodes.find((n) => n.kind === 'import');
  2536. expect(importNode).toBeDefined();
  2537. expect(importNode?.name).toBe('PHPUnit\\Framework\\TestCase');
  2538. });
  2539. it('should extract aliased use', () => {
  2540. const code = `<?php use Mockery as m;`;
  2541. const result = extractFromSource('Test.php', code);
  2542. const importNode = result.nodes.find((n) => n.kind === 'import');
  2543. expect(importNode).toBeDefined();
  2544. expect(importNode?.name).toBe('Mockery');
  2545. expect(importNode?.signature).toContain('as m');
  2546. });
  2547. it('should extract function use', () => {
  2548. const code = `<?php use function Illuminate\\Support\\env;`;
  2549. const result = extractFromSource('helpers.php', code);
  2550. const importNode = result.nodes.find((n) => n.kind === 'import');
  2551. expect(importNode).toBeDefined();
  2552. expect(importNode?.name).toBe('Illuminate\\Support\\env');
  2553. expect(importNode?.signature).toContain('function');
  2554. });
  2555. it('should extract grouped use', () => {
  2556. const code = `<?php use Illuminate\\Database\\{Model, Builder};`;
  2557. const result = extractFromSource('Models.php', code);
  2558. const importNodes = result.nodes.filter((n) => n.kind === 'import');
  2559. expect(importNodes.length).toBe(2);
  2560. const names = importNodes.map((n) => n.name);
  2561. expect(names).toContain('Illuminate\\Database\\Model');
  2562. expect(names).toContain('Illuminate\\Database\\Builder');
  2563. });
  2564. it('should extract multiple uses', () => {
  2565. const code = `<?php
  2566. use Illuminate\\Support\\Collection;
  2567. use Illuminate\\Support\\Str;
  2568. use Closure;
  2569. `;
  2570. const result = extractFromSource('Service.php', code);
  2571. const importNodes = result.nodes.filter((n) => n.kind === 'import');
  2572. expect(importNodes.length).toBe(3);
  2573. const names = importNodes.map((n) => n.name);
  2574. expect(names).toContain('Illuminate\\Support\\Collection');
  2575. expect(names).toContain('Illuminate\\Support\\Str');
  2576. expect(names).toContain('Closure');
  2577. });
  2578. it('should extract include/require (+_once) static paths as imports (#660)', () => {
  2579. const code = `<?php
  2580. require_once("lib.php");
  2581. include 'other.php';
  2582. require 'r.php';
  2583. include_once("io.php");
  2584. `;
  2585. const result = extractFromSource('page.php', code);
  2586. const names = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name);
  2587. expect(names).toContain('lib.php');
  2588. expect(names).toContain('other.php');
  2589. expect(names).toContain('r.php');
  2590. expect(names).toContain('io.php');
  2591. });
  2592. it('should skip dynamic include/require with no static path (#660)', () => {
  2593. const code = `<?php
  2594. require_once(__DIR__ . '/dyn.php');
  2595. include $file;
  2596. include "tpl/{$name}.php";
  2597. `;
  2598. const result = extractFromSource('page.php', code);
  2599. const imports = result.nodes.filter((n) => n.kind === 'import');
  2600. expect(imports).toHaveLength(0);
  2601. });
  2602. it('should extract include alongside namespace use without interference (#660)', () => {
  2603. const code = `<?php
  2604. use App\\Service\\Mailer;
  2605. require_once("bootstrap.php");
  2606. `;
  2607. const result = extractFromSource('page.php', code);
  2608. const names = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name);
  2609. expect(names).toContain('App\\Service\\Mailer');
  2610. expect(names).toContain('bootstrap.php');
  2611. });
  2612. });
  2613. describe('Ruby imports', () => {
  2614. it('should extract require', () => {
  2615. const code = `require 'json'`;
  2616. const result = extractFromSource('app.rb', code);
  2617. const importNode = result.nodes.find((n) => n.kind === 'import');
  2618. expect(importNode).toBeDefined();
  2619. expect(importNode?.name).toBe('json');
  2620. expect(importNode?.signature).toBe("require 'json'");
  2621. });
  2622. it('should extract require with path', () => {
  2623. const code = `require 'active_support/core_ext/string'`;
  2624. const result = extractFromSource('config.rb', code);
  2625. const importNode = result.nodes.find((n) => n.kind === 'import');
  2626. expect(importNode).toBeDefined();
  2627. expect(importNode?.name).toBe('active_support/core_ext/string');
  2628. });
  2629. it('should extract require_relative', () => {
  2630. const code = `require_relative '../test_helper'`;
  2631. const result = extractFromSource('test/my_test.rb', code);
  2632. const importNode = result.nodes.find((n) => n.kind === 'import');
  2633. expect(importNode).toBeDefined();
  2634. expect(importNode?.name).toBe('../test_helper');
  2635. expect(importNode?.signature).toContain('require_relative');
  2636. });
  2637. it('should not extract non-require calls', () => {
  2638. const code = `puts 'hello'`;
  2639. const result = extractFromSource('app.rb', code);
  2640. const importNode = result.nodes.find((n) => n.kind === 'import');
  2641. expect(importNode).toBeUndefined();
  2642. });
  2643. it('should extract multiple requires', () => {
  2644. const code = `
  2645. require 'json'
  2646. require 'yaml'
  2647. require_relative 'helper'
  2648. `;
  2649. const result = extractFromSource('lib.rb', code);
  2650. const importNodes = result.nodes.filter((n) => n.kind === 'import');
  2651. expect(importNodes.length).toBe(3);
  2652. const names = importNodes.map((n) => n.name);
  2653. expect(names).toContain('json');
  2654. expect(names).toContain('yaml');
  2655. expect(names).toContain('helper');
  2656. });
  2657. });
  2658. describe('Ruby modules', () => {
  2659. it('should extract module as module node with containment', () => {
  2660. const code = `
  2661. module CachedCounting
  2662. def self.disable
  2663. @enabled = false
  2664. end
  2665. def perform_increment!(key, count)
  2666. write_cache!(key, count)
  2667. end
  2668. end
  2669. `;
  2670. const result = extractFromSource('concerns/cached_counting.rb', code);
  2671. const moduleNode = result.nodes.find((n) => n.kind === 'module' && n.name === 'CachedCounting');
  2672. expect(moduleNode).toBeDefined();
  2673. expect(moduleNode?.qualifiedName).toBe('CachedCounting');
  2674. // Methods inside module should have module-qualified names
  2675. const disableMethod = result.nodes.find((n) => n.name === 'disable' && n.kind === 'method');
  2676. expect(disableMethod).toBeDefined();
  2677. expect(disableMethod?.qualifiedName).toBe('CachedCounting::disable');
  2678. const incrementMethod = result.nodes.find((n) => n.name === 'perform_increment!' && n.kind === 'method');
  2679. expect(incrementMethod).toBeDefined();
  2680. expect(incrementMethod?.qualifiedName).toBe('CachedCounting::perform_increment!');
  2681. // Containment edge from module to methods
  2682. const containsEdges = result.edges.filter((e) => e.source === moduleNode?.id && e.kind === 'contains');
  2683. expect(containsEdges.length).toBeGreaterThanOrEqual(2);
  2684. });
  2685. it('should handle nested modules with classes', () => {
  2686. const code = `
  2687. module Discourse
  2688. module Auth
  2689. class AuthProvider
  2690. def authenticate(params)
  2691. validate(params)
  2692. end
  2693. end
  2694. end
  2695. end
  2696. `;
  2697. const result = extractFromSource('lib/auth.rb', code);
  2698. const discourseModule = result.nodes.find((n) => n.kind === 'module' && n.name === 'Discourse');
  2699. expect(discourseModule).toBeDefined();
  2700. const authModule = result.nodes.find((n) => n.kind === 'module' && n.name === 'Auth');
  2701. expect(authModule).toBeDefined();
  2702. expect(authModule?.qualifiedName).toBe('Discourse::Auth');
  2703. const authProvider = result.nodes.find((n) => n.kind === 'class' && n.name === 'AuthProvider');
  2704. expect(authProvider).toBeDefined();
  2705. expect(authProvider?.qualifiedName).toBe('Discourse::Auth::AuthProvider');
  2706. const authMethod = result.nodes.find((n) => n.name === 'authenticate');
  2707. expect(authMethod).toBeDefined();
  2708. expect(authMethod?.qualifiedName).toBe('Discourse::Auth::AuthProvider::authenticate');
  2709. });
  2710. });
  2711. describe('PHP return type capture (#608)', () => {
  2712. it('captures self/static factory returns as the `self` marker; primitives as undefined', () => {
  2713. const code = `<?php
  2714. class ApiClient {
  2715. public static function for(string $c): self { return new self; }
  2716. public static function make(): static { return new static; }
  2717. public function send(array $p): array { return []; }
  2718. }`;
  2719. const result = extractFromSource('ApiClient.php', code);
  2720. expect(result.nodes.find((n) => n.name === 'for' && n.kind === 'method')?.returnType).toBe('self');
  2721. expect(result.nodes.find((n) => n.name === 'make' && n.kind === 'method')?.returnType).toBe('self');
  2722. // `array` is not a class to chain on → no return type recorded.
  2723. expect(result.nodes.find((n) => n.name === 'send' && n.kind === 'method')?.returnType).toBeUndefined();
  2724. });
  2725. it('captures a concrete return type as its short class name', () => {
  2726. const code = `<?php
  2727. namespace App;
  2728. class WidgetFactory { public static function make(): Widget { return new Widget(); } }`;
  2729. const result = extractFromSource('WidgetFactory.php', code);
  2730. expect(result.nodes.find((n) => n.name === 'make' && n.kind === 'method')?.returnType).toBe('Widget');
  2731. });
  2732. });
  2733. describe('C/C++ return type capture (#645)', () => {
  2734. it('captures the normalized return type of a C++ method/function', () => {
  2735. const code = `
  2736. struct Widget { void draw(); };
  2737. class Factory { public: static Widget create(); };
  2738. Widget Factory::create() { return Widget(); }
  2739. void doNothing() {}
  2740. `;
  2741. const result = extractFromSource('f.cpp', code);
  2742. const create = result.nodes.find(
  2743. (n) => n.name === 'create' && (n.kind === 'method' || n.kind === 'function')
  2744. );
  2745. expect(create?.returnType).toBe('Widget');
  2746. // A `void` return records no type, so resolution never tries to resolve a
  2747. // method on it.
  2748. const doNothing = result.nodes.find((n) => n.name === 'doNothing');
  2749. expect(doNothing).toBeDefined();
  2750. expect(doNothing?.returnType).toBeUndefined();
  2751. });
  2752. it('unwraps a smart-pointer return type to its pointee', () => {
  2753. const code = `
  2754. #include <memory>
  2755. struct Widget {};
  2756. std::unique_ptr<Widget> makeWidget() { return nullptr; }
  2757. `;
  2758. const result = extractFromSource('f.cpp', code);
  2759. const make = result.nodes.find((n) => n.name === 'makeWidget');
  2760. expect(make?.returnType).toBe('Widget');
  2761. });
  2762. });
  2763. describe('C++ macro-prefixed class/struct misparse (#946 → recovered in #1061)', () => {
  2764. // An export/visibility macro before the class name (`class MACRO Name :
  2765. // public Base { … }`) makes tree-sitter read `class MACRO` as an elaborated
  2766. // type and the whole declaration as a function_definition named after the
  2767. // class — a phantom `function` that polluted callers/impact/blast-radius.
  2768. // #946 dropped that phantom; #1061's preParse (`blankCppExportMacros`) now
  2769. // blanks the ALL-CAPS macro before parsing, so the class parses normally and
  2770. // is *recovered* — node, members, and base edge all present — not just
  2771. // de-phantomed. The #946 drop survives as the fallback for any residual
  2772. // misparse the blanking doesn't catch.
  2773. it('recovers a macro-annotated class that inherits (no phantom, real class + base edge)', () => {
  2774. const code = `#pragma once
  2775. #define MAPCORE_EXPORT __attribute__((visibility("default")))
  2776. class DataProvider {
  2777. public:
  2778. virtual bool Request(void* param) = 0;
  2779. };
  2780. class MAPCORE_EXPORT LocalDataProvider : public DataProvider
  2781. {
  2782. public:
  2783. LocalDataProvider(int dataType);
  2784. virtual bool Request(void* param) override;
  2785. };
  2786. `;
  2787. // A header rich in C++ (class / public: / virtual) detects as C++ — the
  2788. // issue's exact scenario (a `.h` file). Guard it so a detection regression
  2789. // can't make this test pass for the wrong reason.
  2790. expect(detectLanguage('provider.h', code)).toBe('cpp');
  2791. const result = extractFromSource('provider.h', code);
  2792. // The misparse used to surface as `function | LocalDataProvider` spanning
  2793. // the whole class body — a false caller in the graph. It's gone.
  2794. expect(
  2795. result.nodes.find((n) => n.name === 'LocalDataProvider' && n.kind === 'function')
  2796. ).toBeUndefined();
  2797. // …and the class is now recovered (was dropped under #946), with its
  2798. // `extends DataProvider` edge — the whole point of #1061.
  2799. expect(result.nodes.find((n) => n.name === 'LocalDataProvider')?.kind).toBe('class');
  2800. expect(
  2801. result.unresolvedReferences.find(
  2802. (r) => r.referenceKind === 'extends' && r.referenceName === 'DataProvider'
  2803. )
  2804. ).toBeTruthy();
  2805. // The sibling class without the macro is unaffected — still a class.
  2806. expect(result.nodes.find((n) => n.name === 'DataProvider')?.kind).toBe('class');
  2807. });
  2808. it('recovers the struct variant too, without disturbing a genuine class', () => {
  2809. const code = `
  2810. #define API __declspec(dllexport)
  2811. struct API Widget : public Base { int x; };
  2812. class Plain : public Base { public: int y; };
  2813. `;
  2814. const result = extractFromSource('widget.cpp', code);
  2815. // `struct MACRO Name : Base { … }` misparses the same way — no phantom
  2816. // function, and the struct is recovered with its base edge.
  2817. expect(
  2818. result.nodes.find((n) => n.name === 'Widget' && n.kind === 'function')
  2819. ).toBeUndefined();
  2820. expect(result.nodes.find((n) => n.name === 'Widget')?.kind).toBe('struct');
  2821. // A normal class with a base clause and no macro is untouched.
  2822. expect(result.nodes.find((n) => n.name === 'Plain')?.kind).toBe('class');
  2823. const exts = result.unresolvedReferences
  2824. .filter((r) => r.referenceKind === 'extends')
  2825. .map((r) => r.referenceName);
  2826. expect(exts.filter((n) => n === 'Base').length).toBe(2); // Widget + Plain both extend Base
  2827. });
  2828. });
  2829. describe('C++ export-macro class recovery (#1061)', () => {
  2830. // Unreal-Engine style: `class MYGAME_API UMyComponent : public UActorComponent`.
  2831. // The leading `*_API` macro alone (base clause or not) triggers the #946
  2832. // misparse and dropped the class — breaking subclass / type-hierarchy /
  2833. // inheritance-impact queries for effectively every gameplay class in a UE
  2834. // project. blankCppExportMacros recovers them.
  2835. it('recovers UE *_API classes and the inheritance edge (the issue repro)', () => {
  2836. const code = `class ENGINE_API UActorComponent { };
  2837. class MYGAME_API UMyComponent : public UActorComponent { };
  2838. `;
  2839. const result = extractFromSource('ue.cpp', code);
  2840. const classes = result.nodes.filter((n) => n.kind === 'class').map((n) => n.name);
  2841. expect(classes).toContain('UActorComponent'); // macro, no base — also was dropped
  2842. expect(classes).toContain('UMyComponent');
  2843. expect(result.nodes.find((n) => n.kind === 'function')).toBeUndefined(); // no phantom
  2844. expect(
  2845. result.unresolvedReferences.find(
  2846. (r) => r.referenceKind === 'extends' && r.referenceName === 'UActorComponent'
  2847. )
  2848. ).toBeTruthy();
  2849. });
  2850. it('blankCppExportMacros blanks only the header macro, offset-preserving', () => {
  2851. // Blanking replaces the macro with equal-length spaces, so the output is
  2852. // byte-for-byte the same length and identical *except* the macro is gone —
  2853. // every downstream line/column stays exact.
  2854. const check = (inp: string, macro: string, rest: string) => {
  2855. const out = blankCppExportMacros(inp);
  2856. expect(out.length).toBe(inp.length); // every byte offset preserved
  2857. expect(out).not.toContain(macro); // the macro token is blanked
  2858. expect(out.replace(/ +/g, ' ')).toBe(rest); // nothing else changed
  2859. };
  2860. // Generalizes across the export-macro space: UE _API, Qt/Boost _EXPORT,
  2861. // LLVM _ABI, bare API.
  2862. check(
  2863. 'class MYGAME_API UMyComponent : public UActorComponent { };',
  2864. 'MYGAME_API',
  2865. 'class UMyComponent : public UActorComponent { };'
  2866. );
  2867. check('struct MAPCORE_EXPORT W : B {}', 'MAPCORE_EXPORT', 'struct W : B {}');
  2868. check('class LLVM_ABI Foo {}', 'LLVM_ABI', 'class Foo {}');
  2869. });
  2870. it('does NOT blank an all-caps class NAME or an elaborated-type var decl', () => {
  2871. // The name itself being ALL-CAPS (with or without a base) must survive —
  2872. // the macro is only the token *before* the name, gated on a `: { ` def.
  2873. for (const c of [
  2874. 'class FOO { int x; };',
  2875. 'class FOO : public Base { int x; };',
  2876. 'struct BAR : public Base { int y; };',
  2877. 'enum class COLOR { Red, Green };',
  2878. // elaborated-type variable declarations end in ; = [ — never : {
  2879. 'struct FOO bar;',
  2880. 'class FOO obj = make();',
  2881. 'struct FOO arr[10];',
  2882. // a *_API macro used as an ordinary value elsewhere
  2883. 'int x = SOME_API; void f() { use(MYMODULE_API); }',
  2884. ]) {
  2885. expect(blankCppExportMacros(c)).toBe(c);
  2886. }
  2887. // And the all-caps-named class keeps its base edge through real extraction.
  2888. const result = extractFromSource('ctrl.cpp', 'class FOO : public Base { int x; };');
  2889. expect(result.nodes.find((n) => n.name === 'FOO')?.kind).toBe('class');
  2890. expect(
  2891. result.unresolvedReferences.find(
  2892. (r) => r.referenceKind === 'extends' && r.referenceName === 'Base'
  2893. )
  2894. ).toBeTruthy();
  2895. });
  2896. });
  2897. describe('Metal shader extraction (#1121)', () => {
  2898. // Metal Shading Language (≈ C++14) parses with the C++ grammar. MSL puts
  2899. // `[[attribute]]` annotations AFTER the declarator — a position
  2900. // tree-sitter-cpp misparses: a struct field with a trailing attribute
  2901. // emitted a spurious `extends` ref from the struct to the field's own type.
  2902. // blankMetalAttributes (preParse, `.metal`-gated) blanks them so extraction
  2903. // matches plain C++.
  2904. const METAL = `#include <metal_stdlib>
  2905. using namespace metal;
  2906. struct VertexIn {
  2907. float3 position [[attribute(0)]];
  2908. float2 texCoord [[attribute(1)]];
  2909. };
  2910. struct VertexOut {
  2911. float4 position [[position]];
  2912. float2 texCoord;
  2913. };
  2914. struct Uniforms {
  2915. float4x4 modelViewProjection;
  2916. };
  2917. static float4 applyGamma(float4 color) {
  2918. return pow(color, float4(1.0 / 2.2));
  2919. }
  2920. vertex VertexOut vertexShader(VertexIn in [[stage_in]],
  2921. constant Uniforms &uniforms [[buffer(0)]]) {
  2922. VertexOut out;
  2923. out.position = uniforms.modelViewProjection * float4(in.position, 1.0);
  2924. out.texCoord = in.texCoord;
  2925. return out;
  2926. }
  2927. fragment float4 fragmentShader(VertexOut in [[stage_in]],
  2928. texture2d<float> colorTexture [[texture(0)]],
  2929. sampler textureSampler [[sampler(0)]]) {
  2930. float4 color = colorTexture.sample(textureSampler, in.texCoord);
  2931. return applyGamma(color);
  2932. }
  2933. kernel void computeBlur(texture2d<float, access::read> inTexture [[texture(0)]],
  2934. texture2d<float, access::write> outTexture [[texture(1)]],
  2935. uint2 gid [[thread_position_in_grid]]) {
  2936. float4 color = inTexture.read(gid);
  2937. outTexture.write(color, gid);
  2938. }
  2939. `;
  2940. it('extracts vertex/fragment/kernel functions, structs, and calls from a .metal file', () => {
  2941. const result = extractFromSource('Shaders.metal', METAL);
  2942. expect(result.errors).toHaveLength(0);
  2943. const functions = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name);
  2944. expect(functions).toEqual(
  2945. expect.arrayContaining(['applyGamma', 'vertexShader', 'fragmentShader', 'computeBlur'])
  2946. );
  2947. const structs = result.nodes.filter((n) => n.kind === 'struct').map((n) => n.name);
  2948. expect(structs).toEqual(expect.arrayContaining(['VertexIn', 'VertexOut', 'Uniforms']));
  2949. expect(result.nodes.find((n) => n.kind === 'import')?.name).toBe('metal_stdlib');
  2950. // Attribute blanking is offset-preserving, so positions stay exact.
  2951. const vertexFn = result.nodes.find((n) => n.name === 'vertexShader')!;
  2952. expect(vertexFn.startLine).toBe(22);
  2953. // The shader call graph connects: fragmentShader → applyGamma.
  2954. expect(
  2955. result.unresolvedReferences.find(
  2956. (r) => r.referenceKind === 'calls' && r.referenceName === 'applyGamma'
  2957. )
  2958. ).toBeTruthy();
  2959. // The regression the blanking fixes: field attributes (`float3 position
  2960. // [[attribute(0)]];`) misparsed into `extends` refs from the struct to the
  2961. // field's type — a wrong inheritance edge whenever the repo defines that
  2962. // type itself (simd typedefs in a shared ShaderTypes.h are common).
  2963. expect(result.unresolvedReferences.filter((r) => r.referenceKind === 'extends')).toHaveLength(0);
  2964. });
  2965. it('blankMetalAttributes blanks every attribute form, offset-preserving', () => {
  2966. const inp = [
  2967. 'float4 position [[position]];',
  2968. 'constant Uniforms &u [[buffer(0)]]',
  2969. 'float2 uv [[user(locn0)]];',
  2970. 'device float *out [[buffer(0), raster_order_group(0)]]',
  2971. ].join('\n');
  2972. const out = blankMetalAttributes(inp);
  2973. expect(out.length).toBe(inp.length); // every byte offset preserved
  2974. expect(out).not.toContain('[[');
  2975. // Nothing but the attributes changed: collapsing the blank runs gives the
  2976. // plain declarations back, newlines untouched.
  2977. expect(out.split('\n').map((l) => l.replace(/ +/g, ' ').trimEnd())).toEqual([
  2978. 'float4 position ;',
  2979. 'constant Uniforms &u',
  2980. 'float2 uv ;',
  2981. 'device float *out',
  2982. ]);
  2983. });
  2984. it('blankMetalAttributes never touches non-attribute [[ sequences', () => {
  2985. for (const c of [
  2986. 'auto x = arr[[]{ return 0; }()];', // lambda in subscript — the only other [[ in C++-family code
  2987. 'int y = a[b[i]];', // nested subscript
  2988. 'int z = 1;', // no [[ at all — early-return path
  2989. ]) {
  2990. expect(blankMetalAttributes(c)).toBe(c);
  2991. }
  2992. });
  2993. });
  2994. describe('C++ in-body reflection-macro annotations do not collapse the class (UE)', () => {
  2995. // Unreal reflection markup — `UPROPERTY(...)`, `UFUNCTION(...)`,
  2996. // `GENERATED_BODY()`, `UE_DEPRECATED_*(...)`, `DECLARE_DELEGATE_*(...)` — are
  2997. // no-semicolon macro CALLS decorating members. tree-sitter doesn't know they
  2998. // are macros, so each drops into error recovery; in a heavily-reflected class
  2999. // the errors accumulate until the enclosing class_specifier can't close and
  3000. // the whole class (its base clause and members) collapses into an ERROR node
  3001. // and disappears from the graph. blankCppAnnotationMacroCalls strips them,
  3002. // offset-preserving, so the class parses normally.
  3003. it('recovers a heavily-reflected class with multiple inheritance + members', () => {
  3004. const code = `UCLASS(MinimalAPI)
  3005. class UMyMovement : public UPawnMovementComponent, public IRVOAvoidanceInterface, public INetworkPredictionInterface
  3006. {
  3007. \tGENERATED_BODY()
  3008. public:
  3009. \tUE_DEPRECATED_FORGAME(5.0, "Deprecated; note the commas, and (parens) inside the string")
  3010. \tUPROPERTY(Category="Move", EditAnywhere, BlueprintReadWrite, meta=(ClampMin="0", UIMin="0"))
  3011. \tfloat MaxWalkSpeed;
  3012. \tUFUNCTION(BlueprintCallable, Category="Move")
  3013. \tfloat ComputeSpeed() const { return MaxWalkSpeed * 2.0f; }
  3014. };
  3015. `;
  3016. const result = extractFromSource('movement.cpp', code);
  3017. const cls = result.nodes.find((n) => n.kind === 'class' && n.name === 'UMyMovement');
  3018. expect(cls).toBeTruthy();
  3019. // The class body parses, so its inline method definition is extracted too —
  3020. // proof the class_specifier closed instead of collapsing into an ERROR node.
  3021. expect(result.nodes.some((n) => n.name === 'ComputeSpeed')).toBe(true);
  3022. // The base clause survives (inheritance queries keep working).
  3023. expect(
  3024. result.unresolvedReferences.find(
  3025. (r) => r.referenceKind === 'extends' && r.referenceName === 'UPawnMovementComponent'
  3026. )
  3027. ).toBeTruthy();
  3028. });
  3029. it('strips line-leading no-semicolon ALL-CAPS calls, offset-preserving', () => {
  3030. const inp = `\tUPROPERTY(EditAnywhere, meta=(ClampMin="0"))\n\tfloat X;\n`;
  3031. const out = blankCppAnnotationMacroCalls(inp);
  3032. expect(out.length).toBe(inp.length); // every byte offset preserved
  3033. expect(out).not.toContain('UPROPERTY');
  3034. expect(out).toContain('float X;');
  3035. // A macro whose args carry commas/parens inside a string still balances.
  3036. const inp2 = `UE_DEPRECATED_FORGAME(5.0, "a, b (c)")\nUPROPERTY(Foo)\nfloat Y;\n`;
  3037. const out2 = blankCppAnnotationMacroCalls(inp2);
  3038. expect(out2.length).toBe(inp2.length);
  3039. expect(out2).not.toContain('UE_DEPRECATED_FORGAME');
  3040. expect(out2).not.toContain('UPROPERTY');
  3041. expect(out2).toContain('float Y;');
  3042. });
  3043. it('does NOT blank expression / condition / statement / init-list macro uses', () => {
  3044. for (const c of [
  3045. 'void f() {\n\tif (CHECK_FLAG(x)) { g(); }\n}', // condition — not line-leading
  3046. 'void f() {\n\tLOG_MESSAGE("hi");\n}', // statement call — trailing ;
  3047. 'C::C()\n\t: MEMBER_A(1)\n\t, MEMBER_B(2)\n{}', // init-list — comma / not line-leading
  3048. 'C::C() :\n\tMEMBER_A(1),\n\tMEMBER_B(2)\n{}', // init-list wrapped — trailing , / {
  3049. 'auto y =\n\tMAKE_THING(a) + 1;', // line-leading but an expression fragment
  3050. ]) {
  3051. expect(blankCppAnnotationMacroCalls(c)).toBe(c);
  3052. }
  3053. });
  3054. });
  3055. describe('C++ member/method-level export macros do not orphan declarations (UE)', () => {
  3056. // The `*_API` visibility macro doesn't only prefix the class header — it
  3057. // prefixes almost every exported member/method of a big UE class
  3058. // (`ENGINE_API virtual void Tick(…)`, `static ENGINE_API void Foo(…)`).
  3059. // blankCppExportMacros only recovers the class-HEADER form; without blanking
  3060. // the member form, tree-sitter reads `MACRO <ret> <name>(` as an extra type
  3061. // token and each declaration drops into error recovery.
  3062. it('recovers a class + base + members when members are *_API-prefixed', () => {
  3063. const code = `class ENGINE_API AActor : public UObject
  3064. {
  3065. \tGENERATED_BODY()
  3066. public:
  3067. \tENGINE_API virtual void Tick(float DeltaSeconds);
  3068. \tstatic ENGINE_API void AddReferencedObjects(int32 Count);
  3069. \tENGINE_API float GetLifeSpan() const { return LifeSpan; }
  3070. };
  3071. `;
  3072. const result = extractFromSource('actor.cpp', code);
  3073. expect(result.nodes.some((n) => n.kind === 'class' && n.name === 'AActor')).toBe(true);
  3074. // The inline definition (its body prefixed by ENGINE_API) is extracted —
  3075. // proof the class_specifier closed instead of collapsing into an ERROR.
  3076. expect(result.nodes.some((n) => n.name === 'GetLifeSpan')).toBe(true);
  3077. // The base clause survives (inheritance queries keep working).
  3078. expect(
  3079. result.unresolvedReferences.find(
  3080. (r) => r.referenceKind === 'extends' && r.referenceName === 'UObject'
  3081. )
  3082. ).toBeTruthy();
  3083. });
  3084. it('blanks only the suffix macro before a declaration, offset-preserving', () => {
  3085. const inp = `ENGINE_API void Tick();\nstatic MYMOD_EXPORT int32 X;\nLLVM_ABI bool Y();\n`;
  3086. const out = blankCppApiPrefixMacros(inp);
  3087. expect(out.length).toBe(inp.length); // every byte offset preserved
  3088. expect(out).not.toContain('ENGINE_API');
  3089. expect(out).not.toContain('MYMOD_EXPORT');
  3090. expect(out).not.toContain('LLVM_ABI');
  3091. expect(out).toContain('void Tick();');
  3092. expect(out).toContain('int32 X;');
  3093. expect(out).toContain('bool Y();');
  3094. expect(out).toMatch(/static\s+int32 X;/); // `static` kept, only the macro blanked
  3095. });
  3096. it('does NOT blank an *_API token used as a value or in non-declaration position', () => {
  3097. for (const c of [
  3098. 'int x = SOME_API;', // rvalue — trailing ;
  3099. 'if (mode == FOO_API) { g(); }', // comparison — trailing )
  3100. 'return DEFAULT_API, other;', // comma operand
  3101. 'auto v = NS_API::Make();', // qualified name — trailing ::
  3102. 'x = A_API + B_API;', // operands of + / trailing ;
  3103. ]) {
  3104. expect(blankCppApiPrefixMacros(c)).toBe(c);
  3105. }
  3106. });
  3107. it('leaves a genuine _API-suffixed word alone when it is itself the name', () => {
  3108. // A longer word merely CONTAINING _API (not ending in it) must not match.
  3109. const inp = 'FOO_APIENTRY handler;';
  3110. expect(blankCppApiPrefixMacros(inp)).toBe(inp);
  3111. });
  3112. });
  3113. describe('C++ mid-line UE annotation macros do not collapse the enum/class (UE)', () => {
  3114. // UMETA / UPARAM / UE_DEPRECATED can sit MID-LINE (not line-leading), where
  3115. // blankCppAnnotationMacroCalls structurally can't reach them: an enum value's
  3116. // `UMETA(...)`, or a deprecation tag wedged into a class-scope `using`
  3117. // (`using X UE_DEPRECATED(5.5, "…") = …;`) — which alone collapsed UWorld in
  3118. // World.h. blankCppInlineAnnotationMacros strips them, offset-preserving.
  3119. it('recovers a class whose in-body using-alias carries a mid-line UE_DEPRECATED', () => {
  3120. const code = `class ENGINE_API UWorld : public UObject
  3121. {
  3122. \tGENERATED_BODY()
  3123. public:
  3124. \tusing FOnNetTickEvent UE_DEPRECATED(5.5, "use TMulticastDelegate<void(float)>") = TMulticastDelegate<void(float)>;
  3125. \tENGINE_API float GetTimeSeconds() const { return TimeSeconds; }
  3126. };
  3127. `;
  3128. const result = extractFromSource('world.cpp', code);
  3129. expect(result.nodes.some((n) => n.kind === 'class' && n.name === 'UWorld')).toBe(true);
  3130. // The member after the poison using-alias is reached — the class closed.
  3131. expect(result.nodes.some((n) => n.name === 'GetTimeSeconds')).toBe(true);
  3132. expect(
  3133. result.unresolvedReferences.find(
  3134. (r) => r.referenceKind === 'extends' && r.referenceName === 'UObject'
  3135. )
  3136. ).toBeTruthy();
  3137. });
  3138. it('blanks mid-line UMETA/UPARAM/UE_DEPRECATED with balanced parens, offset-preserving', () => {
  3139. const inp = `enum class EMode : uint8 {\n\tWalk UMETA(DisplayName="Walk (fast), safe"),\n\tRun\n};\n`;
  3140. const out = blankCppInlineAnnotationMacros(inp);
  3141. expect(out.length).toBe(inp.length);
  3142. expect(out).not.toContain('UMETA');
  3143. expect(out).toContain('Walk');
  3144. expect(out).toContain('Run');
  3145. const inp2 = `void F(UPARAM(ref) int& x) {}\n`;
  3146. const out2 = blankCppInlineAnnotationMacros(inp2);
  3147. expect(out2.length).toBe(inp2.length);
  3148. expect(out2).not.toContain('UPARAM');
  3149. expect(out2).toContain('int& x');
  3150. });
  3151. it('does NOT touch source without those UE-only macro names', () => {
  3152. const c = 'enum class E { A, B };\nvoid metadata(int meta) { return; }\n';
  3153. expect(blankCppInlineAnnotationMacros(c)).toBe(c);
  3154. });
  3155. });
  3156. describe('C++ dense Unreal-Engine header regression (#1160/#1158)', () => {
  3157. // Regression guard for the three UE blank passes together, on a HEAVILY
  3158. // reflected class in the shape that broke real engine headers
  3159. // (`CharacterMovementComponent.h` carries ~240 in-body reflection macros).
  3160. // On the real headers the accumulated tree-sitter errors collapse the whole
  3161. // `class_specifier` into an ERROR node and the class itself vanishes; that
  3162. // full collapse is emergent from real-header content we can't ship here
  3163. // (Unreal's source is EULA-licensed), so this reproduces the *recoverable*
  3164. // signal it leaves: with the blank passes reverted, tree-sitter drops every
  3165. // one of these decorated members and the `UMETA` enum into error recovery,
  3166. // so the assertions below flip from pass to fail. Verified against the
  3167. // pre-fix build: `Compute0`, the last member, and `EDenseMode` are all
  3168. // absent before the fix and present after — reverting any of
  3169. // blankCppAnnotationMacroCalls / blankCppApiPrefixMacros /
  3170. // blankCppInlineAnnotationMacros regresses at least one of them.
  3171. const N = 120; // 120 UPROPERTY + 120 UFUNCTION = ~240 in-body macros
  3172. function denseReflectedHeader(): string {
  3173. let members = '';
  3174. for (let i = 0; i < N; i++) {
  3175. // line-leading UPROPERTY with nested meta=(...) (blankCppAnnotationMacroCalls)
  3176. members += `\tUPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Move", meta=(ClampMin="0.0", EditCondition="bOn${i}", AllowedClasses="/Script/Engine.Texture"))\n\tTSubclassOf<AActor> Prop${i};\n`;
  3177. // line-leading UFUNCTION + member-level ENGINE_API + UPARAM(ref) param
  3178. // (all three passes) on an inline definition (has a body → is a node)
  3179. members += `\tUFUNCTION(BlueprintCallable, Category="Move", meta=(DisplayName="Compute ${i}"))\n\tENGINE_API float Compute${i}(UPARAM(ref) float& In) const { return In * ${i}.0f; }\n`;
  3180. }
  3181. return `UCLASS(MinimalAPI, Blueprintable)
  3182. class ENGINE_API UDenseMovement : public UPawnMovementComponent, public IRVOAvoidanceInterface, public INetworkPredictionInterface
  3183. {
  3184. \tGENERATED_BODY()
  3185. public:
  3186. \tDECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnMoved, float, Speed, FVector, Loc);
  3187. \tusing FLegacyTick UE_DEPRECATED(5.5, "use TDelegate<void(float)>") = TMulticastDelegate<void(float)>;
  3188. ${members}};
  3189. UENUM(BlueprintType)
  3190. enum class EDenseMode : uint8
  3191. {
  3192. \tWalking UMETA(DisplayName="Walk (fast), safe"),
  3193. \tFlying UMETA(DisplayName="Fly"),
  3194. \tCustom UMETA(Hidden),
  3195. };
  3196. `;
  3197. }
  3198. it('recovers a ~240-macro reflected class, its base clause, and every decorated member', () => {
  3199. const result = extractFromSource('DenseMovement.h', denseReflectedHeader());
  3200. // The class and its whole multiple-inheritance base clause survive.
  3201. expect(result.nodes.some((n) => n.kind === 'class' && n.name === 'UDenseMovement')).toBe(true);
  3202. for (const base of ['UPawnMovementComponent', 'IRVOAvoidanceInterface', 'INetworkPredictionInterface']) {
  3203. expect(
  3204. result.unresolvedReferences.find((r) => r.referenceKind === 'extends' && r.referenceName === base)
  3205. ).toBeTruthy();
  3206. }
  3207. // The real guard: the decorated inline members parse instead of being lost
  3208. // to error recovery — the first, a middle, and the LAST (proof the whole
  3209. // dense body closed, not just the head).
  3210. expect(result.nodes.some((n) => n.name === 'Compute0')).toBe(true);
  3211. expect(result.nodes.some((n) => n.name === 'Compute60')).toBe(true);
  3212. expect(result.nodes.some((n) => n.name === `Compute${N - 1}`)).toBe(true);
  3213. });
  3214. it('recovers a UENUM whose values carry mid-line UMETA', () => {
  3215. const result = extractFromSource('DenseMovement.h', denseReflectedHeader());
  3216. // A mid-line UMETA drops the enum into error recovery pre-fix;
  3217. // blankCppInlineAnnotationMacros restores it.
  3218. expect(result.nodes.some((n) => n.kind === 'enum' && n.name === 'EDenseMode')).toBe(true);
  3219. });
  3220. });
  3221. describe('CUDA extraction (#387)', () => {
  3222. // CUDA parses with the C++ grammar. Three CUDA-only shapes misparse:
  3223. // execution-space specifiers (`__global__ void f(…)`) shunt the real return
  3224. // type into an ERROR node, `__shared__ float tile[256]` mangles the declared
  3225. // name to `float`, and — the critical one — `k<<<grid, block>>>(args)` lexes
  3226. // as shift operators around an empty-named template so NO call_expression
  3227. // (and therefore no host→kernel call edge) exists. blankCudaConstructs
  3228. // (preParse, `.cu`/`.cuh`-gated) blanks all three so extraction matches
  3229. // plain C++.
  3230. const CUDA = `#include <cuda_runtime.h>
  3231. #include "kernels.cuh"
  3232. __constant__ float d_scale[16];
  3233. __device__ __forceinline__ float warp_reduce_sum(float val) {
  3234. for (int offset = 16; offset > 0; offset /= 2) {
  3235. val += __shfl_down_sync(0xffffffff, val, offset);
  3236. }
  3237. return val;
  3238. }
  3239. __global__ void scale_kernel(float* out, const float* __restrict__ in, int n) {
  3240. int i = blockIdx.x * blockDim.x + threadIdx.x;
  3241. __shared__ float tile[256];
  3242. if (i < n) {
  3243. tile[threadIdx.x] = in[i];
  3244. __syncthreads();
  3245. out[i] = warp_reduce_sum(tile[threadIdx.x]) * d_scale[0];
  3246. }
  3247. }
  3248. __global__ void __launch_bounds__(256, 4) bounded_kernel(float* data, int n) {
  3249. if (blockIdx.x * blockDim.x + threadIdx.x < n) data[0] *= 2.0f;
  3250. }
  3251. template <typename T, int BLOCK>
  3252. __global__ void templated_kernel(T* data, int n) {
  3253. if (blockIdx.x * BLOCK + threadIdx.x < n) data[0] += T(1);
  3254. }
  3255. class GpuBuffer {
  3256. public:
  3257. explicit GpuBuffer(size_t n) { cudaMalloc(&ptr_, n * sizeof(float)); }
  3258. ~GpuBuffer() { cudaFree(ptr_); }
  3259. private:
  3260. float* ptr_ = nullptr;
  3261. };
  3262. void launch_scale(float* out, const float* in, int n, cudaStream_t stream) {
  3263. dim3 block(256);
  3264. dim3 grid((n + block.x - 1) / block.x);
  3265. scale_kernel<<<grid, block, 0, stream>>>(out, in, n);
  3266. bounded_kernel<<<grid,
  3267. block>>>(out, n);
  3268. templated_kernel<float, 256><<<grid, block>>>(out, n);
  3269. }
  3270. `;
  3271. it('extracts kernels, device functions, and host→kernel launch calls from a .cu file', () => {
  3272. const result = extractFromSource('kernels/scan.cu', CUDA);
  3273. expect(result.errors).toHaveLength(0);
  3274. const functions = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name);
  3275. expect(functions).toEqual(
  3276. expect.arrayContaining([
  3277. 'warp_reduce_sum',
  3278. 'scale_kernel',
  3279. 'bounded_kernel',
  3280. 'templated_kernel',
  3281. 'launch_scale',
  3282. ])
  3283. );
  3284. expect(result.nodes.filter((n) => n.kind === 'class').map((n) => n.name)).toContain('GpuBuffer');
  3285. expect(result.nodes.find((n) => n.kind === 'import')?.name).toBe('cuda_runtime.h');
  3286. // No misparse artifacts: pre-blank, `__shared__ float tile[256]` parsed
  3287. // with `float` as the declared name. (Top-level C++ variables aren't
  3288. // extracted as nodes — matching plain-C++ behavior is the target.)
  3289. expect(result.nodes.map((n) => n.name)).not.toContain('float');
  3290. // Blanking is offset-preserving, so positions stay exact.
  3291. expect(result.nodes.find((n) => n.name === 'scale_kernel')!.startLine).toBe(13);
  3292. // THE point of CUDA support: every `<<<…>>>` launch form — plain,
  3293. // launch-bounds, multi-line config, and templated — emits a `calls`
  3294. // reference, so the host→kernel edge exists in the graph. Pre-blank,
  3295. // the chevrons lexed as shifts and none of these existed.
  3296. const calls = result.unresolvedReferences
  3297. .filter((r) => r.referenceKind === 'calls')
  3298. .map((r) => r.referenceName);
  3299. // The templated launch is normalized to the bare kernel name (template
  3300. // args stripped at extraction, like base-class extends refs — #1043), so
  3301. // it resolves to the kernel the template was defined as.
  3302. expect(calls).toEqual(
  3303. expect.arrayContaining([
  3304. 'scale_kernel',
  3305. 'bounded_kernel',
  3306. 'templated_kernel',
  3307. 'warp_reduce_sum',
  3308. ])
  3309. );
  3310. });
  3311. it('blankCudaConstructs blanks every CUDA form, offset- and newline-preserving', () => {
  3312. const inp = [
  3313. '__global__ void __launch_bounds__(256, 4) step(float* p) {',
  3314. ' __shared__ float tile[32];',
  3315. '}',
  3316. '__host__ __device__ int both() { return 0; }',
  3317. 'void run(float* p, int n) {',
  3318. ' step<<<grid,',
  3319. ' block, 0, stream>>>(p);',
  3320. '}',
  3321. ].join('\n');
  3322. const out = blankCudaConstructs(inp);
  3323. expect(out.length).toBe(inp.length); // every byte offset preserved
  3324. expect(out.split('\n').length).toBe(inp.split('\n').length); // newlines survive the multi-line launch config
  3325. expect(out).not.toMatch(/__global__|__launch_bounds__|__shared__|__host__|__device__|<<<|>>>/);
  3326. // Collapsing blank runs gives plain C++ back.
  3327. expect(out.split('\n').map((l) => l.replace(/ +/g, ' ').trimEnd())).toEqual([
  3328. ' void step(float* p) {',
  3329. ' float tile[32];',
  3330. '}',
  3331. ' int both() { return 0; }',
  3332. 'void run(float* p, int n) {',
  3333. ' step',
  3334. ' (p);',
  3335. '}',
  3336. ]);
  3337. });
  3338. it('blankCudaConstructs never touches non-CUDA chevrons or identifiers', () => {
  3339. for (const c of [
  3340. 'std::cout << "a" << b << c;', // shift chains — never three consecutive <
  3341. 'auto x = f(a >> 3, b >> 3);', // right shifts
  3342. 'std::vector<std::vector<std::vector<int>>> deep;', // template >>> closer with no <<< opener
  3343. 'printf("<<<unterminated");', // <<< in a string with no >>> anywhere
  3344. 'int __restrict__like = 1;', // dunder-ish identifier not in the specifier list
  3345. 'int z = 1;', // nothing CUDA at all — early-return path
  3346. ]) {
  3347. expect(blankCudaConstructs(c)).toBe(c);
  3348. }
  3349. // A stray `<<<` (committed merge-conflict marker) must not blank the code
  3350. // between markers. Two independent guards: statements between markers
  3351. // carry `;` (excluded from the span)…
  3352. const conflict = [
  3353. '<<<<<<< HEAD',
  3354. 'int a = compute(1);',
  3355. '=======',
  3356. 'int a = compute(2);',
  3357. '>>>>>>> feature-branch',
  3358. ].join('\n');
  3359. expect(blankCudaConstructs(conflict)).toBe(conflict);
  3360. // …and a `;`-free region still fails the brace-balance check (the `{`s
  3361. // opened between the markers never close before the `>>>`).
  3362. const semicolonFree = [
  3363. '<<<<<<< HEAD',
  3364. 'void foo() {',
  3365. '=======',
  3366. 'void bar() {',
  3367. '>>>>>>> feature-branch',
  3368. ].join('\n');
  3369. expect(blankCudaConstructs(semicolonFree)).toBe(semicolonFree);
  3370. });
  3371. it('blanks brace-initialized launch configs (`dim3{…}`), balanced-only', () => {
  3372. const inp = 'run_it<<<dim3{1, 2, 1}, dim3{256, 1, 1}, 0, stream>>>(data, n);';
  3373. const out = blankCudaConstructs(inp);
  3374. expect(out.length).toBe(inp.length);
  3375. expect(out.replace(/ +/g, ' ')).toBe('run_it (data, n);');
  3376. });
  3377. it('recovers the real kernel name from a macro-definition idiom, gtest/pybind untouched', () => {
  3378. const code = `#define DEFINE_MY_FWD_KERNEL(kernelName, ...) \\
  3379. template<typename Traits, __VA_ARGS__> \\
  3380. __global__ void kernelName(const Params params)
  3381. DEFINE_MY_FWD_KERNEL(fwd_kernel, bool Is_causal, int kBlockM) {
  3382. do_work(params);
  3383. }
  3384. TEST_F(MyFixture, HandlesEmptyInput) {
  3385. check(1);
  3386. }
  3387. `;
  3388. const result = extractFromSource('kernels/impl.cu', code);
  3389. const functions = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name);
  3390. // The macro invocation's first argument is the defined name.
  3391. expect(functions).toContain('fwd_kernel');
  3392. expect(functions).not.toContain('DEFINE_MY_FWD_KERNEL');
  3393. // gtest's TEST_F(Fixture, Name) has TWO lone identifiers — ambiguous, so
  3394. // it keeps the macro name rather than guessing.
  3395. expect(functions).toContain('TEST_F');
  3396. expect(functions).not.toContain('MyFixture');
  3397. });
  3398. it('links launches through a local function pointer to the real kernel(s)', () => {
  3399. // The flash-attention launch-template shape end-to-end: a macro-defined
  3400. // kernel + `auto kernel = &fn<…>` + branch reassignment + launch through
  3401. // the local. The call refs must name the real kernels, not `kernel`.
  3402. const code = `template <typename T, bool Flag>
  3403. __global__ void fwd_kernel(T* data, int n) {
  3404. if (blockIdx.x * blockDim.x + threadIdx.x < n) data[0] += T(1);
  3405. }
  3406. template <typename T>
  3407. __global__ void fwd_splitkv_kernel(T* data, int n) {
  3408. if (blockIdx.x * blockDim.x + threadIdx.x < n) data[0] += T(2);
  3409. }
  3410. template <typename T>
  3411. void run_fwd(T* data, int n, cudaStream_t stream) {
  3412. auto kernel = &fwd_kernel<T, true>;
  3413. if (n % 2 == 0) {
  3414. kernel = &fwd_kernel<T, false>;
  3415. } else if (n % 3 == 0) {
  3416. kernel = &fwd_splitkv_kernel<T>;
  3417. }
  3418. kernel<<<(n + 255) / 256, 256, 0, stream>>>(data, n);
  3419. }
  3420. `;
  3421. const result = extractFromSource('kernels/launch.cu', code);
  3422. expect(result.errors).toHaveLength(0);
  3423. const calls = result.unresolvedReferences
  3424. .filter((r) => r.referenceKind === 'calls')
  3425. .map((r) => r.referenceName);
  3426. // Every DISTINCT branch target recorded once — the two fwd_kernel<…>
  3427. // instantiations strip to one target, the splitkv branch adds a second.
  3428. // The local's name never leaks as a callee.
  3429. expect(calls.filter((c) => c === 'fwd_kernel')).toHaveLength(1);
  3430. expect(calls.filter((c) => c === 'fwd_splitkv_kernel')).toHaveLength(1);
  3431. expect(calls).not.toContain('kernel');
  3432. });
  3433. it('CUDA blanking is gated by extension or content — plain C++ shift/template chevrons are untouched', () => {
  3434. const cpp = `#include <vector>
  3435. int shift_it(int a, int b) { return a << b << 1; }
  3436. std::vector<std::vector<std::vector<int>>> matrix() { return {}; }
  3437. `;
  3438. const result = extractFromSource('math.cpp', cpp);
  3439. expect(result.errors).toHaveLength(0);
  3440. const functions = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name);
  3441. expect(functions).toEqual(expect.arrayContaining(['shift_it', 'matrix']));
  3442. });
  3443. it('CUDA in extension-less headers is caught by content: launch templates in .h connect host→kernel', () => {
  3444. // Much real CUDA lives in .h: cutlass launches most of its kernels from
  3445. // headers, flash-attention's launch templates are .h, llm.c keeps device
  3446. // helpers in C-detected .h. `looksLikeCudaSource` content-gates the same
  3447. // blank there — no CUDA marker is valid C++ anywhere, so this can't
  3448. // affect a genuinely-plain C++ header.
  3449. const header = `#pragma once
  3450. #include <cuda_runtime.h>
  3451. template <typename T>
  3452. __global__ void fill_kernel(T* out, T value, int n) {
  3453. int i = blockIdx.x * blockDim.x + threadIdx.x;
  3454. if (i < n) out[i] = value;
  3455. }
  3456. template <typename T>
  3457. void launch_fill(T* out, T value, int n, cudaStream_t stream) {
  3458. fill_kernel<T><<<(n + 255) / 256, 256, 0, stream>>>(out, value, n);
  3459. }
  3460. `;
  3461. const result = extractFromSource('include/fill_launch_template.h', header);
  3462. expect(result.errors).toHaveLength(0);
  3463. const functions = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name);
  3464. expect(functions).toEqual(expect.arrayContaining(['fill_kernel', 'launch_fill']));
  3465. const calls = result.unresolvedReferences
  3466. .filter((r) => r.referenceKind === 'calls')
  3467. .map((r) => r.referenceName);
  3468. expect(calls).toContain('fill_kernel');
  3469. });
  3470. });
  3471. describe('C++ namespace qualifiedName prefixing', () => {
  3472. // C++ namespaces previously left no trace in qualifiedNames, so a
  3473. // namespace-qualified call (`flash::compute(...)`) could never match its
  3474. // definition — every `ns::fn()` call site was a permanently dead edge.
  3475. // The namespace name now prefixes contained symbols' qualifiedNames
  3476. // (prefix-only: no namespace node is minted — `namespace cutlass {` opens
  3477. // in thousands of files and a node per block would crowd search, #1093).
  3478. it('prefixes contained symbols and handles nesting; anonymous stays bare', () => {
  3479. const code = `namespace flash {
  3480. namespace detail {
  3481. void helper() {}
  3482. }
  3483. void compute_attn(int x) { detail::helper(); }
  3484. class Softmax {
  3485. public:
  3486. void rescale() {}
  3487. };
  3488. }
  3489. namespace {
  3490. void file_local() {}
  3491. }
  3492. void global_fn() { flash::compute_attn(1); }
  3493. `;
  3494. const result = extractFromSource('dispatch.cpp', code);
  3495. expect(result.errors).toHaveLength(0);
  3496. const byName = new Map(result.nodes.map((n) => [n.name, n]));
  3497. expect(byName.get('compute_attn')?.qualifiedName).toBe('flash::compute_attn');
  3498. expect(byName.get('helper')?.qualifiedName).toBe('flash::detail::helper');
  3499. expect(byName.get('Softmax')?.qualifiedName).toBe('flash::Softmax');
  3500. // Class scope still stacks under the namespace prefix.
  3501. expect(byName.get('rescale')?.qualifiedName).toBe('flash::Softmax::rescale');
  3502. // Anonymous namespace contents and true globals stay bare.
  3503. expect(byName.get('file_local')?.qualifiedName).toBe('file_local');
  3504. expect(byName.get('global_fn')?.qualifiedName).toBe('global_fn');
  3505. // The qualified call refs are emitted as spelled.
  3506. const calls = result.unresolvedReferences
  3507. .filter((r) => r.referenceKind === 'calls')
  3508. .map((r) => r.referenceName);
  3509. expect(calls).toEqual(expect.arrayContaining(['flash::compute_attn', 'detail::helper']));
  3510. });
  3511. it('C++17 nested namespace form prefixes as written', () => {
  3512. const code = `namespace a::b {
  3513. int f() { return 1; }
  3514. }
  3515. `;
  3516. const result = extractFromSource('nested.cpp', code);
  3517. expect(result.nodes.find((n) => n.name === 'f')?.qualifiedName).toBe('a::b::f');
  3518. });
  3519. // Out-of-line member definitions take their qualifiedName from the
  3520. // declarator's receiver (`ManifestStartup::Apply`), which is spelled
  3521. // RELATIVE to the enclosing namespace — the namespace prefix must compose
  3522. // in, or the method's qualifiedName diverges from its own class node's
  3523. // and `ns::Class::Method(...)` call sites never resolve (#1291).
  3524. it('out-of-line method definitions inside a namespace carry the namespace prefix', () => {
  3525. const code = `namespace simulator {
  3526. class ManifestStartup {
  3527. public:
  3528. struct Input { int x; };
  3529. struct Output { int y; };
  3530. static Output Apply(const Input& input);
  3531. };
  3532. ManifestStartup::Output ManifestStartup::Apply(const Input& input) { return {}; }
  3533. }
  3534. `;
  3535. const result = extractFromSource('manifest_startup.cpp', code);
  3536. const apply = result.nodes.filter((n) => n.name === 'Apply');
  3537. // The out-of-line definition's QN matches the class node's prefix.
  3538. expect(apply.map((n) => n.qualifiedName)).toContain('simulator::ManifestStartup::Apply');
  3539. expect(result.nodes.find((n) => n.kind === 'class')?.qualifiedName).toBe(
  3540. 'simulator::ManifestStartup'
  3541. );
  3542. });
  3543. it('a receiver that re-spells the namespace path is not double-prefixed', () => {
  3544. const code = `namespace sim {
  3545. class M { public: static void f(); static void g(); };
  3546. void sim::M::f() {}
  3547. void M::g() {}
  3548. }
  3549. void sim::M::f2() {}
  3550. `;
  3551. const result = extractFromSource('m.cpp', code);
  3552. const qns = result.nodes.filter((n) => n.kind === 'method').map((n) => n.qualifiedName);
  3553. expect(qns).toContain('sim::M::f'); // fully re-spelled inside the namespace
  3554. expect(qns).toContain('sim::M::g'); // relative form
  3555. expect(qns).toContain('sim::M::f2'); // global scope, spelled absolute
  3556. expect(qns.find((q) => q?.includes('sim::sim'))).toBeUndefined();
  3557. });
  3558. });
  3559. describe('C++ forward declarations do not mint phantom class nodes (#1093)', () => {
  3560. // `class Foo;` parses as a bodiless class_specifier. Repeated across headers,
  3561. // each forward decl minted a phantom bodiless `class` node that crowded out —
  3562. // and could be picked as the blast-radius representative over — the single
  3563. // real definition. Bodiless struct/enum specifiers were already skipped;
  3564. // classes now are too, but ONLY for C/C++ (opt-in flag), never for languages
  3565. // where a bodiless class is a complete definition.
  3566. it('keeps only the real definition, dropping repeated forward decls', () => {
  3567. const code = `
  3568. class APXCharacter; // forward decl (header 1)
  3569. class APXCharacter; // forward decl (header 2)
  3570. friend class APXCharacter; // elaborated / friend forward reference
  3571. class APXCharacter { // the one real definition
  3572. int hp;
  3573. void takeDamage(int amount) { hp -= amount; }
  3574. };
  3575. `;
  3576. const result = extractFromSource('character.cpp', code);
  3577. const classes = result.nodes.filter(
  3578. (n) => n.kind === 'class' && n.name === 'APXCharacter'
  3579. );
  3580. // Exactly one class node, and it's the definition (carries the member).
  3581. expect(classes).toHaveLength(1);
  3582. expect(classes[0].startLine).toBe(6);
  3583. expect(
  3584. result.nodes.some((n) => n.kind === 'method' && n.name === 'takeDamage')
  3585. ).toBe(true);
  3586. });
  3587. it('elaborated type references in declarations create no phantom class', () => {
  3588. // `class Foo obj;` is a variable declaration using an elaborated type, not
  3589. // a class definition — it must not mint a `Foo` class node.
  3590. const result = extractFromSource('use.cpp', 'class Foo;\nvoid f() { class Foo *p = nullptr; (void)p; }\n');
  3591. expect(result.nodes.filter((n) => n.kind === 'class' && n.name === 'Foo')).toHaveLength(0);
  3592. });
  3593. it('does NOT affect languages where a bodiless class is complete', () => {
  3594. // Kotlin `class Empty` and Scala `trait`/`case object`/`class` with no body
  3595. // are complete definitions — the C/C++-only skip must leave them indexed.
  3596. const kt = extractFromSource('Empty.kt', 'class Empty\nclass Full { val x = 1 }\n');
  3597. const ktClasses = kt.nodes.filter((n) => n.kind === 'class').map((n) => n.name);
  3598. expect(ktClasses).toContain('Empty');
  3599. expect(ktClasses).toContain('Full');
  3600. const scala = extractFromSource('M.scala', 'trait Marker\ncase object Red\nclass Foo\n');
  3601. const scalaNames = scala.nodes
  3602. .filter((n) => ['class', 'trait', 'interface'].includes(n.kind))
  3603. .map((n) => n.name);
  3604. expect(scalaNames).toEqual(expect.arrayContaining(['Marker', 'Red', 'Foo']));
  3605. });
  3606. });
  3607. describe('C++ reference-return method/function names (#1093 follow-up)', () => {
  3608. // An inline method/function returning a reference parses with a
  3609. // `reference_declarator` wrapping the `function_declarator`. That wrapper
  3610. // wasn't unwrapped (only `pointer_declarator` was), so the name captured the
  3611. // whole declarator — `const int& getRef() const {…}` became the method named
  3612. // "& getRef() const" instead of "getRef", polluting search and callers. Very
  3613. // common in Unreal Engine headers (`const FGameplayTagContainer& GetActiveTags() const`).
  3614. const namesOf = (code: string) =>
  3615. extractFromSource('r.cpp', code).nodes
  3616. .filter((n) => n.kind === 'method' || n.kind === 'function')
  3617. .map((n) => n.name);
  3618. it('names an inline reference-returning method by its identifier, not the declarator', () => {
  3619. const names = namesOf('class C {\npublic:\n const int& getRef() const { return x; }\n int& mutRef() { return x; }\n int x;\n};');
  3620. expect(names).toContain('getRef');
  3621. expect(names).toContain('mutRef');
  3622. // No name leaks the reference sigil or the parameter/qualifier tail.
  3623. expect(names.some((n) => /[&()]/.test(n))).toBe(false);
  3624. });
  3625. it('handles rvalue-reference returns and reference-returning free functions', () => {
  3626. expect(namesOf('class C { int&& take() { return 1; } };')).toContain('take');
  3627. expect(namesOf('const int& globalRef() { static int x; return x; }')).toContain('globalRef');
  3628. });
  3629. it('leaves pointer, value, and out-of-line reference returns unchanged (controls)', () => {
  3630. expect(namesOf('class C { int* getPtr() { return &x; } int x; };')).toContain('getPtr');
  3631. expect(namesOf('class C { int getVal() const { return x; } int x; };')).toContain('getVal');
  3632. // Out-of-line `T& C::f()` already resolves via the qualified-name hook.
  3633. expect(namesOf('const int& C::getRef() const { return x; }')).toContain('getRef');
  3634. });
  3635. });
  3636. describe('C++ user-defined conversion operator names (#1093 follow-up)', () => {
  3637. // A conversion operator's declarator is an `operator_cast` (target type +
  3638. // `() const` tail). It was named with the whole declarator —
  3639. // `operator EALSMovementState() const` — so it didn't match the symbolic-
  3640. // overload style (`operator+`) and carried parameter noise. It's now named
  3641. // `operator <type>`. Common in Unreal Engine enum-wrapper structs.
  3642. const namesOf = (code: string) =>
  3643. extractFromSource('o.cpp', code).nodes
  3644. .filter((n) => n.kind === 'method' || n.kind === 'function')
  3645. .map((n) => n.name);
  3646. it('names a conversion operator as "operator <type>", not the full declarator', () => {
  3647. const names = namesOf('struct S {\n operator int() const { return 1; }\n operator bool() { return true; }\n int x;\n};');
  3648. expect(names).toContain('operator int');
  3649. expect(names).toContain('operator bool');
  3650. expect(names.some((n) => n.includes('(') || n.includes('const'))).toBe(false);
  3651. });
  3652. it('handles a user-type conversion operator', () => {
  3653. expect(
  3654. namesOf('struct FALSMovementState {\n operator EALSMovementState() const { return State; }\n EALSMovementState State;\n};')
  3655. ).toContain('operator EALSMovementState');
  3656. });
  3657. it('leaves symbolic operator overloads unchanged (control)', () => {
  3658. const names = namesOf('struct S {\n S operator+(const S& o) const { return o; }\n int& operator[](int i) { return x; }\n int x;\n};');
  3659. expect(names).toContain('operator+');
  3660. expect(names).toContain('operator[]'); // reference-returning subscript, name still clean
  3661. });
  3662. });
  3663. describe('C++ explicit operator-call refs (#1247)', () => {
  3664. // tree-sitter-cpp can't parse an operator_name in field position:
  3665. // `a.operator+(b)` yields `call_expression(function: identifier «a»,
  3666. // ERROR(operator_name), argument_list)` instead of a field_expression
  3667. // callee, so the emitted ref was just the receiver (`a`) and the call never
  3668. // resolved. The extractor recovers the operator_name from the ERROR child
  3669. // and emits `<receiver>.operator+` like any other member call.
  3670. const HEADER = 'struct V {\n V operator+(const V& o) const;\n V operator[](int i) const;\n V operator()(int i) const;\n bool operator==(const V& o) const;\n int get() const;\n};\n';
  3671. const callRefsOf = (body: string) =>
  3672. extractFromSource('op.cpp', HEADER + body)
  3673. .unresolvedReferences.filter((r) => r.referenceKind === 'calls')
  3674. .map((r) => r.referenceName);
  3675. it('recovers receiver.operator+ from the explicit call form', () => {
  3676. expect(callRefsOf('V f(const V& a, const V& b) { return a.operator+(b); }\n')).toContain('a.operator+');
  3677. });
  3678. it('recovers pointer receivers (p->operator+ → p.operator+)', () => {
  3679. expect(callRefsOf('V f(const V* p, const V& b) { return p->operator+(b); }\n')).toContain('p.operator+');
  3680. });
  3681. it('recovers subscript, call, and comparison operator forms', () => {
  3682. const refs = callRefsOf(
  3683. 'V f1(const V& a) { return a.operator[](3); }\n' +
  3684. 'V f2(V& a) { return a.operator()(1); }\n' +
  3685. 'bool f3(const V& a, const V& b) { return a.operator==(b); }\n'
  3686. );
  3687. expect(refs).toContain('a.operator[]');
  3688. expect(refs).toContain('a.operator()');
  3689. expect(refs).toContain('a.operator==');
  3690. });
  3691. it('normalizes spaced call-site operator names to the compact definition form', () => {
  3692. // nlohmann/json calls `it.operator * ()` / `other.operator < (*this)`
  3693. // while defining `operator*` / `operator<` compact.
  3694. const refs = callRefsOf(
  3695. 'bool f(const V& a, const V& b) { return a.operator == (b); }\n' +
  3696. 'V g(const V& a) { return a.operator [] (3); }\n'
  3697. );
  3698. expect(refs).toContain('a.operator==');
  3699. expect(refs).toContain('a.operator[]');
  3700. });
  3701. it('drops the ref for a complex receiver instead of guessing (no wrong edge)', () => {
  3702. // `object->operator[](val)` through a member chain ending in a call —
  3703. // the receiver type isn't inferable and a bare `operator[]` ref would
  3704. // let exact-name matching guess among unrelated operators.
  3705. const refs = callRefsOf(
  3706. 'struct W { V* obj(); };\n' +
  3707. 'V f(W& w, const V& b) { return w.obj()->operator+(b); }\n'
  3708. );
  3709. expect(refs.some((r) => r.includes('operator+'))).toBe(false);
  3710. expect(refs).toContain('w.obj'); // the inner call itself still refs normally
  3711. });
  3712. it('emits the bare operator name for a this-> receiver', () => {
  3713. const refs = extractFromSource(
  3714. 'op.cpp',
  3715. 'struct V {\n V operator+(const V& o) const;\n V twice() const { return this->operator+(*this); }\n};\n'
  3716. ).unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName);
  3717. expect(refs).toContain('operator+');
  3718. expect(refs.some((r) => r.includes('this'))).toBe(false);
  3719. });
  3720. it('leaves plain member calls unchanged (control)', () => {
  3721. expect(callRefsOf('int f(const V& a) { return a.get(); }\n')).toContain('a.get');
  3722. });
  3723. });
  3724. describe('C++ macro-prefixed function names (#1093 follow-up)', () => {
  3725. // An unknown inline-specifier macro before the return type
  3726. // (`FORCEINLINE FString GetName(…)`) threw tree-sitter into error recovery:
  3727. // the macro became the return type and — for a non-primitive return — the
  3728. // return type was glued onto the name (`"FString GetName"`), so the function
  3729. // was unfindable by name and its callers didn't link. `blankCppInlineMacros`
  3730. // blanks the known UE inline macros before parsing (offset-preserving), the
  3731. // same recover-don't-drop approach as the macro-annotated-class fix. Pervasive
  3732. // in Unreal Engine (`FORCEINLINE`).
  3733. const infoOf = (code: string) =>
  3734. extractFromSource('m.cpp', code).nodes
  3735. .filter((n) => n.kind === 'method' || n.kind === 'function')
  3736. .map((n) => ({ name: n.name, ret: n.returnType }));
  3737. it('recovers the real name AND return type of a FORCEINLINE function', () => {
  3738. expect(infoOf('static FORCEINLINE FString GetName(int V) { return H(V); }')).toEqual([
  3739. { name: 'GetName', ret: 'FString' },
  3740. ]);
  3741. });
  3742. it('handles the templated UE helper shape (GetEnumerationToString)', () => {
  3743. const names = infoOf(
  3744. 'template <typename E> static FORCEINLINE FString GetEnumerationToString(const E V) { return H(V); }'
  3745. ).map((x) => x.name);
  3746. expect(names).toContain('GetEnumerationToString');
  3747. });
  3748. it('handles FORCENOINLINE / FORCEINLINE_DEBUGGABLE, methods, void, and reference returns', () => {
  3749. expect(infoOf('FORCENOINLINE FString A(int V){return H(V);}').map((x) => x.name)).toContain('A');
  3750. expect(infoOf('FORCEINLINE_DEBUGGABLE FString B(int V){return H(V);}').map((x) => x.name)).toContain('B');
  3751. expect(infoOf('struct S { FORCEINLINE FString GetName(int V) { return H(V); } };').map((x) => x.name)).toContain('GetName');
  3752. expect(infoOf('static FORCEINLINE void DoThing(int V) { H(V); }').map((x) => x.name)).toContain('DoThing');
  3753. expect(infoOf('static FORCEINLINE const FString& GetRef(int V) { return H(V); }').map((x) => x.name)).toContain('GetRef');
  3754. });
  3755. it('handles common third-party inline macros (pugixml, Godot, Boost, generic)', () => {
  3756. // pugixml: PUGI__FN before the return type; PUGIXML_FUNCTION (linkage)
  3757. // between the return type and the name — both recovered.
  3758. expect(infoOf('PUGI__FN void* default_allocate(size_t n) { return H(n); }').map((x) => x.name)).toContain('default_allocate');
  3759. expect(infoOf('PUGI__FN_NO_INLINE bool strequal(const char_t* a) { return H(a); }').map((x) => x.name)).toContain('strequal');
  3760. expect(infoOf('std::string PUGIXML_FUNCTION as_utf8(const wchar_t* s) { return H(s); }').map((x) => x.name)).toContain('as_utf8');
  3761. // Godot / Boost / generic inline hints
  3762. expect(infoOf('_FORCE_INLINE_ String get_name() const { return H(); }').map((x) => x.name)).toContain('get_name');
  3763. expect(infoOf('_ALWAYS_INLINE_ Vector2 get_pos() { return H(); }').map((x) => x.name)).toContain('get_pos');
  3764. expect(infoOf('BOOST_FORCEINLINE result_type call() { return H(); }').map((x) => x.name)).toContain('call');
  3765. expect(infoOf('ALWAYS_INLINE MyType compute() { return H(); }').map((x) => x.name)).toContain('compute');
  3766. });
  3767. it('leaves ordinary functions and real all-caps return types untouched (controls)', () => {
  3768. expect(infoOf('FString GetName(int V) { return H(V); }')).toEqual([{ name: 'GetName', ret: 'FString' }]);
  3769. // A real all-caps type that is NOT a listed inline macro stays the return type.
  3770. expect(infoOf('HRESULT DoIt(int V) { return H(V); }')).toEqual([{ name: 'DoIt', ret: 'HRESULT' }]);
  3771. });
  3772. it('blankCppInlineMacros preserves offsets and only touches specifier-position macros', () => {
  3773. // Blanked with equal-length spaces (byte offsets preserved).
  3774. expect(blankCppInlineMacros('FORCEINLINE FString F()')).toBe(' FString F()');
  3775. expect(blankCppInlineMacros('FORCEINLINE FString F()')).toHaveLength('FORCEINLINE FString F()'.length);
  3776. // Not in specifier position → untouched: string literals, expressions,
  3777. // longer word (`FORCEINLINE_COUNT`), and the fast path.
  3778. expect(blankCppInlineMacros('const char* s = "FORCEINLINE";')).toBe('const char* s = "FORCEINLINE";');
  3779. expect(blankCppInlineMacros('x = FORCEINLINE + 1;')).toBe('x = FORCEINLINE + 1;');
  3780. expect(blankCppInlineMacros('int FORCEINLINE_COUNT = 3;')).toBe('int FORCEINLINE_COUNT = 3;');
  3781. expect(blankCppInlineMacros('no macros here')).toBe('no macros here');
  3782. });
  3783. });
  3784. describe('C++ universal macro-mangled name recovery', () => {
  3785. // Curated pre-parse blanking can't list every library's inline macro, so a
  3786. // post-parse salvage recovers the real function name from ANY leftover
  3787. // `MACRO Ret name(…)` mangle — no list needed. It only ever touches an
  3788. // already-mangled name, so it can't corrupt a clean one.
  3789. const namesOf = (code: string, file = 's.cpp') =>
  3790. extractFromSource(file, code).nodes
  3791. .filter((n) => n.kind === 'method' || n.kind === 'function')
  3792. .map((n) => n.name);
  3793. it('recovers the name from a completely unknown macro (no list entry)', () => {
  3794. expect(namesOf('WEBKIT_EXPORT WTFString computeThing(int x) { return H(x); }')).toContain('computeThing');
  3795. expect(namesOf('SOMELIB_INLINE MyResult doWork(int x) { return H(x); }')).toContain('doWork');
  3796. expect(namesOf('MZ_FORCEINLINE char_t* to_str(double v) { return H(v); }')).toContain('to_str');
  3797. });
  3798. it('recoverMangledCppName only touches already-mangled names, with guards', () => {
  3799. // Recovered:
  3800. expect(recoverMangledCppName('WTFString computeThing')).toBe('computeThing');
  3801. expect(recoverMangledCppName('char_t* to_str(double v)')).toBe('to_str');
  3802. expect(recoverMangledCppName('unspecified_bool_type() const')).toBe('unspecified_bool_type');
  3803. // Left unchanged — clean names, operators, destructors, the `Ret (name)`
  3804. // idiom, and non-identifier tails:
  3805. expect(recoverMangledCppName('computeThing')).toBe('computeThing');
  3806. expect(recoverMangledCppName('operator EALSMovementState')).toBe('operator EALSMovementState');
  3807. expect(recoverMangledCppName('~Widget')).toBe('~Widget');
  3808. expect(recoverMangledCppName('bool (likely)')).toBe('bool (likely)');
  3809. expect(recoverMangledCppName('void (free)')).toBe('void (free)');
  3810. expect(recoverMangledCppName('QDockWidget *')).toBe('QDockWidget *');
  3811. });
  3812. it('does not disturb clean C++ names or non-C++ (Kotlin backtick) names', () => {
  3813. expect(namesOf('int foo(int x) { return x; }')).toEqual(['foo']);
  3814. // Kotlin backtick identifiers legitimately contain spaces; the salvage is
  3815. // C/C++-only, so they are untouched.
  3816. const kt = extractFromSource('T.kt', 'class T {\n fun `decode simple cert`() { }\n}').nodes
  3817. .filter((n) => n.kind === 'method' || n.kind === 'function')
  3818. .map((n) => n.name);
  3819. expect(kt).toContain('`decode simple cert`');
  3820. });
  3821. it('curated list now also covers Qt / Folly / Abseil / LLVM / V8 / Eigen / rapidjson (full recovery)', () => {
  3822. const info = (c: string) =>
  3823. extractFromSource('x.cpp', c).nodes
  3824. .filter((n) => n.kind === 'method' || n.kind === 'function')
  3825. .map((n) => ({ name: n.name, ret: n.returnType }));
  3826. expect(info('FOLLY_ALWAYS_INLINE Str f(int x) { return H(x); }')).toEqual([{ name: 'f', ret: 'Str' }]);
  3827. expect(namesOf('Q_INVOKABLE void onClicked() { H(); }')).toContain('onClicked');
  3828. expect(namesOf('ABSL_ATTRIBUTE_ALWAYS_INLINE int hash(int x) { return H(x); }')).toContain('hash');
  3829. expect(namesOf('EIGEN_STRONG_INLINE Scalar dot(const V& v) { return H(v); }')).toContain('dot');
  3830. expect(namesOf('V8_INLINE MaybeLocal Get(int i) { return H(i); }')).toContain('Get');
  3831. expect(namesOf('RAPIDJSON_FORCEINLINE bool Parse(const char* s) { return H(s); }')).toContain('Parse');
  3832. });
  3833. it('curated list spans the broader ecosystem (Mozilla, GLM, Bullet, OpenCV, Skia, EASTL, protobuf, fmt, Windows conventions)', () => {
  3834. const info = (c: string) =>
  3835. extractFromSource('x.cpp', c).nodes
  3836. .filter((n) => n.kind === 'method' || n.kind === 'function')
  3837. .map((n) => ({ name: n.name, ret: n.returnType }));
  3838. expect(info('MOZ_ALWAYS_INLINE Value get(int i) { return H(i); }')).toEqual([{ name: 'get', ret: 'Value' }]);
  3839. expect(info('GLM_FUNC_QUALIFIER vec3 cross(const vec3& a) { return H(a); }')).toEqual([{ name: 'cross', ret: 'vec3' }]);
  3840. expect(info('SIMD_FORCE_INLINE btScalar dot(const btVector3& v) const { return H(v); }')).toEqual([{ name: 'dot', ret: 'btScalar' }]);
  3841. expect(info('CV_INLINE Mat clone() const { return H(); }')).toEqual([{ name: 'clone', ret: 'Mat' }]);
  3842. expect(namesOf('PROTOBUF_ALWAYS_INLINE int size() const { return H(); }')).toContain('size');
  3843. expect(namesOf('FMT_CONSTEXPR auto parse(int x) { return H(x); }')).toContain('parse');
  3844. expect(namesOf('SK_ALWAYS_INLINE SkScalar width() const { return H(); }')).toContain('width');
  3845. expect(namesOf('EA_FORCE_INLINE size_type size() const { return H(); }')).toContain('size');
  3846. // Windows calling-convention macros sit between return type and name; the
  3847. // macro is blanked so the real return type survives.
  3848. expect(info('HRESULT WINAPI CreateThing(int x) { return H(x); }')).toEqual([{ name: 'CreateThing', ret: 'HRESULT' }]);
  3849. expect(info('ULONG STDMETHODCALLTYPE AddRef() { return H(); }')).toEqual([{ name: 'AddRef', ret: 'ULONG' }]);
  3850. });
  3851. });
  3852. describe('C++ templated base-class inheritance (#1043)', () => {
  3853. // Inheriting from a template (`class D : public Base<int>`) recorded the base
  3854. // ref as the full instantiation `Base<int>`, which never name-matched the
  3855. // template indexed as the bare node `Base`. The `<…>` args are stripped so the
  3856. // `extends` reference matches.
  3857. it('strips template args from a templated base so the extends ref is the bare name', () => {
  3858. const code = `
  3859. template<typename T> class Base {};
  3860. template<typename D> class CRTPBase {};
  3861. namespace ns { template<typename T> class Tpl {}; }
  3862. class Plain {};
  3863. class Widget : public Base<int> {};
  3864. class App : public CRTPBase<App> {};
  3865. class Q : public ns::Tpl<int> {};
  3866. class Both : public Base<char>, public Plain {};
  3867. `;
  3868. const extendsRefs = extractFromSource('f.cpp', code).unresolvedReferences.filter(
  3869. (r) => r.referenceKind === 'extends'
  3870. );
  3871. const names = extendsRefs.map((r) => r.referenceName);
  3872. // Templated bases carry the bare name, NOT the `<…>` instantiation.
  3873. expect(names).toContain('Base'); // from Base<int> / Base<char>
  3874. expect(names).toContain('CRTPBase'); // from CRTPBase<App> (CRTP)
  3875. expect(names).toContain('ns::Tpl'); // qualified head preserved, args dropped
  3876. expect(names).toContain('Plain'); // non-templated base unchanged
  3877. // No reference still carries angle brackets.
  3878. expect(names.find((n) => n.includes('<'))).toBeUndefined();
  3879. });
  3880. it('stripCppTemplateArgs removes balanced <…> at any depth and is a no-op without them', () => {
  3881. expect(stripCppTemplateArgs('Base<int>')).toBe('Base');
  3882. expect(stripCppTemplateArgs('ns::Tpl<int>')).toBe('ns::Tpl');
  3883. expect(stripCppTemplateArgs('ns::Tpl<Foo<int>>')).toBe('ns::Tpl'); // nested
  3884. expect(stripCppTemplateArgs('Outer<int>::Inner')).toBe('Outer::Inner'); // mid-name
  3885. expect(stripCppTemplateArgs('Base')).toBe('Base'); // no-op
  3886. expect(stripCppTemplateArgs('ns::Plain')).toBe('ns::Plain'); // no-op qualified
  3887. });
  3888. });
  3889. describe('C leading attribute macro before typedef return type (#1211)', () => {
  3890. // `SEC_ATTR UINT32 LostName(VOID)` — tree-sitter's C grammar reads the
  3891. // unknown macro as the type, the typedef'd return as the declarator, and
  3892. // stores the PARAMETER LIST as the function name ("(VOID)"). The
  3893. // structural pre-parse blank recovers the definition; the issue's whole
  3894. // isolation table is pinned here.
  3895. it("recovers the issue's full isolation table under their real names", () => {
  3896. const code = `#define SEC_ATTR __attribute__((section(".init")))
  3897. typedef unsigned int UINT32;
  3898. #define VOID void
  3899. SEC_ATTR VOID GoodName(VOID) { }
  3900. SEC_ATTR UINT32 LostName(VOID) { return 0; }
  3901. UINT32 NoAttr(void) { return 0; }
  3902. SEC_ATTR int BuiltinRet(void) { return 0; }
  3903. __attribute__((section(".init"))) UINT32 RawAttr(void) { return 0; }
  3904. SEC_ATTR UINT32 OneNamedArg(UINT32 x) { return x; }
  3905. SEC_ATTR UINT32* PtrRet(VOID) { return 0; }
  3906. `;
  3907. const result = extractFromSource('attrs.c', code);
  3908. const fns = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name);
  3909. expect(fns).toEqual(
  3910. expect.arrayContaining([
  3911. 'GoodName', 'LostName', 'NoAttr', 'BuiltinRet', 'RawAttr', 'OneNamedArg', 'PtrRet',
  3912. ])
  3913. );
  3914. // The bug shape: a parameter list stored as a name.
  3915. expect(fns.find((n) => n.includes('('))).toBeUndefined();
  3916. });
  3917. it('blankCLeadingAttrMacros only touches the MACRO-ret-name-( definition shape', () => {
  3918. // Blanked: the definition shape (offset-preserving).
  3919. expect(blankCLeadingAttrMacros('SEC_ATTR UINT32 f(void) {}')).toBe(
  3920. ' UINT32 f(void) {}'
  3921. );
  3922. // Untouched: a plain typedef'd return with ONE identifier before `(`.
  3923. expect(blankCLeadingAttrMacros('UINT32 helper(void) {}')).toBe('UINT32 helper(void) {}');
  3924. // Untouched: an ALL-CAPS function CALL at line start.
  3925. expect(blankCLeadingAttrMacros('MY_ASSERT(x);')).toBe('MY_ASSERT(x);');
  3926. // Untouched: #define lines (start with #, not line-leading CAPS).
  3927. const def = '#define SEC_ATTR __attribute__((section(".init")))';
  3928. expect(blankCLeadingAttrMacros(def)).toBe(def);
  3929. // Untouched: multi-word builtin returns (the grammar keeps the name there).
  3930. expect(blankCLeadingAttrMacros('SEC_ATTR unsigned int f(void) {}')).toBe(
  3931. 'SEC_ATTR unsigned int f(void) {}'
  3932. );
  3933. // Untouched: mid-line uses.
  3934. expect(blankCLeadingAttrMacros('x = SEC_ATTR UINT32 y(z);')).toBe(
  3935. 'x = SEC_ATTR UINT32 y(z);'
  3936. );
  3937. });
  3938. });
  3939. describe('C++ out-of-line template method receivers (#1286)', () => {
  3940. // `template<typename T> T Box<T>::get()` used to store qualified_name
  3941. // `Box<T>::get` — the `<T>` qualifier never matched the class node indexed
  3942. // as `Box`, and long multi-line parameter lists could push qualified_name
  3943. // past NAME_MAX. Inline definitions of the same method produce `Box::get`,
  3944. // so the out-of-line form must normalize to the identical name.
  3945. it('strips the template parameter list from the receiver qualifier', () => {
  3946. const code = `template <typename T>
  3947. class Box {
  3948. public:
  3949. T get() const;
  3950. void set(T v);
  3951. private:
  3952. T value;
  3953. };
  3954. template <typename T> T Box<T>::get() const { return value; }
  3955. template <typename T> void Box<T>::set(T v) { value = v; }
  3956. `;
  3957. const result = extractFromSource('box.cpp', code);
  3958. expect(result.errors).toHaveLength(0);
  3959. const methods = result.nodes.filter((n) => n.kind === 'method');
  3960. const qns = methods.map((n) => n.qualifiedName).sort();
  3961. // Out-of-line definitions carry the SAME qualifier as the class node.
  3962. expect(qns).toContain('Box::get');
  3963. expect(qns).toContain('Box::set');
  3964. expect(qns.find((q) => q?.includes('<'))).toBeUndefined();
  3965. // Names themselves stay clean.
  3966. expect(methods.map((n) => n.name).sort()).toEqual(expect.arrayContaining(['get', 'set']));
  3967. });
  3968. it('multi-line template parameter lists cannot leak into qualified_name (NAME_MAX overflow shape)', () => {
  3969. // The ICU capi_helper.h shape: enormous multi-line parameter names made
  3970. // qualified_name 272 bytes (> NAME_MAX 255) including embedded newlines.
  3971. const code = `template <typename CType,
  3972. typename CPPType,
  3973. int32_t kMagicValidationSentinelConstantForTheHelperTemplateClassInstanceGuardLong>
  3974. class ApiHelper {
  3975. public:
  3976. CPPType* validate();
  3977. };
  3978. template <typename CType,
  3979. typename CPPType,
  3980. int32_t kMagicValidationSentinelConstantForTheHelperTemplateClassInstanceGuardLong>
  3981. CPPType* ApiHelper<CType,
  3982. CPPType,
  3983. kMagicValidationSentinelConstantForTheHelperTemplateClassInstanceGuardLong>::validate() {
  3984. return nullptr;
  3985. }
  3986. `;
  3987. const result = extractFromSource('capi_helper.h', code);
  3988. const validate = result.nodes.find((n) => n.kind === 'method' && n.name === 'validate' && n.qualifiedName?.includes('::'));
  3989. expect(validate).toBeDefined();
  3990. expect(validate!.qualifiedName).toBe('ApiHelper::validate');
  3991. expect(validate!.qualifiedName!.length).toBeLessThan(255);
  3992. expect(validate!.qualifiedName).not.toMatch(/[<>\n]/);
  3993. });
  3994. });
  3995. describe('C++ stack-allocation construction (#1035)', () => {
  3996. // `Calculator calc(0)` (direct-init) and `Widget w{1, 2}` (brace-init) carry
  3997. // the constructor args directly on the declarator — no call/new node — so
  3998. // they emitted no constructor reference, unlike heap `new Calculator(0)`. An
  3999. // `instantiates` ref to the constructed type is now emitted for both.
  4000. const instNames = (code: string) =>
  4001. extractFromSource('f.cpp', `void run() {\n${code}\n}`)
  4002. .unresolvedReferences.filter((r) => r.referenceKind === 'instantiates')
  4003. .map((r) => r.referenceName);
  4004. it('emits an instantiates ref for direct-init and brace-init', () => {
  4005. expect(instNames('Calculator calc(0);')).toEqual(['Calculator']);
  4006. expect(instNames('Widget w{1, 2};')).toEqual(['Widget']);
  4007. });
  4008. it('strips template args and namespace to the bare class name', () => {
  4009. // `std::vector<int> v(10)` → `vector`; `ns::Widget w(0)` → `Widget`.
  4010. expect(instNames('std::vector<int> v(10);')).toEqual(['vector']);
  4011. expect(instNames('ns::Widget w(0);')).toEqual(['Widget']);
  4012. });
  4013. it('does not emit for primitives, default construction, or the most-vexing parse', () => {
  4014. expect(instNames('int x(5);')).toEqual([]); // primitive direct-init
  4015. expect(instNames('int y{6};')).toEqual([]); // primitive brace-init
  4016. expect(instNames('auto z = make();')).toEqual([]); // auto + call (handled elsewhere)
  4017. expect(instNames('Calculator deferred;')).toEqual([]); // default construction, no args
  4018. expect(instNames('Calculator calc();')).toEqual([]); // function declaration (most-vexing parse)
  4019. });
  4020. it('emits a single instantiates ref for a multi-declarator statement', () => {
  4021. // `Calculator a(1), b(2);` shares one `type` field; both construct a
  4022. // Calculator, so one ref suffices (it dedups to one edge regardless).
  4023. expect(instNames('Calculator a(1), b(2);')).toEqual(['Calculator']);
  4024. });
  4025. });
  4026. describe('C/C++ imports', () => {
  4027. it('should extract system include', () => {
  4028. const code = `#include <iostream>`;
  4029. const result = extractFromSource('main.cpp', code);
  4030. const importNode = result.nodes.find((n) => n.kind === 'import');
  4031. expect(importNode).toBeDefined();
  4032. expect(importNode?.name).toBe('iostream');
  4033. expect(importNode?.signature).toBe('#include <iostream>');
  4034. });
  4035. it('should extract system include with path', () => {
  4036. const code = `#include <nlohmann/json.hpp>`;
  4037. const result = extractFromSource('app.cpp', code);
  4038. const importNode = result.nodes.find((n) => n.kind === 'import');
  4039. expect(importNode).toBeDefined();
  4040. expect(importNode?.name).toBe('nlohmann/json.hpp');
  4041. });
  4042. it('should extract local include', () => {
  4043. const code = `#include "myheader.h"`;
  4044. const result = extractFromSource('main.cpp', code);
  4045. const importNode = result.nodes.find((n) => n.kind === 'import');
  4046. expect(importNode).toBeDefined();
  4047. expect(importNode?.name).toBe('myheader.h');
  4048. });
  4049. it('should extract C header', () => {
  4050. const code = `#include <stdio.h>`;
  4051. const result = extractFromSource('main.c', code);
  4052. const importNode = result.nodes.find((n) => n.kind === 'import');
  4053. expect(importNode).toBeDefined();
  4054. expect(importNode?.name).toBe('stdio.h');
  4055. });
  4056. it('should extract multiple includes', () => {
  4057. const code = `
  4058. #include <iostream>
  4059. #include <vector>
  4060. #include "config.h"
  4061. `;
  4062. const result = extractFromSource('app.cpp', code);
  4063. const importNodes = result.nodes.filter((n) => n.kind === 'import');
  4064. expect(importNodes.length).toBe(3);
  4065. const names = importNodes.map((n) => n.name);
  4066. expect(names).toContain('iostream');
  4067. expect(names).toContain('vector');
  4068. expect(names).toContain('config.h');
  4069. });
  4070. it('should create unresolved references for local includes', () => {
  4071. const code = `#include "myheader.h"`;
  4072. const result = extractFromSource('main.cpp', code);
  4073. const importRef = result.unresolvedReferences.find(
  4074. (r) => r.referenceKind === 'imports' && r.referenceName === 'myheader.h'
  4075. );
  4076. expect(importRef).toBeDefined();
  4077. expect(importRef?.line).toBe(1);
  4078. });
  4079. it('should create unresolved references for system includes', () => {
  4080. const code = `#include <iostream>`;
  4081. const result = extractFromSource('main.cpp', code);
  4082. const importRef = result.unresolvedReferences.find(
  4083. (r) => r.referenceKind === 'imports' && r.referenceName === 'iostream'
  4084. );
  4085. expect(importRef).toBeDefined();
  4086. });
  4087. });
  4088. describe('Dart imports', () => {
  4089. it('should extract dart: import', () => {
  4090. const code = `import 'dart:async';`;
  4091. const result = extractFromSource('main.dart', code);
  4092. const importNode = result.nodes.find((n) => n.kind === 'import');
  4093. expect(importNode).toBeDefined();
  4094. expect(importNode?.name).toBe('dart:async');
  4095. expect(importNode?.signature).toBe("import 'dart:async';");
  4096. });
  4097. it('should extract package import', () => {
  4098. const code = `import 'package:flutter/material.dart';`;
  4099. const result = extractFromSource('app.dart', code);
  4100. const importNode = result.nodes.find((n) => n.kind === 'import');
  4101. expect(importNode).toBeDefined();
  4102. expect(importNode?.name).toBe('package:flutter/material.dart');
  4103. });
  4104. it('should extract aliased import', () => {
  4105. const code = `import 'package:http/http.dart' as http;`;
  4106. const result = extractFromSource('api.dart', code);
  4107. const importNode = result.nodes.find((n) => n.kind === 'import');
  4108. expect(importNode).toBeDefined();
  4109. expect(importNode?.name).toBe('package:http/http.dart');
  4110. expect(importNode?.signature).toContain('as http');
  4111. });
  4112. it('should extract multiple imports', () => {
  4113. const code = `
  4114. import 'dart:async';
  4115. import 'dart:convert';
  4116. import 'package:flutter/material.dart';
  4117. `;
  4118. const result = extractFromSource('main.dart', code);
  4119. const importNodes = result.nodes.filter((n) => n.kind === 'import');
  4120. expect(importNodes.length).toBe(3);
  4121. const names = importNodes.map((n) => n.name);
  4122. expect(names).toContain('dart:async');
  4123. expect(names).toContain('dart:convert');
  4124. expect(names).toContain('package:flutter/material.dart');
  4125. });
  4126. it('should extract relative import', () => {
  4127. const code = `import '../utils/helpers.dart';`;
  4128. const result = extractFromSource('lib/main.dart', code);
  4129. const importNode = result.nodes.find((n) => n.kind === 'import');
  4130. expect(importNode).toBeDefined();
  4131. expect(importNode?.name).toBe('../utils/helpers.dart');
  4132. });
  4133. });
  4134. describe('Liquid imports', () => {
  4135. it('should extract render tag', () => {
  4136. const code = `{% render 'loading-spinner' %}`;
  4137. const result = extractFromSource('template.liquid', code);
  4138. const importNode = result.nodes.find((n) => n.kind === 'import');
  4139. expect(importNode).toBeDefined();
  4140. expect(importNode?.name).toBe('loading-spinner');
  4141. expect(importNode?.signature).toContain('render');
  4142. });
  4143. it('should extract section tag', () => {
  4144. const code = `{% section 'header' %}`;
  4145. const result = extractFromSource('layout/theme.liquid', code);
  4146. const importNode = result.nodes.find((n) => n.kind === 'import');
  4147. expect(importNode).toBeDefined();
  4148. expect(importNode?.name).toBe('header');
  4149. expect(importNode?.signature).toContain('section');
  4150. });
  4151. it('should extract include tag', () => {
  4152. const code = `{% include 'icon-cart' %}`;
  4153. const result = extractFromSource('snippets/header.liquid', code);
  4154. const importNode = result.nodes.find((n) => n.kind === 'import');
  4155. expect(importNode).toBeDefined();
  4156. expect(importNode?.name).toBe('icon-cart');
  4157. expect(importNode?.signature).toContain('include');
  4158. });
  4159. it('should extract render with whitespace control', () => {
  4160. const code = `{%- render 'price' -%}`;
  4161. const result = extractFromSource('snippets/product.liquid', code);
  4162. const importNode = result.nodes.find((n) => n.kind === 'import');
  4163. expect(importNode).toBeDefined();
  4164. expect(importNode?.name).toBe('price');
  4165. });
  4166. it('should extract multiple imports', () => {
  4167. const code = `
  4168. {% section 'header' %}
  4169. {% render 'loading-spinner' %}
  4170. {% render 'cart-drawer' %}
  4171. `;
  4172. const result = extractFromSource('layout/theme.liquid', code);
  4173. const importNodes = result.nodes.filter((n) => n.kind === 'import');
  4174. expect(importNodes.length).toBe(3);
  4175. const names = importNodes.map((n) => n.name);
  4176. expect(names).toContain('header');
  4177. expect(names).toContain('loading-spinner');
  4178. expect(names).toContain('cart-drawer');
  4179. });
  4180. });
  4181. });
  4182. // =============================================================================
  4183. // Pascal / Delphi Extraction
  4184. // =============================================================================
  4185. describe('Pascal / Delphi Extraction', () => {
  4186. describe('Language detection', () => {
  4187. it('should detect Pascal files', () => {
  4188. expect(detectLanguage('UAuth.pas')).toBe('pascal');
  4189. expect(detectLanguage('App.dpr')).toBe('pascal');
  4190. expect(detectLanguage('Package.dpk')).toBe('pascal');
  4191. expect(detectLanguage('App.lpr')).toBe('pascal');
  4192. expect(detectLanguage('MainForm.dfm')).toBe('pascal');
  4193. expect(detectLanguage('MainForm.fmx')).toBe('pascal');
  4194. });
  4195. it('should report Pascal as supported', () => {
  4196. expect(isLanguageSupported('pascal')).toBe(true);
  4197. expect(getSupportedLanguages()).toContain('pascal');
  4198. });
  4199. });
  4200. describe('Unit extraction', () => {
  4201. it('should extract unit as module', () => {
  4202. const code = `unit MyUnit;\ninterface\nimplementation\nend.`;
  4203. const result = extractFromSource('MyUnit.pas', code);
  4204. const moduleNode = result.nodes.find((n) => n.kind === 'module');
  4205. expect(moduleNode).toBeDefined();
  4206. expect(moduleNode?.name).toBe('MyUnit');
  4207. expect(moduleNode?.language).toBe('pascal');
  4208. });
  4209. it('should extract program as module', () => {
  4210. const code = `program MyApp;\nbegin\nend.`;
  4211. const result = extractFromSource('MyApp.dpr', code);
  4212. const moduleNode = result.nodes.find((n) => n.kind === 'module');
  4213. expect(moduleNode).toBeDefined();
  4214. expect(moduleNode?.name).toBe('MyApp');
  4215. });
  4216. it('should fallback to filename when module name is empty', () => {
  4217. // Some .dpr templates use "program;" without a name
  4218. const code = `program;\nuses SysUtils;\nbegin\nend.`;
  4219. const result = extractFromSource('Console.dpr', code);
  4220. const moduleNode = result.nodes.find((n) => n.kind === 'module');
  4221. expect(moduleNode).toBeDefined();
  4222. expect(moduleNode?.name).toBe('Console');
  4223. });
  4224. });
  4225. describe('Uses clause (imports)', () => {
  4226. it('should extract uses as individual imports', () => {
  4227. const code = `unit Test;\ninterface\nuses\n System.SysUtils,\n System.Classes;\nimplementation\nend.`;
  4228. const result = extractFromSource('Test.pas', code);
  4229. const imports = result.nodes.filter((n) => n.kind === 'import');
  4230. expect(imports.length).toBe(2);
  4231. expect(imports.map((n) => n.name)).toContain('System.SysUtils');
  4232. expect(imports.map((n) => n.name)).toContain('System.Classes');
  4233. });
  4234. it('should create unresolved references for imports', () => {
  4235. const code = `unit Test;\ninterface\nuses\n UAuth;\nimplementation\nend.`;
  4236. const result = extractFromSource('Test.pas', code);
  4237. const importRef = result.unresolvedReferences.find(
  4238. (r) => r.referenceKind === 'imports'
  4239. );
  4240. expect(importRef).toBeDefined();
  4241. expect(importRef?.referenceName).toBe('UAuth');
  4242. });
  4243. });
  4244. describe('Class extraction', () => {
  4245. it('should extract class declarations', () => {
  4246. const code = `unit Test;\ninterface\ntype\n TMyClass = class\n public\n procedure DoSomething;\n end;\nimplementation\nend.`;
  4247. const result = extractFromSource('Test.pas', code);
  4248. const classNode = result.nodes.find((n) => n.kind === 'class');
  4249. expect(classNode).toBeDefined();
  4250. expect(classNode?.name).toBe('TMyClass');
  4251. });
  4252. it('should extract class with inheritance', () => {
  4253. const code = `unit Test;\ninterface\ntype\n TChild = class(TParent)\n end;\nimplementation\nend.`;
  4254. const result = extractFromSource('Test.pas', code);
  4255. const extendsRef = result.unresolvedReferences.find(
  4256. (r) => r.referenceKind === 'extends'
  4257. );
  4258. expect(extendsRef).toBeDefined();
  4259. expect(extendsRef?.referenceName).toBe('TParent');
  4260. });
  4261. it('should extract class with interface implementation', () => {
  4262. const code = `unit Test;\ninterface\ntype\n TService = class(TInterfacedObject, ILogger)\n end;\nimplementation\nend.`;
  4263. const result = extractFromSource('Test.pas', code);
  4264. const extendsRef = result.unresolvedReferences.find(
  4265. (r) => r.referenceKind === 'extends'
  4266. );
  4267. const implementsRef = result.unresolvedReferences.find(
  4268. (r) => r.referenceKind === 'implements'
  4269. );
  4270. expect(extendsRef?.referenceName).toBe('TInterfacedObject');
  4271. expect(implementsRef?.referenceName).toBe('ILogger');
  4272. });
  4273. });
  4274. describe('Record extraction', () => {
  4275. it('should extract records as class nodes', () => {
  4276. const code = `unit Test;\ninterface\ntype\n TPoint = record\n X: Double;\n Y: Double;\n end;\nimplementation\nend.`;
  4277. const result = extractFromSource('Test.pas', code);
  4278. const classNode = result.nodes.find((n) => n.kind === 'class');
  4279. expect(classNode).toBeDefined();
  4280. expect(classNode?.name).toBe('TPoint');
  4281. const fields = result.nodes.filter((n) => n.kind === 'field');
  4282. expect(fields.length).toBe(2);
  4283. expect(fields.map((f) => f.name)).toContain('X');
  4284. expect(fields.map((f) => f.name)).toContain('Y');
  4285. });
  4286. });
  4287. describe('Interface extraction', () => {
  4288. it('should extract interface declarations', () => {
  4289. const code = `unit Test;\ninterface\ntype\n ILogger = interface\n procedure Log(const AMsg: string);\n end;\nimplementation\nend.`;
  4290. const result = extractFromSource('Test.pas', code);
  4291. const ifaceNode = result.nodes.find((n) => n.kind === 'interface');
  4292. expect(ifaceNode).toBeDefined();
  4293. expect(ifaceNode?.name).toBe('ILogger');
  4294. });
  4295. });
  4296. describe('Method extraction', () => {
  4297. it('should extract methods with visibility', () => {
  4298. const code = `unit Test;\ninterface\ntype\n TMyClass = class\n private\n FValue: Integer;\n public\n constructor Create;\n function GetValue: Integer;\n end;\nimplementation\nend.`;
  4299. const result = extractFromSource('Test.pas', code);
  4300. const methods = result.nodes.filter((n) => n.kind === 'method');
  4301. expect(methods.length).toBe(2);
  4302. const createMethod = methods.find((m) => m.name === 'Create');
  4303. expect(createMethod?.visibility).toBe('public');
  4304. const getValue = methods.find((m) => m.name === 'GetValue');
  4305. expect(getValue?.visibility).toBe('public');
  4306. const fields = result.nodes.filter((n) => n.kind === 'field');
  4307. const fValue = fields.find((f) => f.name === 'FValue');
  4308. expect(fValue?.visibility).toBe('private');
  4309. });
  4310. it('should detect static methods (class methods)', () => {
  4311. const code = `unit Test;\ninterface\ntype\n THelper = class\n public\n class function Create: THelper; static;\n end;\nimplementation\nend.`;
  4312. const result = extractFromSource('Test.pas', code);
  4313. const methods = result.nodes.filter((n) => n.kind === 'method');
  4314. const staticMethod = methods.find((m) => m.name === 'Create');
  4315. expect(staticMethod?.isStatic).toBe(true);
  4316. });
  4317. });
  4318. describe('Enum extraction', () => {
  4319. it('should extract enums with members', () => {
  4320. const code = `unit Test;\ninterface\ntype\n TColor = (clRed, clGreen, clBlue);\nimplementation\nend.`;
  4321. const result = extractFromSource('Test.pas', code);
  4322. const enumNode = result.nodes.find((n) => n.kind === 'enum');
  4323. expect(enumNode).toBeDefined();
  4324. expect(enumNode?.name).toBe('TColor');
  4325. const members = result.nodes.filter((n) => n.kind === 'enum_member');
  4326. expect(members.length).toBe(3);
  4327. expect(members.map((m) => m.name)).toEqual(['clRed', 'clGreen', 'clBlue']);
  4328. });
  4329. });
  4330. describe('Property extraction', () => {
  4331. it('should extract properties', () => {
  4332. const code = `unit Test;\ninterface\ntype\n TObj = class\n public\n property Name: string read FName write FName;\n end;\nimplementation\nend.`;
  4333. const result = extractFromSource('Test.pas', code);
  4334. const propNode = result.nodes.find((n) => n.kind === 'property');
  4335. expect(propNode).toBeDefined();
  4336. expect(propNode?.name).toBe('Name');
  4337. expect(propNode?.visibility).toBe('public');
  4338. });
  4339. });
  4340. describe('Constant extraction', () => {
  4341. it('should extract constants', () => {
  4342. const code = `unit Test;\ninterface\nconst\n MAX_RETRIES = 3;\n APP_NAME = 'MyApp';\nimplementation\nend.`;
  4343. const result = extractFromSource('Test.pas', code);
  4344. const constants = result.nodes.filter((n) => n.kind === 'constant');
  4345. expect(constants.length).toBe(2);
  4346. expect(constants.map((c) => c.name)).toContain('MAX_RETRIES');
  4347. expect(constants.map((c) => c.name)).toContain('APP_NAME');
  4348. });
  4349. });
  4350. describe('Type alias extraction', () => {
  4351. it('should extract type aliases', () => {
  4352. const code = `unit Test;\ninterface\ntype\n TUserName = string;\nimplementation\nend.`;
  4353. const result = extractFromSource('Test.pas', code);
  4354. const aliasNode = result.nodes.find((n) => n.kind === 'type_alias');
  4355. expect(aliasNode).toBeDefined();
  4356. expect(aliasNode?.name).toBe('TUserName');
  4357. });
  4358. });
  4359. describe('Call extraction', () => {
  4360. it('should extract calls from implementation bodies', () => {
  4361. const code = `unit Test;\ninterface\ntype\n TObj = class\n public\n procedure DoWork;\n end;\nimplementation\nprocedure TObj.DoWork;\nbegin\n WriteLn('hello');\nend;\nend.`;
  4362. const result = extractFromSource('Test.pas', code);
  4363. const callRef = result.unresolvedReferences.find(
  4364. (r) => r.referenceKind === 'calls'
  4365. );
  4366. expect(callRef).toBeDefined();
  4367. expect(callRef?.referenceName).toBe('WriteLn');
  4368. });
  4369. });
  4370. describe('Containment edges', () => {
  4371. it('should create contains edges for class members', () => {
  4372. const code = `unit Test;\ninterface\ntype\n TObj = class\n public\n procedure Foo;\n end;\nimplementation\nend.`;
  4373. const result = extractFromSource('Test.pas', code);
  4374. const classNode = result.nodes.find((n) => n.kind === 'class');
  4375. const methodNode = result.nodes.find((n) => n.kind === 'method');
  4376. expect(classNode).toBeDefined();
  4377. expect(methodNode).toBeDefined();
  4378. const containsEdge = result.edges.find(
  4379. (e) => e.source === classNode?.id && e.target === methodNode?.id && e.kind === 'contains'
  4380. );
  4381. expect(containsEdge).toBeDefined();
  4382. });
  4383. });
  4384. describe('Full fixture: UAuth.pas', () => {
  4385. const code = `unit UAuth;
  4386. interface
  4387. uses
  4388. System.SysUtils,
  4389. System.Classes;
  4390. type
  4391. ITokenValidator = interface
  4392. ['{11111111-1111-1111-1111-111111111111}']
  4393. function Validate(const AToken: string): Boolean;
  4394. end;
  4395. TAuthService = class(TInterfacedObject, ITokenValidator)
  4396. private
  4397. FToken: string;
  4398. FLoginCount: Integer;
  4399. procedure IncLoginCount;
  4400. protected
  4401. function GetToken: string;
  4402. public
  4403. constructor Create;
  4404. destructor Destroy; override;
  4405. function Validate(const AToken: string): Boolean;
  4406. function Login(const AUser, APass: string): string;
  4407. property Token: string read GetToken;
  4408. property LoginCount: Integer read FLoginCount;
  4409. end;
  4410. implementation
  4411. constructor TAuthService.Create;
  4412. begin
  4413. inherited Create;
  4414. FToken := '';
  4415. FLoginCount := 0;
  4416. end;
  4417. destructor TAuthService.Destroy;
  4418. begin
  4419. FToken := '';
  4420. inherited Destroy;
  4421. end;
  4422. procedure TAuthService.IncLoginCount;
  4423. begin
  4424. Inc(FLoginCount);
  4425. end;
  4426. function TAuthService.GetToken: string;
  4427. begin
  4428. Result := FToken;
  4429. end;
  4430. function TAuthService.Validate(const AToken: string): Boolean;
  4431. begin
  4432. Result := AToken <> '';
  4433. end;
  4434. function TAuthService.Login(const AUser, APass: string): string;
  4435. begin
  4436. IncLoginCount;
  4437. if Validate(AUser + ':' + APass) then
  4438. begin
  4439. FToken := AUser;
  4440. Result := 'ok';
  4441. end
  4442. else
  4443. Result := '';
  4444. end;
  4445. end.`;
  4446. it('should extract all expected nodes', () => {
  4447. const result = extractFromSource('UAuth.pas', code);
  4448. expect(result.errors).toHaveLength(0);
  4449. // Module
  4450. const moduleNode = result.nodes.find((n) => n.kind === 'module');
  4451. expect(moduleNode?.name).toBe('UAuth');
  4452. // Imports
  4453. const imports = result.nodes.filter((n) => n.kind === 'import');
  4454. expect(imports.length).toBe(2);
  4455. // Interface
  4456. const ifaceNode = result.nodes.find((n) => n.kind === 'interface');
  4457. expect(ifaceNode?.name).toBe('ITokenValidator');
  4458. // Class
  4459. const classNode = result.nodes.find((n) => n.kind === 'class');
  4460. expect(classNode?.name).toBe('TAuthService');
  4461. // Methods
  4462. const methods = result.nodes.filter((n) => n.kind === 'method');
  4463. expect(methods.length).toBeGreaterThanOrEqual(6);
  4464. expect(methods.map((m) => m.name)).toContain('Create');
  4465. expect(methods.map((m) => m.name)).toContain('Destroy');
  4466. expect(methods.map((m) => m.name)).toContain('Login');
  4467. // Fields
  4468. const fields = result.nodes.filter((n) => n.kind === 'field');
  4469. expect(fields.length).toBe(2);
  4470. expect(fields.every((f) => f.visibility === 'private')).toBe(true);
  4471. // Properties
  4472. const props = result.nodes.filter((n) => n.kind === 'property');
  4473. expect(props.length).toBe(2);
  4474. expect(props.map((p) => p.name)).toContain('Token');
  4475. expect(props.map((p) => p.name)).toContain('LoginCount');
  4476. });
  4477. it('should extract inheritance and interface implementation', () => {
  4478. const result = extractFromSource('UAuth.pas', code);
  4479. const extendsRef = result.unresolvedReferences.find(
  4480. (r) => r.referenceKind === 'extends'
  4481. );
  4482. expect(extendsRef?.referenceName).toBe('TInterfacedObject');
  4483. const implementsRef = result.unresolvedReferences.find(
  4484. (r) => r.referenceKind === 'implements'
  4485. );
  4486. expect(implementsRef?.referenceName).toBe('ITokenValidator');
  4487. });
  4488. it('should extract calls from implementation', () => {
  4489. const result = extractFromSource('UAuth.pas', code);
  4490. const callRefs = result.unresolvedReferences.filter(
  4491. (r) => r.referenceKind === 'calls'
  4492. );
  4493. expect(callRefs.map((r) => r.referenceName)).toContain('Inc');
  4494. expect(callRefs.map((r) => r.referenceName)).toContain('Validate');
  4495. });
  4496. });
  4497. describe('Full fixture: UTypes.pas', () => {
  4498. const code = `unit UTypes;
  4499. interface
  4500. uses
  4501. System.SysUtils;
  4502. const
  4503. C_MAX_RETRIES = 3;
  4504. C_DEFAULT_NAME = 'Guest';
  4505. type
  4506. TUserRole = (urAdmin, urEditor, urViewer);
  4507. TPoint2D = record
  4508. X: Double;
  4509. Y: Double;
  4510. end;
  4511. TUserName = string;
  4512. TUserInfo = class
  4513. public
  4514. type
  4515. TAddress = record
  4516. Street: string;
  4517. City: string;
  4518. Zip: string;
  4519. end;
  4520. private
  4521. FName: TUserName;
  4522. FRole: TUserRole;
  4523. FAddress: TAddress;
  4524. public
  4525. constructor Create(const AName: TUserName; ARole: TUserRole);
  4526. function GetDisplayName: string;
  4527. class function CreateAdmin(const AName: TUserName): TUserInfo; static;
  4528. property Name: TUserName read FName write FName;
  4529. property Role: TUserRole read FRole;
  4530. property Address: TAddress read FAddress write FAddress;
  4531. end;
  4532. implementation
  4533. constructor TUserInfo.Create(const AName: TUserName; ARole: TUserRole);
  4534. begin
  4535. FName := AName;
  4536. FRole := ARole;
  4537. end;
  4538. function TUserInfo.GetDisplayName: string;
  4539. begin
  4540. if FRole = urAdmin then
  4541. Result := '[Admin] ' + FName
  4542. else
  4543. Result := FName;
  4544. end;
  4545. class function TUserInfo.CreateAdmin(const AName: TUserName): TUserInfo;
  4546. begin
  4547. Result := TUserInfo.Create(AName, urAdmin);
  4548. end;
  4549. end.`;
  4550. it('should extract enums with members', () => {
  4551. const result = extractFromSource('UTypes.pas', code);
  4552. const enumNode = result.nodes.find((n) => n.kind === 'enum');
  4553. expect(enumNode?.name).toBe('TUserRole');
  4554. const members = result.nodes.filter((n) => n.kind === 'enum_member');
  4555. expect(members.length).toBe(3);
  4556. expect(members.map((m) => m.name)).toEqual(['urAdmin', 'urEditor', 'urViewer']);
  4557. });
  4558. it('should extract constants', () => {
  4559. const result = extractFromSource('UTypes.pas', code);
  4560. const constants = result.nodes.filter((n) => n.kind === 'constant');
  4561. expect(constants.length).toBe(2);
  4562. expect(constants.map((c) => c.name)).toContain('C_MAX_RETRIES');
  4563. expect(constants.map((c) => c.name)).toContain('C_DEFAULT_NAME');
  4564. });
  4565. it('should extract type aliases', () => {
  4566. const result = extractFromSource('UTypes.pas', code);
  4567. const aliases = result.nodes.filter((n) => n.kind === 'type_alias');
  4568. expect(aliases.map((a) => a.name)).toContain('TUserName');
  4569. });
  4570. it('should extract records as classes with fields', () => {
  4571. const result = extractFromSource('UTypes.pas', code);
  4572. const classes = result.nodes.filter((n) => n.kind === 'class');
  4573. expect(classes.map((c) => c.name)).toContain('TPoint2D');
  4574. // TPoint2D fields
  4575. const fields = result.nodes.filter((n) => n.kind === 'field');
  4576. expect(fields.map((f) => f.name)).toContain('X');
  4577. expect(fields.map((f) => f.name)).toContain('Y');
  4578. });
  4579. it('should extract static class methods', () => {
  4580. const result = extractFromSource('UTypes.pas', code);
  4581. const methods = result.nodes.filter((n) => n.kind === 'method');
  4582. const staticMethod = methods.find((m) => m.name === 'CreateAdmin');
  4583. expect(staticMethod).toBeDefined();
  4584. expect(staticMethod?.isStatic).toBe(true);
  4585. });
  4586. it('should extract nested types', () => {
  4587. const result = extractFromSource('UTypes.pas', code);
  4588. const classes = result.nodes.filter((n) => n.kind === 'class');
  4589. expect(classes.map((c) => c.name)).toContain('TAddress');
  4590. });
  4591. });
  4592. });
  4593. // =============================================================================
  4594. // DFM/FMX Extraction
  4595. // =============================================================================
  4596. describe('DFM/FMX Extraction', () => {
  4597. it('should extract components from DFM', () => {
  4598. const code = `object Form1: TForm1
  4599. Left = 0
  4600. Top = 0
  4601. Caption = 'My Form'
  4602. object Button1: TButton
  4603. Left = 10
  4604. Top = 10
  4605. Caption = 'Click Me'
  4606. end
  4607. end`;
  4608. const result = extractFromSource('Form1.dfm', code);
  4609. const components = result.nodes.filter((n) => n.kind === 'component');
  4610. expect(components.length).toBe(2);
  4611. expect(components.map((c) => c.name)).toContain('Form1');
  4612. expect(components.map((c) => c.name)).toContain('Button1');
  4613. const button = components.find((c) => c.name === 'Button1');
  4614. expect(button?.signature).toBe('TButton');
  4615. });
  4616. it('should extract nested component hierarchy', () => {
  4617. const code = `object Form1: TForm1
  4618. object Panel1: TPanel
  4619. object Label1: TLabel
  4620. Caption = 'Hello'
  4621. end
  4622. end
  4623. end`;
  4624. const result = extractFromSource('Form1.dfm', code);
  4625. const components = result.nodes.filter((n) => n.kind === 'component');
  4626. expect(components.length).toBe(3);
  4627. // Check nesting: Panel1 contains Label1
  4628. const panel = components.find((c) => c.name === 'Panel1');
  4629. const label = components.find((c) => c.name === 'Label1');
  4630. const containsEdge = result.edges.find(
  4631. (e) => e.source === panel?.id && e.target === label?.id && e.kind === 'contains'
  4632. );
  4633. expect(containsEdge).toBeDefined();
  4634. });
  4635. it('should extract event handler references', () => {
  4636. const code = `object Form1: TForm1
  4637. OnCreate = FormCreate
  4638. OnDestroy = FormDestroy
  4639. object Button1: TButton
  4640. OnClick = Button1Click
  4641. end
  4642. end`;
  4643. const result = extractFromSource('Form1.dfm', code);
  4644. const refs = result.unresolvedReferences;
  4645. expect(refs.length).toBe(3);
  4646. expect(refs.map((r) => r.referenceName)).toContain('FormCreate');
  4647. expect(refs.map((r) => r.referenceName)).toContain('FormDestroy');
  4648. expect(refs.map((r) => r.referenceName)).toContain('Button1Click');
  4649. expect(refs.every((r) => r.referenceKind === 'references')).toBe(true);
  4650. });
  4651. it('should handle multi-line properties', () => {
  4652. const code = `object Form1: TForm1
  4653. SQL.Strings = (
  4654. 'SELECT * FROM users'
  4655. 'WHERE active = 1')
  4656. object Button1: TButton
  4657. OnClick = Button1Click
  4658. end
  4659. end`;
  4660. const result = extractFromSource('Form1.dfm', code);
  4661. const components = result.nodes.filter((n) => n.kind === 'component');
  4662. expect(components.length).toBe(2);
  4663. const refs = result.unresolvedReferences;
  4664. expect(refs.length).toBe(1);
  4665. expect(refs[0]?.referenceName).toBe('Button1Click');
  4666. });
  4667. it('should handle inherited keyword', () => {
  4668. const code = `inherited Form1: TForm1
  4669. Caption = 'Inherited Form'
  4670. object Button1: TButton
  4671. OnClick = Button1Click
  4672. end
  4673. end`;
  4674. const result = extractFromSource('Form1.dfm', code);
  4675. const components = result.nodes.filter((n) => n.kind === 'component');
  4676. expect(components.length).toBe(2);
  4677. expect(components.map((c) => c.name)).toContain('Form1');
  4678. });
  4679. it('should handle item collection properties', () => {
  4680. const code = `object Form1: TForm1
  4681. object StatusBar1: TStatusBar
  4682. Panels = <
  4683. item
  4684. Width = 200
  4685. end
  4686. item
  4687. Width = 200
  4688. end>
  4689. end
  4690. end`;
  4691. const result = extractFromSource('Form1.dfm', code);
  4692. const components = result.nodes.filter((n) => n.kind === 'component');
  4693. expect(components.length).toBe(2);
  4694. });
  4695. describe('Full fixture: MainForm.dfm', () => {
  4696. const code = `object frmMain: TfrmMain
  4697. Left = 0
  4698. Top = 0
  4699. Caption = 'CodeGraph DFM Fixture'
  4700. ClientHeight = 480
  4701. ClientWidth = 640
  4702. OnCreate = FormCreate
  4703. OnDestroy = FormDestroy
  4704. object pnlTop: TPanel
  4705. Left = 0
  4706. Top = 0
  4707. Width = 640
  4708. Height = 50
  4709. object lblTitle: TLabel
  4710. Left = 16
  4711. Top = 16
  4712. Caption = 'Authentication Service'
  4713. end
  4714. object btnLogin: TButton
  4715. Left = 540
  4716. Top = 12
  4717. OnClick = btnLoginClick
  4718. end
  4719. end
  4720. object pnlContent: TPanel
  4721. Left = 0
  4722. Top = 50
  4723. object edtUsername: TEdit
  4724. Left = 16
  4725. Top = 16
  4726. OnChange = edtUsernameChange
  4727. end
  4728. object edtPassword: TEdit
  4729. Left = 16
  4730. Top = 48
  4731. OnKeyPress = edtPasswordKeyPress
  4732. end
  4733. object mmoLog: TMemo
  4734. Left = 16
  4735. Top = 88
  4736. end
  4737. end
  4738. object pnlStatus: TStatusBar
  4739. Left = 0
  4740. Top = 440
  4741. Panels = <
  4742. item
  4743. Width = 200
  4744. end
  4745. item
  4746. Width = 200
  4747. end>
  4748. end
  4749. end`;
  4750. it('should extract all components', () => {
  4751. const result = extractFromSource('MainForm.dfm', code);
  4752. const components = result.nodes.filter((n) => n.kind === 'component');
  4753. expect(components.length).toBe(9);
  4754. expect(components.map((c) => c.name)).toEqual(
  4755. expect.arrayContaining([
  4756. 'frmMain', 'pnlTop', 'lblTitle', 'btnLogin',
  4757. 'pnlContent', 'edtUsername', 'edtPassword', 'mmoLog', 'pnlStatus',
  4758. ])
  4759. );
  4760. });
  4761. it('should extract all event handlers', () => {
  4762. const result = extractFromSource('MainForm.dfm', code);
  4763. const refs = result.unresolvedReferences;
  4764. expect(refs.length).toBe(5);
  4765. expect(refs.map((r) => r.referenceName)).toEqual(
  4766. expect.arrayContaining([
  4767. 'FormCreate', 'FormDestroy', 'btnLoginClick',
  4768. 'edtUsernameChange', 'edtPasswordKeyPress',
  4769. ])
  4770. );
  4771. });
  4772. });
  4773. });
  4774. describe('Kotlin Multiplatform expect/actual', () => {
  4775. let tempDir: string;
  4776. let cg: CodeGraph;
  4777. beforeEach(() => {
  4778. tempDir = createTempDir();
  4779. });
  4780. afterEach(() => {
  4781. if (cg) cg.close();
  4782. if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
  4783. });
  4784. it('links expect declarations to platform actual implementations and surfaces them in impact', async () => {
  4785. const common = path.join(tempDir, 'src', 'commonMain');
  4786. const jvm = path.join(tempDir, 'src', 'jvmMain');
  4787. fs.mkdirSync(common, { recursive: true });
  4788. fs.mkdirSync(jvm, { recursive: true });
  4789. // common source set: expect declarations + a caller that uses them
  4790. fs.writeFileSync(
  4791. path.join(common, 'SystemProps.kt'),
  4792. `package demo.internal
  4793. expect fun systemProp(name: String): String?
  4794. expect class Platform {
  4795. fun describe(): String
  4796. }
  4797. `
  4798. );
  4799. fs.writeFileSync(
  4800. path.join(common, 'Caller.kt'),
  4801. `package demo
  4802. import demo.internal.systemProp
  4803. import demo.internal.Platform
  4804. fun useIt(): String {
  4805. val v = systemProp("os.name")
  4806. return Platform().describe() + v
  4807. }
  4808. `
  4809. );
  4810. // jvm source set: actual implementations
  4811. fs.writeFileSync(
  4812. path.join(jvm, 'SystemProps.kt'),
  4813. `package demo.internal
  4814. actual fun systemProp(name: String): String? = System.getProperty(name)
  4815. actual class Platform {
  4816. actual fun describe(): String = "JVM"
  4817. }
  4818. `
  4819. );
  4820. cg = CodeGraph.initSync(tempDir);
  4821. await cg.indexAll();
  4822. cg.resolveReferences();
  4823. // The expect/actual markers are captured onto the node's decorators.
  4824. const fns = cg.getNodesByKind('function');
  4825. const actualFn = fns.find(
  4826. (n) => n.name === 'systemProp' && n.decorators?.includes('actual')
  4827. );
  4828. const expectFn = fns.find(
  4829. (n) => n.name === 'systemProp' && n.decorators?.includes('expect')
  4830. );
  4831. expect(actualFn).toBeDefined();
  4832. expect(expectFn).toBeDefined();
  4833. expect(actualFn!.filePath).not.toBe(expectFn!.filePath);
  4834. // Editing the JVM actual must surface the common expect AND its caller —
  4835. // before the expect/actual bridge the actual had zero dependents.
  4836. const impact = cg.getImpactRadius(actualFn!.id, 3);
  4837. const impacted = [...impact.nodes.values()].map((n) => n.name);
  4838. expect(impacted).toContain('systemProp'); // the common expect
  4839. expect(impacted).toContain('useIt'); // the caller, reached transitively
  4840. // The bridging edge is a heuristic `calls` edge tagged by the synthesizer.
  4841. const bridge = impact.edges.find(
  4842. (e) =>
  4843. e.target === actualFn!.id &&
  4844. e.provenance === 'heuristic' &&
  4845. (e.metadata as { synthesizedBy?: string } | undefined)?.synthesizedBy ===
  4846. 'kotlin-expect-actual'
  4847. );
  4848. expect(bridge).toBeDefined();
  4849. expect(bridge!.source).toBe(expectFn!.id);
  4850. });
  4851. it('links an expect class to an actual typealias (different node kinds)', async () => {
  4852. const common = path.join(tempDir, 'src', 'commonMain');
  4853. const jvm = path.join(tempDir, 'src', 'jvmMain');
  4854. fs.mkdirSync(common, { recursive: true });
  4855. fs.mkdirSync(jvm, { recursive: true });
  4856. fs.writeFileSync(
  4857. path.join(common, 'Lock.kt'),
  4858. `package demo
  4859. expect class Lock {
  4860. fun acquire()
  4861. }
  4862. `
  4863. );
  4864. fs.writeFileSync(
  4865. path.join(jvm, 'Lock.kt'),
  4866. `package demo
  4867. actual typealias Lock = java.util.concurrent.locks.ReentrantLock
  4868. `
  4869. );
  4870. cg = CodeGraph.initSync(tempDir);
  4871. await cg.indexAll();
  4872. cg.resolveReferences();
  4873. const aliasNode = cg
  4874. .getNodesByKind('type_alias')
  4875. .find((n) => n.name === 'Lock' && n.decorators?.includes('actual'));
  4876. expect(aliasNode).toBeDefined();
  4877. // The actual typealias is now a cross-file dependency target (linked from
  4878. // the expect class), so it participates in impact rather than being orphaned.
  4879. const impact = cg.getImpactRadius(aliasNode!.id, 3);
  4880. const bridge = impact.edges.find(
  4881. (e) =>
  4882. e.target === aliasNode!.id &&
  4883. (e.metadata as { synthesizedBy?: string } | undefined)?.synthesizedBy ===
  4884. 'kotlin-expect-actual'
  4885. );
  4886. expect(bridge).toBeDefined();
  4887. });
  4888. });
  4889. describe('Scala cross-file dependencies', () => {
  4890. let tempDir: string;
  4891. let cg: CodeGraph;
  4892. beforeEach(() => {
  4893. tempDir = createTempDir();
  4894. });
  4895. afterEach(() => {
  4896. if (cg) cg.close();
  4897. if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
  4898. });
  4899. it('links parameterized supertypes, type annotations, and implicit params across files', async () => {
  4900. const src = path.join(tempDir, 'src', 'main', 'scala', 'demo');
  4901. fs.mkdirSync(src, { recursive: true });
  4902. fs.writeFileSync(
  4903. path.join(src, 'Semigroup.scala'),
  4904. `package demo
  4905. trait Semigroup[A] {
  4906. def combine(x: A, y: A): A
  4907. }
  4908. `
  4909. );
  4910. fs.writeFileSync(
  4911. path.join(src, 'Monoid.scala'),
  4912. `package demo
  4913. trait Monoid[A] extends Semigroup[A] {
  4914. def empty: A
  4915. }
  4916. `
  4917. );
  4918. fs.writeFileSync(
  4919. path.join(src, 'Instances.scala'),
  4920. `package demo
  4921. object Instances {
  4922. implicit val intMonoid: Monoid[Int] = new Monoid[Int] {
  4923. def empty: Int = 0
  4924. def combine(x: Int, y: Int): Int = x + y
  4925. }
  4926. }
  4927. `
  4928. );
  4929. fs.writeFileSync(
  4930. path.join(src, 'Folding.scala'),
  4931. `package demo
  4932. object Folding {
  4933. def fold[A](xs: List[A])(implicit M: Monoid[A]): A =
  4934. xs.foldLeft(M.empty)(M.combine)
  4935. }
  4936. `
  4937. );
  4938. cg = CodeGraph.initSync(tempDir);
  4939. await cg.indexAll();
  4940. cg.resolveReferences();
  4941. const monoid = cg.getNodesByKind('trait').find((n) => n.name === 'Monoid');
  4942. const semigroup = cg.getNodesByKind('trait').find((n) => n.name === 'Semigroup');
  4943. expect(monoid).toBeDefined();
  4944. expect(semigroup).toBeDefined();
  4945. expect(monoid!.filePath).not.toBe(semigroup!.filePath);
  4946. // Parameterized supertype `extends Semigroup[A]` must create an extends edge —
  4947. // the whole point of the fix (the `[A]` used to defeat name matching).
  4948. const semaImpact = cg.getImpactRadius(semigroup!.id, 3);
  4949. expect([...semaImpact.nodes.values()].map((n) => n.name)).toContain('Monoid');
  4950. // Editing Monoid surfaces the cross-file users: the instance val typed
  4951. // `Monoid[Int]` and the method taking it as an implicit (curried) param.
  4952. const impacted = [...cg.getImpactRadius(monoid!.id, 3).nodes.values()].map((n) => n.name);
  4953. expect(impacted).toContain('intMonoid'); // field type annotation
  4954. expect(impacted).toContain('fold'); // trailing implicit parameter list
  4955. });
  4956. });
  4957. describe('PHP namespace + import resolution', () => {
  4958. let tempDir: string;
  4959. let cg: CodeGraph;
  4960. beforeEach(() => {
  4961. tempDir = createTempDir();
  4962. });
  4963. afterEach(() => {
  4964. if (cg) cg.close();
  4965. if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
  4966. });
  4967. it('resolves `use` imports to the namespace-qualified definition and type-hints across files', async () => {
  4968. const src = path.join(tempDir, 'src');
  4969. // Two interfaces with the SAME simple name in different namespaces — the
  4970. // exact ambiguity (Laravel has 7+ `Factory`) that bare-name matching can't
  4971. // resolve. The namespace qualifies them; the `use` import disambiguates.
  4972. fs.mkdirSync(path.join(src, 'Cache'), { recursive: true });
  4973. fs.mkdirSync(path.join(src, 'Mail'), { recursive: true });
  4974. fs.mkdirSync(path.join(src, 'App'), { recursive: true });
  4975. fs.writeFileSync(
  4976. path.join(src, 'Cache', 'Factory.php'),
  4977. `<?php
  4978. namespace Contracts\\Cache;
  4979. interface Factory {
  4980. public function store(): object;
  4981. }
  4982. `
  4983. );
  4984. fs.writeFileSync(
  4985. path.join(src, 'Mail', 'Factory.php'),
  4986. `<?php
  4987. namespace Contracts\\Mail;
  4988. interface Factory {
  4989. public function mailer(): object;
  4990. }
  4991. `
  4992. );
  4993. fs.writeFileSync(
  4994. path.join(src, 'App', 'Service.php'),
  4995. `<?php
  4996. namespace App;
  4997. use Contracts\\Cache\\Factory;
  4998. class Service {
  4999. public function make(): Factory {
  5000. return resolve(Factory::class);
  5001. }
  5002. }
  5003. `
  5004. );
  5005. cg = CodeGraph.initSync(tempDir);
  5006. await cg.indexAll();
  5007. cg.resolveReferences();
  5008. // The PHP namespace is captured into the qualified name, so the two
  5009. // same-named interfaces are distinguishable.
  5010. const cacheFactory = cg
  5011. .getNodesByKind('interface')
  5012. .find((n) => n.qualifiedName === 'Contracts\\Cache::Factory');
  5013. const mailFactory = cg
  5014. .getNodesByKind('interface')
  5015. .find((n) => n.qualifiedName === 'Contracts\\Mail::Factory');
  5016. expect(cacheFactory).toBeDefined();
  5017. expect(mailFactory).toBeDefined();
  5018. // Service `use`s Contracts\Cache\Factory, so editing THAT interface reaches
  5019. // Service.php — and editing the same-named Contracts\Mail\Factory must NOT
  5020. // (the import resolved to the right namespace, not an arbitrary `Factory`).
  5021. const serviceFile = 'src/App/Service.php';
  5022. const cacheReaches = [...cg.getImpactRadius(cacheFactory!.id, 3).nodes.values()].some(
  5023. (n) => (n.filePath ?? '').endsWith(serviceFile)
  5024. );
  5025. const mailReaches = [...cg.getImpactRadius(mailFactory!.id, 3).nodes.values()].some(
  5026. (n) => (n.filePath ?? '').endsWith(serviceFile)
  5027. );
  5028. expect(cacheReaches).toBe(true);
  5029. expect(mailReaches).toBe(false);
  5030. });
  5031. });
  5032. describe('Ruby mixins (include/extend/prepend)', () => {
  5033. let tempDir: string;
  5034. let cg: CodeGraph;
  5035. beforeEach(() => {
  5036. tempDir = createTempDir();
  5037. });
  5038. afterEach(() => {
  5039. if (cg) cg.close();
  5040. if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
  5041. });
  5042. it('links include/extend/prepend to the mixed-in module across files', async () => {
  5043. const lib = path.join(tempDir, 'lib');
  5044. fs.mkdirSync(lib, { recursive: true });
  5045. fs.writeFileSync(
  5046. path.join(lib, 'concerns.rb'),
  5047. `module Trackable
  5048. def track; end
  5049. end
  5050. module Cacheable
  5051. def cache; end
  5052. end
  5053. module Loggable
  5054. def log; end
  5055. end
  5056. `
  5057. );
  5058. fs.writeFileSync(
  5059. path.join(lib, 'model.rb'),
  5060. `class Model
  5061. include Trackable
  5062. prepend Cacheable
  5063. extend Loggable
  5064. end
  5065. `
  5066. );
  5067. cg = CodeGraph.initSync(tempDir);
  5068. await cg.indexAll();
  5069. cg.resolveReferences();
  5070. const model = cg.getNodesByKind('class').find((n) => n.name === 'Model');
  5071. expect(model).toBeDefined();
  5072. // All three mixin forms create an `implements` edge Model → module, so
  5073. // editing a concern surfaces every class that mixes it in (across files).
  5074. for (const moduleName of ['Trackable', 'Cacheable', 'Loggable']) {
  5075. const mod = cg.getNodesByKind('module').find((n) => n.name === moduleName);
  5076. expect(mod, moduleName).toBeDefined();
  5077. const impacted = [...cg.getImpactRadius(mod!.id, 3).nodes.values()].map((n) => n.name);
  5078. expect(impacted, `${moduleName} should be depended on by Model`).toContain('Model');
  5079. }
  5080. });
  5081. it('resolves require / require_relative to the required file', async () => {
  5082. const lib = path.join(tempDir, 'lib');
  5083. fs.mkdirSync(path.join(lib, 'app'), { recursive: true });
  5084. // A leaf file whose class is referenced only dynamically — so without
  5085. // require resolution it would look like nothing depends on it.
  5086. fs.writeFileSync(
  5087. path.join(lib, 'app', 'fetcher.rb'),
  5088. `module App
  5089. class Fetcher
  5090. def fetch; end
  5091. end
  5092. end
  5093. `
  5094. );
  5095. // Pulled in by a load-path `require` …
  5096. fs.writeFileSync(
  5097. path.join(lib, 'app', 'worker.rb'),
  5098. `require "app/fetcher"
  5099. module App
  5100. class Worker; end
  5101. end
  5102. `
  5103. );
  5104. // … and a sibling pulled in by `require_relative`.
  5105. fs.writeFileSync(
  5106. path.join(lib, 'app', 'boot.rb'),
  5107. `require_relative "fetcher"
  5108. `
  5109. );
  5110. cg = CodeGraph.initSync(tempDir);
  5111. await cg.indexAll();
  5112. cg.resolveReferences();
  5113. // The require edges target fetcher.rb's FILE node. Editing it should reach
  5114. // BOTH the load-path requirer (worker.rb) and the require_relative one
  5115. // (boot.rb) — without require resolution its file would have no dependents.
  5116. const fetcher = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('app/fetcher.rb'));
  5117. expect(fetcher, 'fetcher.rb indexed').toBeDefined();
  5118. const reached = [...cg.getImpactRadius(fetcher!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  5119. expect(reached.some((p) => p.endsWith('app/worker.rb'))).toBe(true);
  5120. expect(reached.some((p) => p.endsWith('app/boot.rb'))).toBe(true);
  5121. });
  5122. });
  5123. describe('C++ pure-virtual method nodes (#1727)', () => {
  5124. // Pure-virtual methods are field_declarations (`virtual int read(int key) = 0;`),
  5125. // not function_definitions — they previously minted no method node, so calls
  5126. // through an abstract base and cpp-override synthesis had nothing to attach to.
  5127. // Java interface methods already get nodes; C++ should behave similarly.
  5128. it('indexes Store::read from the issue fixture and records the call', () => {
  5129. const code = `
  5130. class Store {
  5131. public:
  5132. virtual ~Store() {}
  5133. virtual int read(int key) = 0;
  5134. };
  5135. class DiskStore : public Store {
  5136. public:
  5137. int read(int key) override { return key + 1; }
  5138. };
  5139. class MemStore : public Store {
  5140. public:
  5141. int read(int key) override { return key + 2; }
  5142. };
  5143. int fetch(Store* s, int k) {
  5144. return s->read(k);
  5145. }
  5146. `;
  5147. const result = extractFromSource('store.cc', code);
  5148. const methods = result.nodes.filter((n) => n.kind === 'method').map((n) => n.qualifiedName);
  5149. expect(methods).toContain('Store::read');
  5150. expect(methods).toContain('DiskStore::read');
  5151. expect(methods).toContain('MemStore::read');
  5152. const baseRead = result.nodes.find((n) => n.qualifiedName === 'Store::read');
  5153. expect(baseRead?.isAbstract).toBe(true);
  5154. // Call site unresolved ref targets the method name (resolver types the receiver).
  5155. expect(
  5156. result.unresolvedReferences.some(
  5157. (r) => r.referenceKind === 'calls' && (r.referenceName === 'read' || r.referenceName.endsWith('.read') || r.referenceName.endsWith('->read') || r.referenceName === 's.read')
  5158. )
  5159. ).toBe(true);
  5160. });
  5161. it('indexes pure virtuals with pointer/reference return types and operators', () => {
  5162. const code = `
  5163. class Cloneable {
  5164. public:
  5165. virtual Cloneable* clone() = 0;
  5166. virtual const Foo& get() = 0;
  5167. virtual Cloneable& operator=(const Cloneable&) = 0;
  5168. int notPure(int x);
  5169. int data = 0;
  5170. };
  5171. `;
  5172. const result = extractFromSource('clone.hpp', code);
  5173. const methods = result.nodes.filter((n) => n.kind === 'method').map((n) => n.name);
  5174. expect(methods).toContain('clone');
  5175. expect(methods).toContain('get');
  5176. expect(methods).toContain('operator=');
  5177. // Non-pure prototype and data member must NOT become methods here.
  5178. expect(methods).not.toContain('notPure');
  5179. expect(methods).not.toContain('data');
  5180. expect(result.nodes.find((n) => n.name === 'clone')?.isAbstract).toBe(true);
  5181. });
  5182. });
  5183. describe('C++ free-function name extraction', () => {
  5184. let tempDir: string;
  5185. let cg: CodeGraph;
  5186. beforeEach(() => {
  5187. tempDir = createTempDir();
  5188. });
  5189. afterEach(() => {
  5190. if (cg) cg.close();
  5191. if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
  5192. });
  5193. it('names a free function correctly when it has qualified-type params or a trailing return type', async () => {
  5194. const src = path.join(tempDir, 'src');
  5195. fs.mkdirSync(src, { recursive: true });
  5196. // TableFileName has a `const std::string&` parameter; BuildName uses an
  5197. // `auto … -> std::string` trailing return type. Both used to be named
  5198. // `string` (picked up from the parameter / return type), so callers never
  5199. // resolved and the defining file looked like nothing depended on it.
  5200. fs.writeFileSync(
  5201. path.join(src, 'names.cc'),
  5202. `#include <string>
  5203. std::string TableFileName(const std::string& dbname, int number) {
  5204. return dbname;
  5205. }
  5206. auto BuildName(const std::string& a) -> std::string {
  5207. return a;
  5208. }
  5209. `
  5210. );
  5211. fs.writeFileSync(
  5212. path.join(src, 'user.cc'),
  5213. `#include <string>
  5214. std::string use() {
  5215. return TableFileName("db", 1) + BuildName("x");
  5216. }
  5217. `
  5218. );
  5219. cg = CodeGraph.initSync(tempDir);
  5220. await cg.indexAll();
  5221. cg.resolveReferences();
  5222. // The functions are extracted under their real names, not `string`.
  5223. const fns = cg.getNodesByKind('function');
  5224. const tableFn = fns.find((n) => n.name === 'TableFileName');
  5225. const buildFn = fns.find((n) => n.name === 'BuildName');
  5226. expect(tableFn, 'TableFileName extracted (not "string")').toBeDefined();
  5227. expect(buildFn, 'BuildName extracted (not "string")').toBeDefined();
  5228. // And the cross-file calls resolve to them, so editing names.cc surfaces user.cc.
  5229. for (const fn of [tableFn!, buildFn!]) {
  5230. const reached = [...cg.getImpactRadius(fn.id, 3).nodes.values()].map((n) => n.filePath ?? '');
  5231. expect(reached.some((p) => p.endsWith('user.cc')), `${fn.name} should be called from user.cc`).toBe(true);
  5232. }
  5233. });
  5234. });
  5235. describe('C/C++ union declarations', () => {
  5236. it('extracts a named union as a type node, but not a forward declaration', () => {
  5237. const code = `
  5238. union packet_hdr {
  5239. unsigned int raw;
  5240. unsigned short port;
  5241. };
  5242. /* forward declaration — not a definition */
  5243. union opaque_hdr;
  5244. static unsigned int hdr_raw(union packet_hdr *h) { return h->raw; }
  5245. `;
  5246. const result = extractFromSource('packet.c', code);
  5247. const hdr = result.nodes.find((n) => n.name === 'packet_hdr');
  5248. expect(hdr).toBeDefined();
  5249. expect(hdr?.kind).toBe('union');
  5250. // Same rule as `struct Foo;`: bodiless is a forward declaration, so it must
  5251. // not mint a phantom node beside the real definition.
  5252. expect(result.nodes.some((n) => n.name === 'opaque_hdr')).toBe(false);
  5253. // Exactly one node for the type — the definition — so a call site or a
  5254. // `union packet_hdr *` parameter has a single resolution target.
  5255. expect(result.nodes.filter((n) => n.name === 'packet_hdr')).toHaveLength(1);
  5256. });
  5257. it('gives a typedef union the typedef name, not a second <anonymous> node', () => {
  5258. const code = `
  5259. typedef union {
  5260. unsigned int u;
  5261. float f;
  5262. } word_t;
  5263. `;
  5264. const result = extractFromSource('word.c', code);
  5265. const word = result.nodes.find((n) => n.name === 'word_t');
  5266. expect(word?.kind).toBe('union');
  5267. // Resolved through the typedef the same way `typedef struct { … } X;` is,
  5268. // so the anonymous union body does not become its own node.
  5269. expect(result.nodes.some((n) => n.name === '<anonymous>')).toBe(false);
  5270. });
  5271. it('extracts a C++ union with member functions', () => {
  5272. const code = `
  5273. union Value {
  5274. int i;
  5275. double d;
  5276. int as_int() const { return i; }
  5277. };
  5278. `;
  5279. const result = extractFromSource('value.cpp', code);
  5280. const value = result.nodes.find((n) => n.name === 'Value');
  5281. expect(value?.kind).toBe('union');
  5282. const asInt = result.nodes.find((n) => n.name === 'as_int');
  5283. expect(asInt).toBeDefined();
  5284. expect(
  5285. result.edges.some((e) => e.kind === 'contains' && e.source === value?.id && e.target === asInt?.id)
  5286. ).toBe(true);
  5287. });
  5288. });
  5289. describe('Dart mixins and type references', () => {
  5290. let tempDir: string;
  5291. let cg: CodeGraph;
  5292. beforeEach(() => {
  5293. tempDir = createTempDir();
  5294. });
  5295. afterEach(() => {
  5296. if (cg) cg.close();
  5297. if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
  5298. });
  5299. it('links `with` mixins and method parameter/return types across files', async () => {
  5300. const lib = path.join(tempDir, 'lib');
  5301. fs.mkdirSync(lib, { recursive: true });
  5302. fs.writeFileSync(
  5303. path.join(lib, 'models.dart'),
  5304. `class User {
  5305. final String name;
  5306. User(this.name);
  5307. }
  5308. mixin Loggable {
  5309. void log() {}
  5310. }
  5311. abstract class Repository {
  5312. User find(int id);
  5313. }
  5314. `
  5315. );
  5316. fs.writeFileSync(
  5317. path.join(lib, 'service.dart'),
  5318. `import 'models.dart';
  5319. class UserService extends Repository with Loggable {
  5320. @override
  5321. User find(int id) => User('x');
  5322. List<User> all() => [];
  5323. }
  5324. `
  5325. );
  5326. cg = CodeGraph.initSync(tempDir);
  5327. await cg.indexAll();
  5328. cg.resolveReferences();
  5329. const inModels = (name: string) =>
  5330. cg.getNodesByKind('class').concat(cg.getNodesByKind('module'))
  5331. .find((n) => n.name === name && n.filePath.endsWith('models.dart'));
  5332. // The `with Loggable` mixin records a dependency — editing the mixin surfaces
  5333. // the class that mixes it in (across files). Loggable is a `mixin`, indexed
  5334. // as a class-like node.
  5335. const loggable = cg.getNodesByKind('class').find((n) => n.name === 'Loggable')
  5336. ?? cg.getNodesByKind('module').find((n) => n.name === 'Loggable');
  5337. expect(loggable, 'Loggable mixin indexed').toBeDefined();
  5338. const mixinUsers = [...cg.getImpactRadius(loggable!.id, 3).nodes.values()].map((n) => n.name);
  5339. expect(mixinUsers).toContain('UserService');
  5340. // `User` is used only as a method parameter/return type in service.dart —
  5341. // editing it must still surface service.dart via the type references.
  5342. const user = inModels('User') ?? cg.getNodesByKind('class').find((n) => n.name === 'User');
  5343. expect(user, 'User indexed').toBeDefined();
  5344. const userDeps = [...cg.getImpactRadius(user!.id, 3).nodes.values()].map((n) => n.filePath ?? '');
  5345. expect(userDeps.some((p) => p.endsWith('service.dart'))).toBe(true);
  5346. });
  5347. });
  5348. describe('Static-member / value-read references', () => {
  5349. let tempDir: string;
  5350. let cg: CodeGraph;
  5351. beforeEach(() => {
  5352. tempDir = createTempDir();
  5353. });
  5354. afterEach(() => {
  5355. if (cg) cg.close();
  5356. if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
  5357. });
  5358. it('links a type referenced only via a static field / enum value (and ignores lowercase receivers)', async () => {
  5359. fs.writeFileSync(
  5360. path.join(tempDir, 'JsonScope.java'),
  5361. `class JsonScope {
  5362. static final int EMPTY_DOCUMENT = 1;
  5363. }
  5364. `
  5365. );
  5366. fs.writeFileSync(
  5367. path.join(tempDir, 'Reader.java'),
  5368. `class Reader {
  5369. private int helper;
  5370. int peek() {
  5371. return JsonScope.EMPTY_DOCUMENT;
  5372. }
  5373. int noop() {
  5374. return this.helper;
  5375. }
  5376. }
  5377. `
  5378. );
  5379. cg = CodeGraph.initSync(tempDir);
  5380. await cg.indexAll();
  5381. cg.resolveReferences();
  5382. // JsonScope is used ONLY as `JsonScope.EMPTY_DOCUMENT` (a static-field value
  5383. // read — never constructed or called), so before the static-member pass it
  5384. // had no dependents. Editing it now surfaces Reader.java.
  5385. const scope = cg.getNodesByKind('class').find((n) => n.name === 'JsonScope');
  5386. expect(scope, 'JsonScope indexed').toBeDefined();
  5387. const reached = [...cg.getImpactRadius(scope!.id, 3).nodes.values()].map((n) => n.filePath ?? '');
  5388. expect(reached.some((p) => p.endsWith('Reader.java'))).toBe(true);
  5389. // A lowercase receiver (`this.helper`) must NOT be emitted as a type ref —
  5390. // only Capitalized receivers (types) are. No node named `this`/`helper`
  5391. // should appear as a reference target from peek/noop beyond JsonScope.
  5392. const refTargets = cg
  5393. .getNodesByKind('class')
  5394. .filter((n) => n.name === 'this' || n.name === 'helper');
  5395. expect(refTargets.length).toBe(0);
  5396. });
  5397. it('does not link a static-member read across language families (coincidental name)', async () => {
  5398. // A native (Kotlin) `Build.VERSION` reads the Android system class — it must
  5399. // NOT link to a coincidentally same-named TS class (the cross-language false
  5400. // positive that name-matching produces; `references` edges are language-local).
  5401. fs.writeFileSync(
  5402. path.join(tempDir, 'Build.ts'),
  5403. `export class Build {\n static version = 1;\n}\n`
  5404. );
  5405. fs.writeFileSync(
  5406. path.join(tempDir, 'Device.kt'),
  5407. `package app\nclass Device {\n fun sdk(): Int = Build.VERSION\n}\n`
  5408. );
  5409. cg = CodeGraph.initSync(tempDir);
  5410. await cg.indexAll();
  5411. cg.resolveReferences();
  5412. const tsBuild = cg.getNodesByKind('class').find((n) => n.name === 'Build' && n.filePath.endsWith('Build.ts'));
  5413. expect(tsBuild).toBeDefined();
  5414. // The Kotlin file is `app/Device.kt`; the TS Build must have NO dependent there.
  5415. const deps = [...cg.getImpactRadius(tsBuild!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  5416. expect(deps.some((p) => p.endsWith('Device.kt'))).toBe(false);
  5417. });
  5418. });
  5419. describe('Cross-language type/import gate (RN name collisions)', () => {
  5420. let tempDir: string;
  5421. let cg: CodeGraph;
  5422. beforeEach(() => {
  5423. tempDir = createTempDir();
  5424. });
  5425. afterEach(() => {
  5426. if (cg) cg.close();
  5427. if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
  5428. });
  5429. it('a TS PascalCase type ref lands on the TS type, never a same-named native class', async () => {
  5430. // react-native-async-storage's example app has a TS `type TestRunner` AND a
  5431. // Kotlin `class TestRunner`. The React PascalCase resolver name-matched the
  5432. // Kotlin `class` (its COMPONENT_KINDS includes `class`) with no language
  5433. // check at confidence 0.8, outranking the (cross-language-penalized 0.5)
  5434. // TS name-match — so a TS ref to `TestRunner` crossed web→jvm. The ref here
  5435. // is intentionally NOT imported: a clean relative import would mask the bug
  5436. // by resolving via the import map before the framework strategy can win.
  5437. fs.writeFileSync(
  5438. path.join(tempDir, 'package.json'),
  5439. JSON.stringify({ dependencies: { 'react-native': '*' } })
  5440. );
  5441. fs.writeFileSync(
  5442. path.join(tempDir, 'useTests.ts'),
  5443. `export type TestRunner = { run: () => void };\n`
  5444. );
  5445. fs.writeFileSync(
  5446. path.join(tempDir, 'basic.tsx'),
  5447. `export function useBasicTest(r: TestRunner): TestRunner {\n return r;\n}\n`
  5448. );
  5449. fs.writeFileSync(
  5450. path.join(tempDir, 'TestUtils.kt'),
  5451. `package app\nclass TestRunner {\n fun run() {}\n}\n`
  5452. );
  5453. cg = CodeGraph.initSync(tempDir);
  5454. await cg.indexAll();
  5455. cg.resolveReferences();
  5456. const ktRunner = cg
  5457. .getNodesByKind('class')
  5458. .find((n) => n.name === 'TestRunner' && n.filePath.endsWith('TestUtils.kt'));
  5459. expect(ktRunner, 'Kotlin TestRunner class').toBeDefined();
  5460. const ktDeps = [...cg.getImpactRadius(ktRunner!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  5461. expect(ktDeps.some((p) => p.endsWith('basic.tsx')), 'Kotlin class has NO TS dependent').toBe(false);
  5462. const tsRunner = cg.getNodesByKind('type_alias').find((n) => n.name === 'TestRunner');
  5463. expect(tsRunner, 'TS TestRunner type_alias').toBeDefined();
  5464. const tsDeps = [...cg.getImpactRadius(tsRunner!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  5465. expect(tsDeps.some((p) => p.endsWith('basic.tsx')), 'TS type captured the ref (re-pointed)').toBe(true);
  5466. });
  5467. it('gates a cross-family import name collision but keeps same-family imports', async () => {
  5468. // A TS `import { Widget }` that only matches a Swift `class Widget` must not
  5469. // create a web→apple dependency — but a sibling TS module imported by
  5470. // another TS file (same family) must still resolve (no over-gating).
  5471. fs.writeFileSync(path.join(tempDir, 'Widget.swift'), `class Widget {\n func render() {}\n}\n`);
  5472. fs.writeFileSync(
  5473. path.join(tempDir, 'widget.ts'),
  5474. `import { Widget } from './native';\nexport function mount(w: Widget) {}\n`
  5475. );
  5476. fs.writeFileSync(path.join(tempDir, 'util.ts'), `export class Helper {}\n`);
  5477. fs.writeFileSync(
  5478. path.join(tempDir, 'app.ts'),
  5479. `import { Helper } from './util';\nexport const h = new Helper();\n`
  5480. );
  5481. cg = CodeGraph.initSync(tempDir);
  5482. await cg.indexAll();
  5483. cg.resolveReferences();
  5484. const swiftWidget = cg
  5485. .getNodesByKind('class')
  5486. .find((n) => n.name === 'Widget' && n.filePath.endsWith('.swift'));
  5487. expect(swiftWidget, 'Swift Widget class').toBeDefined();
  5488. const wDeps = [...cg.getImpactRadius(swiftWidget!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  5489. expect(wDeps.some((p) => p.endsWith('widget.ts')), 'Swift class has NO TS dependent').toBe(false);
  5490. // Same-family control — the TS Helper must still see its TS dependent.
  5491. const helper = cg.getNodesByKind('class').find((n) => n.name === 'Helper');
  5492. expect(helper, 'TS Helper class').toBeDefined();
  5493. const hDeps = [...cg.getImpactRadius(helper!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  5494. expect(hDeps.some((p) => p.endsWith('app.ts')), 'same-family TS import preserved').toBe(true);
  5495. });
  5496. });
  5497. describe('Python absolute module import resolution', () => {
  5498. let tempDir: string;
  5499. let cg: CodeGraph;
  5500. beforeEach(() => {
  5501. tempDir = createTempDir();
  5502. });
  5503. afterEach(() => {
  5504. if (cg) cg.close();
  5505. if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
  5506. });
  5507. it('links a bare `import pkg.module` of an internal module to its file', async () => {
  5508. // `import conduit.apps.signals` (a Django-style side-effect import, and any
  5509. // dotted absolute module import) had no edge to the module file — only
  5510. // `from x import y` was linked — so a module imported by its dotted path
  5511. // looked like nothing depended on it.
  5512. fs.mkdirSync(path.join(tempDir, 'conduit/apps'), { recursive: true });
  5513. fs.writeFileSync(path.join(tempDir, 'conduit/__init__.py'), '');
  5514. fs.writeFileSync(path.join(tempDir, 'conduit/apps/__init__.py'), '');
  5515. fs.writeFileSync(path.join(tempDir, 'conduit/apps/signals.py'), `def handler():\n pass\n`);
  5516. fs.writeFileSync(
  5517. path.join(tempDir, 'conduit/apps/app.py'),
  5518. `import conduit.apps.signals\nimport os\n\nVALUE = 1\n`
  5519. );
  5520. cg = CodeGraph.initSync(tempDir);
  5521. await cg.indexAll();
  5522. cg.resolveReferences();
  5523. const signals = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('conduit/apps/signals.py'));
  5524. expect(signals, 'signals.py indexed').toBeDefined();
  5525. const deps = [...cg.getImpactRadius(signals!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  5526. expect(deps.some((p) => p.endsWith('app.py')), 'importer depends on the module').toBe(true);
  5527. // `import os` (stdlib) must NOT fabricate an edge — no os.py file in the repo.
  5528. const osNode = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('/os.py'));
  5529. expect(osNode, 'no stdlib os.py node').toBeUndefined();
  5530. });
  5531. it('Django include() links the root URLconf to the included app urls module', async () => {
  5532. // `url(r'^api/', include('app.urls'))` should record a dependency from the
  5533. // root urlconf onto the included app's `urls.py` — so editing an app's routes
  5534. // surfaces the project urlconf that mounts them.
  5535. fs.mkdirSync(path.join(tempDir, 'app'), { recursive: true });
  5536. fs.writeFileSync(path.join(tempDir, 'requirements.txt'), `django==4.0\n`);
  5537. fs.writeFileSync(path.join(tempDir, 'app/__init__.py'), '');
  5538. fs.writeFileSync(path.join(tempDir, 'app/views.py'), `def home(request):\n return None\n`);
  5539. fs.writeFileSync(
  5540. path.join(tempDir, 'app/urls.py'),
  5541. `from django.conf.urls import url\nfrom . import views\nurlpatterns = [url(r'^$', views.home)]\n`
  5542. );
  5543. fs.writeFileSync(
  5544. path.join(tempDir, 'urls.py'),
  5545. `from django.conf.urls import include, url\nurlpatterns = [url(r'^app/', include('app.urls'))]\n`
  5546. );
  5547. cg = CodeGraph.initSync(tempDir);
  5548. await cg.indexAll();
  5549. cg.resolveReferences();
  5550. const appUrls = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('app/urls.py'));
  5551. expect(appUrls, 'app/urls.py indexed').toBeDefined();
  5552. const deps = [...cg.getImpactRadius(appUrls!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  5553. expect(deps.some((p) => p.endsWith('urls.py') && !p.endsWith('app/urls.py')), 'root urlconf depends on the included app urls').toBe(true);
  5554. });
  5555. it('resolves `from pkg import submodule` to the submodule under that package, not a same-named one', async () => {
  5556. // FastAPI router-aggregator pattern: `from app.api.routes import authentication`
  5557. // with same-named modules in sibling packages must resolve via the import's
  5558. // SOURCE (the package), not a coincidental same-basename file elsewhere.
  5559. fs.mkdirSync(path.join(tempDir, 'app/api/routes'), { recursive: true });
  5560. fs.mkdirSync(path.join(tempDir, 'app/api/dependencies'), { recursive: true });
  5561. for (const p of ['app/__init__.py', 'app/api/__init__.py', 'app/api/routes/__init__.py', 'app/api/dependencies/__init__.py']) {
  5562. fs.writeFileSync(path.join(tempDir, p), '');
  5563. }
  5564. fs.writeFileSync(path.join(tempDir, 'app/api/routes/authentication.py'), `def login():\n pass\n`);
  5565. fs.writeFileSync(path.join(tempDir, 'app/api/dependencies/authentication.py'), `def get_user():\n pass\n`);
  5566. fs.writeFileSync(
  5567. path.join(tempDir, 'app/api/routes/api.py'),
  5568. `from app.api.routes import authentication\n\nROUTER = authentication\n`
  5569. );
  5570. cg = CodeGraph.initSync(tempDir);
  5571. await cg.indexAll();
  5572. cg.resolveReferences();
  5573. const routesAuth = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('routes/authentication.py'));
  5574. const depsAuth = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('dependencies/authentication.py'));
  5575. expect(routesAuth && depsAuth).toBeTruthy();
  5576. const routesDeps = [...cg.getImpactRadius(routesAuth!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  5577. const depsDeps = [...cg.getImpactRadius(depsAuth!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  5578. expect(routesDeps.some((p) => p.endsWith('routes/api.py')), 'submodule under the imported package is the dependent').toBe(true);
  5579. expect(depsDeps.some((p) => p.endsWith('routes/api.py')), 'same-named module in a sibling package is NOT').toBe(false);
  5580. });
  5581. });
  5582. describe('Razor / Blazor markup extraction', () => {
  5583. let tempDir: string;
  5584. let cg: CodeGraph;
  5585. beforeEach(() => {
  5586. tempDir = createTempDir();
  5587. });
  5588. afterEach(() => {
  5589. if (cg) cg.close();
  5590. if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
  5591. });
  5592. it('links @model and Blazor component tags to their C# types; ignores HTML elements', async () => {
  5593. fs.mkdirSync(path.join(tempDir, 'Views'), { recursive: true });
  5594. fs.writeFileSync(
  5595. path.join(tempDir, 'LoginViewModel.cs'),
  5596. `namespace App { public class LoginViewModel { public string Email { get; set; } } }`
  5597. );
  5598. fs.writeFileSync(
  5599. path.join(tempDir, 'ToastComponent.cs'),
  5600. `namespace App { public class ToastComponent { } }`
  5601. );
  5602. fs.writeFileSync(
  5603. path.join(tempDir, 'Views/Login.cshtml'),
  5604. `@model LoginViewModel\n<div class="form">\n <input asp-for="Email" />\n</div>\n`
  5605. );
  5606. fs.writeFileSync(
  5607. path.join(tempDir, 'Index.razor'),
  5608. `<div>\n <ToastComponent />\n</div>\n`
  5609. );
  5610. cg = CodeGraph.initSync(tempDir);
  5611. await cg.indexAll();
  5612. cg.resolveReferences();
  5613. // `@model LoginViewModel` → the view-model class.
  5614. const vm = cg.getNodesByKind('class').find((n) => n.name === 'LoginViewModel');
  5615. expect(vm, 'LoginViewModel class').toBeDefined();
  5616. const vmDeps = [...cg.getImpactRadius(vm!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  5617. expect(vmDeps.some((p) => p.endsWith('Login.cshtml')), '@model links the view').toBe(true);
  5618. // `<ToastComponent />` → the component class.
  5619. const toast = cg.getNodesByKind('class').find((n) => n.name === 'ToastComponent');
  5620. expect(toast, 'ToastComponent class').toBeDefined();
  5621. const toastDeps = [...cg.getImpactRadius(toast!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  5622. expect(toastDeps.some((p) => p.endsWith('Index.razor')), 'Blazor tag links the component').toBe(true);
  5623. // HTML elements (`<div>`, `<input>`) must NOT become component references.
  5624. const htmlNodes = cg.getNodesByKind('class').filter((n) => n.name === 'div' || n.name === 'input');
  5625. expect(htmlNodes.length, 'no node for HTML elements').toBe(0);
  5626. });
  5627. it('C# namespaces qualify type names so same-named types are distinct', async () => {
  5628. fs.writeFileSync(path.join(tempDir, 'entity.cs'), `namespace App.Entities { public class CatalogBrand { } }`);
  5629. fs.writeFileSync(path.join(tempDir, 'dto.cs'), `namespace App.Models { public class CatalogBrand { } }`);
  5630. cg = CodeGraph.initSync(tempDir);
  5631. await cg.indexAll();
  5632. const brands = cg.getNodesByKind('class').filter((n) => n.name === 'CatalogBrand');
  5633. expect(brands.length, 'both CatalogBrand classes indexed').toBe(2);
  5634. const qns = brands.map((b) => b.qualifiedName).sort();
  5635. expect(qns[0]).not.toBe(qns[1]); // distinct qualified names (namespace-scoped)
  5636. expect(qns.some((q) => q.includes('Entities') && q.endsWith('CatalogBrand'))).toBe(true);
  5637. expect(qns.some((q) => q.includes('Models') && q.endsWith('CatalogBrand'))).toBe(true);
  5638. });
  5639. it('disambiguates a Razor type ref via @using (incl. folder _Imports.razor)', async () => {
  5640. // `CatalogBrand` exists as both a domain entity and a DTO; the component
  5641. // `@using`s the DTO's namespace (here via the folder _Imports.razor), so the
  5642. // ref must resolve to the DTO, not the same-named entity.
  5643. fs.mkdirSync(path.join(tempDir, 'Models'), { recursive: true });
  5644. fs.mkdirSync(path.join(tempDir, 'Entities'), { recursive: true });
  5645. fs.mkdirSync(path.join(tempDir, 'Pages'), { recursive: true });
  5646. fs.writeFileSync(path.join(tempDir, 'Models/CatalogBrand.cs'), `namespace App.Models { public class CatalogBrand { public int Id { get; set; } } }`);
  5647. fs.writeFileSync(path.join(tempDir, 'Entities/CatalogBrand.cs'), `namespace App.Entities { public class CatalogBrand { public int Id { get; set; } } }`);
  5648. fs.writeFileSync(path.join(tempDir, 'Pages/_Imports.razor'), `@using App.Models\n`);
  5649. fs.writeFileSync(
  5650. path.join(tempDir, 'Pages/List.razor'),
  5651. `<h1>List</h1>\n@code {\n private CatalogBrand _b = new CatalogBrand();\n}\n`
  5652. );
  5653. cg = CodeGraph.initSync(tempDir);
  5654. await cg.indexAll();
  5655. cg.resolveReferences();
  5656. const dto = cg.getNodesByKind('class').find((n) => n.qualifiedName === 'App.Models::CatalogBrand');
  5657. const entity = cg.getNodesByKind('class').find((n) => n.qualifiedName === 'App.Entities::CatalogBrand');
  5658. expect(dto && entity, 'both CatalogBrand classes').toBeTruthy();
  5659. const dtoDeps = [...cg.getImpactRadius(dto!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  5660. const entityDeps = [...cg.getImpactRadius(entity!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  5661. expect(dtoDeps.some((p) => p.endsWith('List.razor')), 'resolves to the @using\'d DTO').toBe(true);
  5662. expect(entityDeps.some((p) => p.endsWith('List.razor')), 'NOT the same-named entity').toBe(false);
  5663. });
  5664. it('delegates Blazor @code block C# to cover types used in component logic', async () => {
  5665. fs.writeFileSync(
  5666. path.join(tempDir, 'CatalogService.cs'),
  5667. `namespace App { public class CatalogService { public void Load() { } } }`
  5668. );
  5669. fs.writeFileSync(
  5670. path.join(tempDir, 'List.razor'),
  5671. `<h1>Catalog</h1>\n\n@code {\n private CatalogService _svc = new CatalogService();\n void Refresh() { _svc.Load(); }\n}\n`
  5672. );
  5673. cg = CodeGraph.initSync(tempDir);
  5674. await cg.indexAll();
  5675. cg.resolveReferences();
  5676. const svc = cg.getNodesByKind('class').find((n) => n.name === 'CatalogService');
  5677. expect(svc, 'CatalogService class').toBeDefined();
  5678. const deps = [...cg.getImpactRadius(svc!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  5679. expect(deps.some((p) => p.endsWith('List.razor')), '@code usage links the component to the service').toBe(true);
  5680. });
  5681. });
  5682. describe('Default import resolution (renamed default export)', () => {
  5683. let tempDir: string;
  5684. let cg: CodeGraph;
  5685. beforeEach(() => {
  5686. tempDir = createTempDir();
  5687. });
  5688. afterEach(() => {
  5689. if (cg) cg.close();
  5690. if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
  5691. });
  5692. it('links a renamed default import to the module file', async () => {
  5693. // Express route aggregator: `import articlesController from './controller'`
  5694. // where the module does `export default router`. The renamed local can't be
  5695. // found as a symbol, so the controller file had no dependent — the dependency
  5696. // is on the module file regardless of the default export's name.
  5697. fs.mkdirSync(path.join(tempDir, 'app'), { recursive: true });
  5698. fs.writeFileSync(path.join(tempDir, 'app/controller.ts'), `const router = { get() {} };\nexport default router;\n`);
  5699. fs.writeFileSync(path.join(tempDir, 'app/routes.ts'), `import myController from './controller';\nexport const api = myController;\n`);
  5700. cg = CodeGraph.initSync(tempDir);
  5701. await cg.indexAll();
  5702. cg.resolveReferences();
  5703. const controller = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('app/controller.ts'));
  5704. expect(controller, 'controller.ts indexed').toBeDefined();
  5705. const deps = [...cg.getImpactRadius(controller!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  5706. expect(deps.some((p) => p.endsWith('routes.ts')), 'importer depends on the default-exporting module').toBe(true);
  5707. });
  5708. });
  5709. describe('Chained method-call resolution (C# extension methods)', () => {
  5710. let tempDir: string;
  5711. let cg: CodeGraph;
  5712. beforeEach(() => {
  5713. tempDir = createTempDir();
  5714. });
  5715. afterEach(() => {
  5716. if (cg) cg.close();
  5717. if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
  5718. });
  5719. it('resolves a chained extension-method call (a.b.Method()) to its definition', async () => {
  5720. // ASP.NET DI registration: `builder.Services.AddCoreServices(...)` calls a
  5721. // static extension method elsewhere. A multi-dot receiver chain matched no
  5722. // method-call pattern before, so the extension method had no caller.
  5723. fs.mkdirSync(path.join(tempDir, 'cfg'), { recursive: true });
  5724. fs.writeFileSync(
  5725. path.join(tempDir, 'cfg/Ext.cs'),
  5726. `namespace App {\n public static class Ext {\n public static object AddCoreServices(this object services, int x) { return services; }\n }\n}\n`
  5727. );
  5728. fs.writeFileSync(
  5729. path.join(tempDir, 'Program.cs'),
  5730. `namespace App {\n public class Program {\n public void Run(object builder) {\n builder.Services.AddCoreServices(1);\n }\n }\n}\n`
  5731. );
  5732. cg = CodeGraph.initSync(tempDir);
  5733. await cg.indexAll();
  5734. cg.resolveReferences();
  5735. const ext = cg
  5736. .getNodesByKind('method')
  5737. .find((n) => n.name === 'AddCoreServices')
  5738. ?? cg.getNodesByKind('function').find((n) => n.name === 'AddCoreServices');
  5739. expect(ext, 'AddCoreServices defined').toBeDefined();
  5740. const callers = [...cg.getImpactRadius(ext!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  5741. expect(callers.some((p) => p.endsWith('Program.cs')), 'chained extension call resolves to its definition').toBe(true);
  5742. });
  5743. });
  5744. describe('Same-directory include + KMP import resolution', () => {
  5745. let tempDir: string;
  5746. let cg: CodeGraph;
  5747. beforeEach(() => {
  5748. tempDir = createTempDir();
  5749. });
  5750. afterEach(() => {
  5751. if (cg) cg.close();
  5752. if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
  5753. });
  5754. it('a C/C++ #include resolves to the same-directory header, not a same-named one elsewhere', async () => {
  5755. // A multi-platform native module has a header of the same basename per
  5756. // platform. `windows/Provider.cpp`'s `#include "Storage.h"` means its OWN
  5757. // sibling header — not `apple/Storage.h` (which sorts first and so was
  5758. // picked arbitrarily before, leaving the real local header with 0 deps).
  5759. fs.mkdirSync(path.join(tempDir, 'apple'), { recursive: true });
  5760. fs.mkdirSync(path.join(tempDir, 'windows'), { recursive: true });
  5761. fs.writeFileSync(path.join(tempDir, 'apple', 'Storage.h'), `#pragma once\nstruct Storage { int n; };\n`);
  5762. fs.writeFileSync(path.join(tempDir, 'windows', 'Storage.h'), `#pragma once\nstruct Storage { int n; };\n`);
  5763. fs.writeFileSync(
  5764. path.join(tempDir, 'windows', 'Provider.cpp'),
  5765. `#include "Storage.h"\nint use() { Storage s; return s.n; }\n`
  5766. );
  5767. cg = CodeGraph.initSync(tempDir);
  5768. await cg.indexAll();
  5769. cg.resolveReferences();
  5770. const winHeader = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('windows/Storage.h'));
  5771. const appleHeader = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('apple/Storage.h'));
  5772. expect(winHeader, 'windows/Storage.h indexed').toBeDefined();
  5773. expect(appleHeader, 'apple/Storage.h indexed').toBeDefined();
  5774. const winDeps = [...cg.getImpactRadius(winHeader!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  5775. const appleDeps = [...cg.getImpactRadius(appleHeader!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  5776. expect(winDeps.some((p) => p.endsWith('Provider.cpp')), 'same-dir header gets the includer').toBe(true);
  5777. expect(appleDeps.some((p) => p.endsWith('Provider.cpp')), 'other-platform header does NOT').toBe(false);
  5778. });
  5779. it('a Kotlin Multiplatform commonMain import resolves to the expect, not a platform actual', async () => {
  5780. const common = path.join(tempDir, 'src/commonMain/kotlin/app');
  5781. const android = path.join(tempDir, 'src/androidMain/kotlin/app');
  5782. fs.mkdirSync(common, { recursive: true });
  5783. fs.mkdirSync(android, { recursive: true });
  5784. fs.writeFileSync(path.join(common, 'Platform.kt'), `package app\nexpect class PlatformContext\n`);
  5785. fs.writeFileSync(path.join(android, 'Platform.android.kt'), `package app\nactual class PlatformContext\n`);
  5786. fs.writeFileSync(
  5787. path.join(common, 'Db.kt'),
  5788. `package app\nimport app.PlatformContext\nclass Db {\n fun open(ctx: PlatformContext) {}\n}\n`
  5789. );
  5790. cg = CodeGraph.initSync(tempDir);
  5791. await cg.indexAll();
  5792. cg.resolveReferences();
  5793. const expectCtx = cg
  5794. .getNodesByKind('class')
  5795. .find((n) => n.name === 'PlatformContext' && n.filePath.endsWith('commonMain/kotlin/app/Platform.kt'));
  5796. expect(expectCtx, 'commonMain expect PlatformContext').toBeDefined();
  5797. const deps = [...cg.getImpactRadius(expectCtx!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  5798. expect(deps.some((p) => p.endsWith('Db.kt')), 'commonMain import lands on the expect, not the actual').toBe(true);
  5799. });
  5800. });
  5801. describe('Delphi form code-behind pairing', () => {
  5802. let tempDir: string;
  5803. let cg: CodeGraph;
  5804. beforeEach(() => {
  5805. tempDir = createTempDir();
  5806. });
  5807. afterEach(() => {
  5808. if (cg) cg.close();
  5809. if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
  5810. });
  5811. it('links a `.dfm` form to its sibling `.pas` code-behind unit', async () => {
  5812. // A Delphi form unit owns its visual form definition via `{$R *.dfm}`, not a
  5813. // `uses` clause — so a `.dfm` used only as a form definition looked orphaned.
  5814. fs.writeFileSync(path.join(tempDir, 'UFRMAbout.dfm'),
  5815. `object FRMAbout: TFRMAbout\n Caption = 'About'\nend\n`);
  5816. fs.writeFileSync(path.join(tempDir, 'UFRMAbout.pas'),
  5817. `unit UFRMAbout;\ninterface\nuses Forms;\ntype\n TFRMAbout = class(TForm)\n end;\nimplementation\n{$R *.dfm}\nend.\n`);
  5818. cg = CodeGraph.initSync(tempDir);
  5819. await cg.indexAll();
  5820. cg.resolveReferences();
  5821. const dfm = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('UFRMAbout.dfm'));
  5822. expect(dfm, 'UFRMAbout.dfm file node').toBeDefined();
  5823. const deps = cg.getFileDependents(dfm!.filePath);
  5824. expect(deps.some((p) => p.endsWith('UFRMAbout.pas')), 'the .pas unit links its .dfm form').toBe(true);
  5825. });
  5826. });
  5827. describe('Liquid Shopify JSON template section resolution', () => {
  5828. let tempDir: string;
  5829. let cg: CodeGraph;
  5830. beforeEach(() => {
  5831. tempDir = createTempDir();
  5832. });
  5833. afterEach(() => {
  5834. if (cg) cg.close();
  5835. if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
  5836. });
  5837. it('links a Shopify JSON template section `type` to its sections/<type>.liquid', async () => {
  5838. // Shopify OS 2.0 templates are JSON, referencing sections by `type` — not
  5839. // a `{% section %}` Liquid tag — so a section used only from a JSON template
  5840. // looked unused. The JSON is now indexed and its `type`s linked.
  5841. fs.mkdirSync(path.join(tempDir, 'sections'), { recursive: true });
  5842. fs.mkdirSync(path.join(tempDir, 'templates/customers'), { recursive: true });
  5843. fs.writeFileSync(path.join(tempDir, 'sections/main-product.liquid'), `<div>{{ product.title }}</div>\n`);
  5844. fs.writeFileSync(path.join(tempDir, 'sections/main-login.liquid'), `<form>{{ 'customer.login' | t }}</form>\n`);
  5845. fs.writeFileSync(path.join(tempDir, 'templates/product.json'), JSON.stringify({ sections: { main: { type: 'main-product' } }, order: ['main'] }));
  5846. // Nested template dir (templates/customers/login.json) must resolve too.
  5847. fs.writeFileSync(path.join(tempDir, 'templates/customers/login.json'), JSON.stringify({ sections: { main: { type: 'main-login' } }, order: ['main'] }));
  5848. cg = CodeGraph.initSync(tempDir);
  5849. await cg.indexAll();
  5850. cg.resolveReferences();
  5851. const product = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('sections/main-product.liquid'));
  5852. const login = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('sections/main-login.liquid'));
  5853. expect(product, 'main-product section').toBeDefined();
  5854. expect(login, 'main-login section').toBeDefined();
  5855. expect(cg.getFileDependents(product!.filePath).some((p) => p.endsWith('templates/product.json')), 'top-level JSON template links its section').toBe(true);
  5856. expect(cg.getFileDependents(login!.filePath).some((p) => p.endsWith('customers/login.json')), 'nested JSON template links its section').toBe(true);
  5857. });
  5858. });
  5859. describe('Lua/Luau require resolution', () => {
  5860. let tempDir: string;
  5861. let cg: CodeGraph;
  5862. beforeEach(() => {
  5863. tempDir = createTempDir();
  5864. });
  5865. afterEach(() => {
  5866. if (cg) cg.close();
  5867. if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
  5868. });
  5869. it('resolves a dotted Lua require and an instance-path Luau require to their module files', async () => {
  5870. // The require is the ONLY link (no method call), so coverage here proves the
  5871. // require resolver specifically, not method-call name-matching.
  5872. // Lua dotted module path: require("myapp.config") → lua/myapp/config.lua.
  5873. fs.mkdirSync(path.join(tempDir, 'lua/myapp'), { recursive: true });
  5874. fs.writeFileSync(path.join(tempDir, 'lua/myapp/config.lua'), `local M = {}\nfunction M.setup() end\nreturn M\n`);
  5875. fs.writeFileSync(path.join(tempDir, 'lua/myapp/init.lua'), `local config = require("myapp.config")\nreturn config\n`);
  5876. // Luau Roblox instance-path require (only the leaf survives extraction):
  5877. // require(script.Util.helper) → src/Util/helper.luau.
  5878. fs.mkdirSync(path.join(tempDir, 'src/Util'), { recursive: true });
  5879. fs.writeFileSync(path.join(tempDir, 'src/Util/helper.luau'), `local H = {}\nfunction H.go() end\nreturn H\n`);
  5880. fs.writeFileSync(path.join(tempDir, 'src/init.luau'), `local helper = require(script.Util.helper)\nreturn helper\n`);
  5881. cg = CodeGraph.initSync(tempDir);
  5882. await cg.indexAll();
  5883. cg.resolveReferences();
  5884. const config = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('myapp/config.lua'));
  5885. const helper = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('Util/helper.luau'));
  5886. expect(config, 'config.lua file node').toBeDefined();
  5887. expect(helper, 'helper.luau file node').toBeDefined();
  5888. const cfgDeps = cg.getFileDependents(config!.filePath);
  5889. const helpDeps = cg.getFileDependents(helper!.filePath);
  5890. expect(cfgDeps.some((p) => p.endsWith('myapp/init.lua')), 'dotted Lua require resolves to the module').toBe(true);
  5891. expect(helpDeps.some((p) => p.endsWith('src/init.luau')), 'instance-path Luau require resolves to the module').toBe(true);
  5892. });
  5893. });
  5894. describe('Rust module-path call resolution', () => {
  5895. let tempDir: string;
  5896. let cg: CodeGraph;
  5897. beforeEach(() => {
  5898. tempDir = createTempDir();
  5899. });
  5900. afterEach(() => {
  5901. if (cg) cg.close();
  5902. if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
  5903. });
  5904. it('a bare submodule call (`users::router()`) resolves self-relative to the submodule fn', async () => {
  5905. // The canonical Axum router-assembly pattern: a parent module calls each
  5906. // submodule's `router()`. `users::` / `profiles::` are SELF-relative
  5907. // submodule prefixes (2018 edition) — `mod users;` makes `users` a child of
  5908. // the CURRENT module, NOT `crate::users`. Before the fix the bare prefix was
  5909. // resolved crate-relative only (looking for `src/users.rs`), so it found
  5910. // nothing and the handler modules looked dependent-less.
  5911. const http = path.join(tempDir, 'src/http');
  5912. fs.mkdirSync(http, { recursive: true });
  5913. fs.writeFileSync(path.join(tempDir, 'src/lib.rs'), `pub mod http;\n`);
  5914. fs.writeFileSync(
  5915. path.join(http, 'mod.rs'),
  5916. `mod users;\nmod profiles;\npub fn api_router() {\n users::router();\n profiles::router();\n}\n`
  5917. );
  5918. fs.writeFileSync(path.join(http, 'users.rs'), `pub fn router() -> i32 { 1 }\n`);
  5919. fs.writeFileSync(path.join(http, 'profiles.rs'), `pub fn router() -> i32 { 2 }\n`);
  5920. cg = CodeGraph.initSync(tempDir);
  5921. await cg.indexAll();
  5922. cg.resolveReferences();
  5923. // Each submodule's same-named `router` fn must get mod.rs as a dependent —
  5924. // proving the bare prefix resolved self-relative AND disambiguated the
  5925. // colliding `router` name to the correct file (not an arbitrary one).
  5926. const routers = cg.getNodesByKind('function').filter((n) => n.name === 'router');
  5927. const usersRouter = routers.find((n) => n.filePath.endsWith('http/users.rs'));
  5928. const profilesRouter = routers.find((n) => n.filePath.endsWith('http/profiles.rs'));
  5929. expect(usersRouter, 'users.rs router fn').toBeDefined();
  5930. expect(profilesRouter, 'profiles.rs router fn').toBeDefined();
  5931. const usersDeps = [...cg.getImpactRadius(usersRouter!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  5932. const profilesDeps = [...cg.getImpactRadius(profilesRouter!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  5933. expect(usersDeps.some((p) => p.endsWith('http/mod.rs')), 'users::router() lands on users.rs').toBe(true);
  5934. expect(profilesDeps.some((p) => p.endsWith('http/mod.rs')), 'profiles::router() lands on profiles.rs').toBe(true);
  5935. });
  5936. it('a 3-segment module-path call (`database::profiles::find()`) resolves to the leaf fn', async () => {
  5937. // A 2-level module path — the common `db.run(move |c| database::profiles::find(c))`
  5938. // / `crate::a::b::func()` shape. The reference-resolver pre-filter used to drop any
  5939. // `a::b::c` whose leaf it never checked (it tested only the first segment and the
  5940. // `b::c` remainder, neither of which names a symbol), so the call never reached the
  5941. // Rust path resolver and the leaf module looked dependent-less.
  5942. const routes = path.join(tempDir, 'src/routes');
  5943. const database = path.join(tempDir, 'src/database');
  5944. fs.mkdirSync(routes, { recursive: true });
  5945. fs.mkdirSync(database, { recursive: true });
  5946. fs.writeFileSync(path.join(tempDir, 'src/lib.rs'), `pub mod routes;\npub mod database;\n`);
  5947. fs.writeFileSync(path.join(database, 'mod.rs'), `pub mod profiles;\n`);
  5948. fs.writeFileSync(path.join(database, 'profiles.rs'), `pub fn find(id: i32) -> i32 { id }\n`);
  5949. fs.writeFileSync(
  5950. path.join(routes, 'mod.rs'),
  5951. `use crate::database;\npub fn get_profile(id: i32) -> i32 {\n database::profiles::find(id)\n}\n`
  5952. );
  5953. cg = CodeGraph.initSync(tempDir);
  5954. await cg.indexAll();
  5955. cg.resolveReferences();
  5956. const find = cg
  5957. .getNodesByKind('function')
  5958. .find((n) => n.name === 'find' && n.filePath.endsWith('database/profiles.rs'));
  5959. expect(find, 'database/profiles.rs find fn').toBeDefined();
  5960. const deps = [...cg.getImpactRadius(find!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  5961. expect(deps.some((p) => p.endsWith('routes/mod.rs')), 'database::profiles::find() resolves to the leaf fn').toBe(true);
  5962. });
  5963. it('Rocket `routes![…]` / `catchers![…]` macros link the mount to the handler fns', async () => {
  5964. // Tree-sitter leaves the macro body as a raw token tree, so the handler
  5965. // paths inside `routes![a::b::handler, …]` are invisible to the call walker
  5966. // and the handlers — mounted by Rocket at runtime, not called in-repo — look
  5967. // like they have no caller. The route-macro extractor reconstructs each path
  5968. // and emits a reference, which the Rust path resolver links to the handler.
  5969. const routes = path.join(tempDir, 'src/routes');
  5970. fs.mkdirSync(routes, { recursive: true });
  5971. fs.writeFileSync(path.join(tempDir, 'src/lib.rs'),
  5972. `mod routes;\nfn not_found() {}\npub fn rocket() {\n` +
  5973. ` rocket::build()\n` +
  5974. ` .mount("/api", routes![routes::users::post_users, routes::users::get_user])\n` +
  5975. ` .register("/", catchers![not_found]);\n}\n`);
  5976. fs.writeFileSync(path.join(routes, 'mod.rs'), `pub mod users;\n`);
  5977. fs.writeFileSync(path.join(routes, 'users.rs'), `pub fn post_users() {}\npub fn get_user() {}\n`);
  5978. cg = CodeGraph.initSync(tempDir);
  5979. await cg.indexAll();
  5980. cg.resolveReferences();
  5981. const handlers = cg.getNodesByKind('function').filter((n) => n.filePath.endsWith('routes/users.rs'));
  5982. expect(handlers.length, 'both handler fns indexed').toBe(2);
  5983. for (const h of handlers) {
  5984. const deps = [...cg.getImpactRadius(h.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  5985. expect(deps.some((p) => p.endsWith('lib.rs')), `routes![] links ${h.name} to its mount in lib.rs`).toBe(true);
  5986. }
  5987. });
  5988. });
  5989. describe('SvelteKit load → page synthesizer', () => {
  5990. let tempDir: string;
  5991. let cg: CodeGraph;
  5992. beforeEach(() => {
  5993. tempDir = createTempDir();
  5994. });
  5995. afterEach(() => {
  5996. if (cg) cg.close();
  5997. if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
  5998. });
  5999. it('links a +page.svelte to its OWN directory\'s +page.server.js load, not another route\'s', async () => {
  6000. // SvelteKit wires +page.server.js's `load` to +page.svelte's `data` BY FILE
  6001. // PATH — there is no static import — so editing a loader showed no impact on
  6002. // the page it feeds. The synthesizer links each page component to the `load`
  6003. // in its OWN directory (path-deterministic, so it never crosses routes).
  6004. const login = path.join(tempDir, 'src/routes/login');
  6005. const register = path.join(tempDir, 'src/routes/register');
  6006. fs.mkdirSync(login, { recursive: true });
  6007. fs.mkdirSync(register, { recursive: true });
  6008. fs.writeFileSync(path.join(login, '+page.svelte'), `<script>export let data;</script>\n<h1>Login {data.x}</h1>\n`);
  6009. fs.writeFileSync(path.join(login, '+page.server.js'), `export function load() { return { x: 1 }; }\n`);
  6010. fs.writeFileSync(path.join(register, '+page.svelte'), `<script>export let data;</script>\n<h1>Register</h1>\n`);
  6011. fs.writeFileSync(path.join(register, '+page.server.js'), `export function load() { return { y: 2 }; }\n`);
  6012. cg = CodeGraph.initSync(tempDir);
  6013. await cg.indexAll();
  6014. cg.resolveReferences();
  6015. const loginLoad = cg
  6016. .getNodesByKind('function')
  6017. .find((n) => n.name === 'load' && n.filePath.endsWith('login/+page.server.js'));
  6018. expect(loginLoad, 'login load fn').toBeDefined();
  6019. const impacted = [...cg.getImpactRadius(loginLoad!.id, 3).nodes.values()].map((n) => n.filePath ?? '');
  6020. // editing login's load surfaces login's page (the framework-wired data flow)…
  6021. expect(impacted.some((p) => p.endsWith('login/+page.svelte')), 'load links to its own page').toBe(true);
  6022. // …but never register's page (same-directory only).
  6023. expect(impacted.some((p) => p.endsWith('register/+page.svelte')), 'does not cross routes').toBe(false);
  6024. });
  6025. });
  6026. describe('Nuxt nested auto-imported component resolution', () => {
  6027. let tempDir: string;
  6028. let cg: CodeGraph;
  6029. beforeEach(() => {
  6030. tempDir = createTempDir();
  6031. });
  6032. afterEach(() => {
  6033. if (cg) cg.close();
  6034. if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
  6035. });
  6036. it('links a `<MediaCard/>` usage to components/media/Card.vue (Nuxt dir-prefixed auto-import)', async () => {
  6037. // Nuxt auto-imports a nested component by a DIRECTORY-PREFIXED name —
  6038. // components/media/Card.vue is used as <MediaCard/>, not <Card/> — but the
  6039. // component node is named by basename (`Card`), so the PascalCase usage
  6040. // didn't resolve and the nested component looked unused.
  6041. const media = path.join(tempDir, 'components/media');
  6042. fs.mkdirSync(media, { recursive: true });
  6043. fs.writeFileSync(path.join(media, 'Card.vue'), `<template><div>card</div></template>\n<script setup>defineProps(['item'])</script>\n`);
  6044. fs.writeFileSync(
  6045. path.join(tempDir, 'components/Grid.vue'),
  6046. `<template>\n <div><MediaCard :item="i" /></div>\n</template>\n<script setup>const i = {}</script>\n`
  6047. );
  6048. cg = CodeGraph.initSync(tempDir);
  6049. await cg.indexAll();
  6050. cg.resolveReferences();
  6051. const card = cg.getNodesByKind('component').find((n) => n.filePath.endsWith('media/Card.vue'));
  6052. expect(card, 'media/Card.vue component').toBeDefined();
  6053. const deps = [...cg.getImpactRadius(card!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  6054. expect(deps.some((p) => p.endsWith('components/Grid.vue')), '<MediaCard> links Grid to media/Card.vue').toBe(true);
  6055. });
  6056. });
  6057. describe('Swift property-wrapper attribute type references', () => {
  6058. let tempDir: string;
  6059. let cg: CodeGraph;
  6060. beforeEach(() => {
  6061. tempDir = createTempDir();
  6062. });
  6063. afterEach(() => {
  6064. if (cg) cg.close();
  6065. if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
  6066. });
  6067. it('a Fluent `@Siblings(through: Pivot.self)` links the model to the pivot type', async () => {
  6068. // A many-to-many pivot/join model is referenced ONLY through the relationship
  6069. // property wrapper's metatype argument (`Pivot.self`), never by a controller
  6070. // query. The wrapper type was captured but the argument expression wasn't
  6071. // walked, so the pivot model looked like nothing depended on it.
  6072. fs.writeFileSync(path.join(tempDir, 'Pivot.swift'),
  6073. `import Fluent\nfinal class AcronymCategoryPivot: Model {\n static let schema = "acronym-category"\n}\n`);
  6074. fs.writeFileSync(path.join(tempDir, 'Acronym.swift'),
  6075. `import Fluent\nfinal class Acronym: Model {\n` +
  6076. ` @Siblings(through: AcronymCategoryPivot.self, from: \\.$acronym, to: \\.$category)\n` +
  6077. ` var categories: [Category]\n}\n`);
  6078. cg = CodeGraph.initSync(tempDir);
  6079. await cg.indexAll();
  6080. cg.resolveReferences();
  6081. const pivot = cg.getNodesByKind('class').find((n) => n.name === 'AcronymCategoryPivot');
  6082. expect(pivot, 'pivot model class').toBeDefined();
  6083. const deps = [...cg.getImpactRadius(pivot!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  6084. expect(deps.some((p) => p.endsWith('Acronym.swift')), '@Siblings metatype arg links Acronym to the pivot').toBe(true);
  6085. });
  6086. });
  6087. describe('Objective-C messages, class receivers, and #import', () => {
  6088. let tempDir: string;
  6089. let cg: CodeGraph;
  6090. beforeEach(() => {
  6091. tempDir = createTempDir();
  6092. });
  6093. afterEach(() => {
  6094. if (cg) cg.close();
  6095. if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
  6096. });
  6097. it('resolves single-arg selectors, class-message receivers, and #import headers', async () => {
  6098. fs.writeFileSync(
  6099. path.join(tempDir, 'SDImageCache.h'),
  6100. `#import <Foundation/Foundation.h>
  6101. @interface SDImageCache : NSObject
  6102. + (instancetype)sharedCache;
  6103. + (void)storeImage:(NSString *)key;
  6104. @end
  6105. `
  6106. );
  6107. fs.writeFileSync(
  6108. path.join(tempDir, 'SDImageCache.m'),
  6109. `#import "SDImageCache.h"
  6110. @implementation SDImageCache
  6111. + (instancetype)sharedCache { return nil; }
  6112. + (void)storeImage:(NSString *)key { }
  6113. @end
  6114. `
  6115. );
  6116. fs.writeFileSync(
  6117. path.join(tempDir, 'SDManager.m'),
  6118. `#import "SDImageCache.h"
  6119. @interface SDManager : NSObject
  6120. @end
  6121. @implementation SDManager
  6122. - (void)run {
  6123. [SDImageCache sharedCache];
  6124. [SDImageCache storeImage:@"k"];
  6125. }
  6126. @end
  6127. `
  6128. );
  6129. cg = CodeGraph.initSync(tempDir);
  6130. await cg.indexAll();
  6131. cg.resolveReferences();
  6132. // 1. The single-argument selector `[SDImageCache storeImage:@"k"]` resolves
  6133. // to the `storeImage:` method — named WITH its colon both at the call site
  6134. // and the definition (before the fix the call site dropped the colon).
  6135. const storeImage = cg.getNodesByKind('method').find((n) => n.name === 'storeImage:');
  6136. expect(storeImage, 'storeImage: method').toBeDefined();
  6137. const storeCallers = [...cg.getImpactRadius(storeImage!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  6138. expect(storeCallers.some((p) => p.endsWith('SDManager.m'))).toBe(true);
  6139. // 2. The class-message receiver `[SDImageCache sharedCache]` references the
  6140. // SDImageCache class (whose @interface lives in the header).
  6141. const cache = cg.getNodesByKind('class').find((n) => n.name === 'SDImageCache');
  6142. expect(cache, 'SDImageCache class').toBeDefined();
  6143. const classDeps = [...cg.getImpactRadius(cache!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  6144. expect(classDeps.some((p) => p.endsWith('SDManager.m'))).toBe(true);
  6145. // 3. `#import "SDImageCache.h"` resolves to the header FILE — editing it
  6146. // surfaces both importers.
  6147. const header = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('SDImageCache.h'));
  6148. expect(header, 'SDImageCache.h indexed').toBeDefined();
  6149. const importers = [...cg.getImpactRadius(header!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
  6150. expect(importers.some((p) => p.endsWith('SDManager.m'))).toBe(true);
  6151. });
  6152. });
  6153. describe('Full Indexing', () => {
  6154. let tempDir: string;
  6155. beforeEach(() => {
  6156. tempDir = createTempDir();
  6157. });
  6158. afterEach(() => {
  6159. cleanupTempDir(tempDir);
  6160. });
  6161. it('should index a TypeScript file', async () => {
  6162. // Create test file
  6163. const srcDir = path.join(tempDir, 'src');
  6164. fs.mkdirSync(srcDir);
  6165. fs.writeFileSync(
  6166. path.join(srcDir, 'utils.ts'),
  6167. `
  6168. export function add(a: number, b: number): number {
  6169. return a + b;
  6170. }
  6171. export function multiply(a: number, b: number): number {
  6172. return a * b;
  6173. }
  6174. `
  6175. );
  6176. // Initialize and index
  6177. const cg = CodeGraph.initSync(tempDir);
  6178. const result = await cg.indexAll();
  6179. expect(result.success).toBe(true);
  6180. expect(result.filesIndexed).toBe(1);
  6181. expect(result.nodesCreated).toBeGreaterThanOrEqual(2);
  6182. // Check nodes were stored
  6183. const nodes = cg.getNodesInFile('src/utils.ts');
  6184. expect(nodes.length).toBeGreaterThanOrEqual(2);
  6185. const addFunc = nodes.find((n) => n.name === 'add');
  6186. expect(addFunc).toBeDefined();
  6187. expect(addFunc?.kind).toBe('function');
  6188. cg.close();
  6189. });
  6190. it('should index multiple files', async () => {
  6191. // Create test files
  6192. const srcDir = path.join(tempDir, 'src');
  6193. fs.mkdirSync(srcDir);
  6194. fs.writeFileSync(
  6195. path.join(srcDir, 'math.ts'),
  6196. `export function add(a: number, b: number) { return a + b; }`
  6197. );
  6198. fs.writeFileSync(
  6199. path.join(srcDir, 'string.ts'),
  6200. `export function capitalize(s: string) { return s.toUpperCase(); }`
  6201. );
  6202. // Initialize and index
  6203. const cg = CodeGraph.initSync(tempDir);
  6204. const result = await cg.indexAll();
  6205. expect(result.success).toBe(true);
  6206. expect(result.filesIndexed).toBe(2);
  6207. const files = cg.getFiles();
  6208. expect(files.length).toBe(2);
  6209. cg.close();
  6210. });
  6211. it('should track file hashes for incremental updates', async () => {
  6212. // Create initial file
  6213. const srcDir = path.join(tempDir, 'src');
  6214. fs.mkdirSync(srcDir);
  6215. fs.writeFileSync(path.join(srcDir, 'main.ts'), `export const x = 1;`);
  6216. // Initialize and index
  6217. const cg = CodeGraph.initSync(tempDir);
  6218. await cg.indexAll();
  6219. // Check file is tracked
  6220. const file = cg.getFile('src/main.ts');
  6221. expect(file).toBeDefined();
  6222. expect(file?.contentHash).toBeDefined();
  6223. // Modify file
  6224. fs.writeFileSync(path.join(srcDir, 'main.ts'), `export const x = 2;`);
  6225. // Check for changes
  6226. const changes = cg.getChangedFiles();
  6227. expect(changes.modified).toContain('src/main.ts');
  6228. cg.close();
  6229. });
  6230. it('should sync and detect changes', async () => {
  6231. // Create initial file
  6232. const srcDir = path.join(tempDir, 'src');
  6233. fs.mkdirSync(srcDir);
  6234. fs.writeFileSync(
  6235. path.join(srcDir, 'main.ts'),
  6236. `export function original() { return 1; }`
  6237. );
  6238. // Initialize and index
  6239. const cg = CodeGraph.initSync(tempDir);
  6240. await cg.indexAll();
  6241. const initialNodes = cg.getNodesInFile('src/main.ts');
  6242. expect(initialNodes.some((n) => n.name === 'original')).toBe(true);
  6243. // Modify file
  6244. fs.writeFileSync(
  6245. path.join(srcDir, 'main.ts'),
  6246. `export function updated() { return 2; }`
  6247. );
  6248. // Sync
  6249. const syncResult = await cg.sync();
  6250. expect(syncResult.filesModified).toBe(1);
  6251. // Check nodes were updated
  6252. const updatedNodes = cg.getNodesInFile('src/main.ts');
  6253. expect(updatedNodes.some((n) => n.name === 'updated')).toBe(true);
  6254. expect(updatedNodes.some((n) => n.name === 'original')).toBe(false);
  6255. cg.close();
  6256. });
  6257. it('should count file-level tracked YAML files as indexed', async () => {
  6258. fs.writeFileSync(path.join(tempDir, 'app.yaml'), 'name: test\n');
  6259. fs.writeFileSync(path.join(tempDir, 'routes.yml'), 'route: value\n');
  6260. const cg = CodeGraph.initSync(tempDir);
  6261. const result = await cg.indexAll();
  6262. expect(result.success).toBe(true);
  6263. expect(result.filesIndexed).toBe(2);
  6264. expect(result.filesSkipped).toBe(0);
  6265. expect(cg.getFiles().map((f) => f.path).sort()).toEqual(['app.yaml', 'routes.yml']);
  6266. cg.close();
  6267. });
  6268. it('should count file-level tracked YAML/Twig files as indexed in indexFiles()', async () => {
  6269. fs.writeFileSync(path.join(tempDir, 'app.yaml'), 'name: test\n');
  6270. fs.writeFileSync(path.join(tempDir, 'view.twig'), '{{ title }}\n');
  6271. const cg = CodeGraph.initSync(tempDir);
  6272. const result = await cg.indexFiles(['app.yaml', 'view.twig']);
  6273. expect(result.success).toBe(true);
  6274. expect(result.filesIndexed).toBe(2);
  6275. expect(result.filesSkipped).toBe(0);
  6276. const tracked = cg.getFiles().map((f) => `${f.path}:${f.language}`).sort();
  6277. expect(tracked).toEqual(['app.yaml:yaml', 'view.twig:twig']);
  6278. cg.close();
  6279. });
  6280. it('should count file-level tracked .properties files as indexed', async () => {
  6281. fs.writeFileSync(path.join(tempDir, 'application.properties'), 'server.port=8080\n');
  6282. fs.writeFileSync(path.join(tempDir, 'log.properties'), 'log.level=INFO\n');
  6283. const cg = CodeGraph.initSync(tempDir);
  6284. const result = await cg.indexAll();
  6285. expect(result.success).toBe(true);
  6286. expect(result.filesIndexed).toBe(2);
  6287. expect(result.filesSkipped).toBe(0);
  6288. cg.close();
  6289. });
  6290. it('should resolve an ES import from a .xsjs file to a .xsjslib file (#556)', async () => {
  6291. // Exercises the JS import-path resolution list: `./helpers` must resolve to
  6292. // `helpers.xsjslib`. `decoy.js` exports the same symbol name and is never
  6293. // imported — without .xsjs/.xsjslib in the list the import resolves to
  6294. // nothing and the call falls back to same-name matching, which binds the
  6295. // edge to the decoy. The decoy is what makes this test fail on a regression:
  6296. // with a lone helpers.xsjslib the fallback happens to pick the right file.
  6297. fs.writeFileSync(
  6298. path.join(tempDir, 'helpers.xsjslib'),
  6299. 'export function buildQuery(table) {\n return "SELECT * FROM " + table;\n}\n'
  6300. );
  6301. fs.writeFileSync(
  6302. path.join(tempDir, 'decoy.js'),
  6303. 'export function buildQuery(table) {\n return "DECOY " + table;\n}\n'
  6304. );
  6305. fs.writeFileSync(
  6306. path.join(tempDir, 'service.xsjs'),
  6307. 'import { buildQuery } from "./helpers";\n\nfunction run() {\n return buildQuery("users");\n}\n'
  6308. );
  6309. const cg = CodeGraph.initSync(tempDir);
  6310. await cg.indexAll();
  6311. const run = cg.getNodesInFile('service.xsjs').find((n) => n.name === 'run');
  6312. const buildQuery = cg.getNodesInFile('helpers.xsjslib').find((n) => n.name === 'buildQuery');
  6313. const decoy = cg.getNodesInFile('decoy.js').find((n) => n.name === 'buildQuery');
  6314. expect(run).toBeDefined();
  6315. expect(buildQuery).toBeDefined();
  6316. expect(decoy).toBeDefined();
  6317. expect(
  6318. cg.getFileDependencies('service.xsjs'),
  6319. "'./helpers' should resolve to helpers.xsjslib, not the same-named decoy"
  6320. ).toEqual(['helpers.xsjslib']);
  6321. const outgoing = cg.getOutgoingEdges(run!.id);
  6322. expect(
  6323. outgoing.find((e) => e.target === buildQuery!.id),
  6324. 'run() should resolve buildQuery across the .xsjs -> .xsjslib import'
  6325. ).toBeDefined();
  6326. expect(
  6327. outgoing.find((e) => e.target === decoy!.id),
  6328. 'run() must not bind to the unrelated same-named export in decoy.js'
  6329. ).toBeUndefined();
  6330. cg.close();
  6331. });
  6332. it('should count the full file-level tracked class (yaml/twig/properties) in indexFiles()', async () => {
  6333. fs.writeFileSync(path.join(tempDir, 'app.yaml'), 'name: test\n');
  6334. fs.writeFileSync(path.join(tempDir, 'view.twig'), '{{ title }}\n');
  6335. fs.writeFileSync(path.join(tempDir, 'application.properties'), 'server.port=8080\n');
  6336. const cg = CodeGraph.initSync(tempDir);
  6337. const result = await cg.indexFiles(['app.yaml', 'view.twig', 'application.properties']);
  6338. expect(result.success).toBe(true);
  6339. expect(result.filesIndexed).toBe(3);
  6340. expect(result.filesSkipped).toBe(0);
  6341. const tracked = cg.getFiles().map((f) => `${f.path}:${f.language}`).sort();
  6342. expect(tracked).toEqual(['app.yaml:yaml', 'application.properties:properties', 'view.twig:twig']);
  6343. cg.close();
  6344. });
  6345. });
  6346. describe('Path Normalization', () => {
  6347. it('should convert backslashes to forward slashes', () => {
  6348. expect(normalizePath('gui\\node_modules\\foo')).toBe('gui/node_modules/foo');
  6349. expect(normalizePath('src\\components\\Button.tsx')).toBe('src/components/Button.tsx');
  6350. });
  6351. it('should leave forward-slash paths unchanged', () => {
  6352. expect(normalizePath('src/components/Button.tsx')).toBe('src/components/Button.tsx');
  6353. });
  6354. it('should handle empty string', () => {
  6355. expect(normalizePath('')).toBe('');
  6356. });
  6357. });
  6358. describe('Directory Exclusion', () => {
  6359. let tempDir: string;
  6360. beforeEach(() => {
  6361. tempDir = createTempDir();
  6362. });
  6363. afterEach(() => {
  6364. cleanupTempDir(tempDir);
  6365. });
  6366. it('should exclude directories listed in .gitignore', () => {
  6367. // Create structure: src/index.ts + node_modules/pkg/index.js, gitignore node_modules
  6368. const srcDir = path.join(tempDir, 'src');
  6369. const nmDir = path.join(tempDir, 'node_modules', 'pkg');
  6370. fs.mkdirSync(srcDir, { recursive: true });
  6371. fs.mkdirSync(nmDir, { recursive: true });
  6372. fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;');
  6373. fs.writeFileSync(path.join(nmDir, 'index.js'), 'module.exports = {};');
  6374. fs.writeFileSync(path.join(tempDir, '.gitignore'), 'node_modules/\n');
  6375. const files = scanDirectory(tempDir);
  6376. expect(files).toContain('src/index.ts');
  6377. expect(files.every((f) => !f.includes('node_modules'))).toBe(true);
  6378. });
  6379. it('should exclude nested node_modules via a root .gitignore', () => {
  6380. // A trailing-slash pattern with no leading slash matches at any depth.
  6381. const srcDir = path.join(tempDir, 'packages', 'app', 'src');
  6382. const nmDir = path.join(tempDir, 'packages', 'app', 'node_modules', 'pkg');
  6383. fs.mkdirSync(srcDir, { recursive: true });
  6384. fs.mkdirSync(nmDir, { recursive: true });
  6385. fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;');
  6386. fs.writeFileSync(path.join(nmDir, 'index.js'), 'module.exports = {};');
  6387. fs.writeFileSync(path.join(tempDir, '.gitignore'), 'node_modules/\n');
  6388. const files = scanDirectory(tempDir);
  6389. expect(files).toContain('packages/app/src/index.ts');
  6390. expect(files.every((f) => !f.includes('node_modules'))).toBe(true);
  6391. });
  6392. it('should apply a nested .gitignore only to its own subtree', () => {
  6393. const appSrc = path.join(tempDir, 'app', 'src');
  6394. fs.mkdirSync(appSrc, { recursive: true });
  6395. fs.writeFileSync(path.join(appSrc, 'keep.ts'), 'export const a = 1;');
  6396. fs.writeFileSync(path.join(appSrc, 'skip.ts'), 'export const b = 2;');
  6397. fs.writeFileSync(path.join(tempDir, 'app', '.gitignore'), 'src/skip.ts\n');
  6398. // A sibling with the same name outside app/ must NOT be ignored.
  6399. const otherDir = path.join(tempDir, 'other', 'src');
  6400. fs.mkdirSync(otherDir, { recursive: true });
  6401. fs.writeFileSync(path.join(otherDir, 'skip.ts'), 'export const c = 3;');
  6402. const files = scanDirectory(tempDir);
  6403. expect(files).toContain('app/src/keep.ts');
  6404. expect(files).not.toContain('app/src/skip.ts');
  6405. expect(files).toContain('other/src/skip.ts');
  6406. });
  6407. it('should always skip .git directories', () => {
  6408. const srcDir = path.join(tempDir, 'src');
  6409. const gitDir = path.join(tempDir, '.git', 'objects');
  6410. fs.mkdirSync(srcDir, { recursive: true });
  6411. fs.mkdirSync(gitDir, { recursive: true });
  6412. fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;');
  6413. fs.writeFileSync(path.join(gitDir, 'pack.ts'), 'export const y = 2;');
  6414. const files = scanDirectory(tempDir);
  6415. expect(files).toContain('src/index.ts');
  6416. expect(files.every((f) => !f.includes('.git'))).toBe(true);
  6417. });
  6418. it('should return forward-slash paths on all platforms', () => {
  6419. const srcDir = path.join(tempDir, 'src', 'components');
  6420. fs.mkdirSync(srcDir, { recursive: true });
  6421. fs.writeFileSync(path.join(srcDir, 'Button.tsx'), 'export function Button() {}');
  6422. const files = scanDirectory(tempDir);
  6423. expect(files.length).toBe(1);
  6424. expect(files[0]).toBe('src/components/Button.tsx');
  6425. expect(files[0]).not.toContain('\\');
  6426. });
  6427. });
  6428. describe('Nested .gitignore node_modules exclusion (#1567)', () => {
  6429. let tempDir: string;
  6430. beforeEach(() => {
  6431. tempDir = createTempDir();
  6432. });
  6433. afterEach(() => {
  6434. cleanupTempDir(tempDir);
  6435. });
  6436. function plantNodeModules(subproject: string, packages = 80): void {
  6437. const base = path.join(tempDir, subproject, 'node_modules');
  6438. for (let i = 0; i < packages; i++) {
  6439. const pkg = path.join(base, `pkg${i}`);
  6440. fs.mkdirSync(pkg, { recursive: true });
  6441. fs.writeFileSync(path.join(pkg, 'index.js'), `module.exports = ${i};`);
  6442. fs.writeFileSync(path.join(pkg, 'index.d.ts'), 'export const n: number;');
  6443. if (i % 4 === 0) fs.writeFileSync(path.join(pkg, '.gitignore'), '*.map\n');
  6444. const nested = path.join(pkg, 'node_modules', `nested${i}`);
  6445. fs.mkdirSync(nested, { recursive: true });
  6446. fs.writeFileSync(path.join(nested, 'lib.ts'), 'export const x = 1;');
  6447. }
  6448. }
  6449. function initGitRepo(): void {
  6450. const { execFileSync } = require('child_process') as typeof import('child_process');
  6451. execFileSync('git', ['init'], { cwd: tempDir, stdio: 'ignore' });
  6452. execFileSync('git', ['add', '-A'], { cwd: tempDir, stdio: 'ignore' });
  6453. execFileSync(
  6454. 'git',
  6455. ['-c', 'user.email=test@example.com', '-c', 'user.name=Test', 'commit', '-m', 'init'],
  6456. { cwd: tempDir, stdio: 'ignore' },
  6457. );
  6458. }
  6459. it('excludes node_modules ignored only by a nested .gitignore (git path)', () => {
  6460. fs.mkdirSync(path.join(tempDir, 'frontend', 'src'), { recursive: true });
  6461. fs.mkdirSync(path.join(tempDir, 'extension', 'src'), { recursive: true });
  6462. fs.writeFileSync(path.join(tempDir, 'frontend', 'src', 'app.ts'), 'export const a = 1;');
  6463. fs.writeFileSync(path.join(tempDir, 'extension', 'src', 'ext.ts'), 'export const b = 1;');
  6464. fs.writeFileSync(path.join(tempDir, 'root.ts'), 'export const r = 1;');
  6465. fs.writeFileSync(path.join(tempDir, '.gitignore'), '*.log\n');
  6466. fs.writeFileSync(path.join(tempDir, 'frontend', '.gitignore'), '/node_modules\n');
  6467. fs.writeFileSync(path.join(tempDir, 'extension', '.gitignore'), 'node_modules/\n');
  6468. plantNodeModules('frontend');
  6469. plantNodeModules('extension');
  6470. initGitRepo();
  6471. const files = scanDirectory(tempDir);
  6472. expect(files.sort()).toEqual(['extension/src/ext.ts', 'frontend/src/app.ts', 'root.ts']);
  6473. expect(files.every((f) => !f.includes('node_modules'))).toBe(true);
  6474. });
  6475. it('excludes nested-gitignore node_modules on the filesystem-walk fallback too', () => {
  6476. fs.mkdirSync(path.join(tempDir, 'frontend', 'src'), { recursive: true });
  6477. fs.mkdirSync(path.join(tempDir, 'extension', 'src'), { recursive: true });
  6478. fs.writeFileSync(path.join(tempDir, 'frontend', 'src', 'app.ts'), 'export const a = 1;');
  6479. fs.writeFileSync(path.join(tempDir, 'extension', 'src', 'ext.ts'), 'export const b = 1;');
  6480. fs.writeFileSync(path.join(tempDir, 'root.ts'), 'export const r = 1;');
  6481. fs.writeFileSync(path.join(tempDir, '.gitignore'), '*.log\n');
  6482. fs.writeFileSync(path.join(tempDir, 'frontend', '.gitignore'), '/node_modules\n');
  6483. fs.writeFileSync(path.join(tempDir, 'extension', '.gitignore'), 'node_modules/\n');
  6484. plantNodeModules('frontend', 60);
  6485. plantNodeModules('extension', 60);
  6486. const files = scanDirectory(tempDir);
  6487. expect(files.sort()).toEqual(['extension/src/ext.ts', 'frontend/src/app.ts', 'root.ts']);
  6488. expect(files.every((f) => !f.includes('node_modules'))).toBe(true);
  6489. });
  6490. it('still excludes when root only lists one subproject node_modules (Boba-like)', () => {
  6491. fs.mkdirSync(path.join(tempDir, 'frontend', 'src'), { recursive: true });
  6492. fs.mkdirSync(path.join(tempDir, 'extension', 'src'), { recursive: true });
  6493. fs.writeFileSync(path.join(tempDir, 'frontend', 'src', 'app.ts'), 'export const a = 1;');
  6494. fs.writeFileSync(path.join(tempDir, 'extension', 'src', 'ext.ts'), 'export const b = 1;');
  6495. fs.writeFileSync(path.join(tempDir, 'root.ts'), 'export const r = 1;');
  6496. fs.writeFileSync(
  6497. path.join(tempDir, '.gitignore'),
  6498. ['*.log', 'frontend/node_modules/', 'frontend/.angular/', ''].join('\n'),
  6499. );
  6500. fs.writeFileSync(path.join(tempDir, 'frontend', '.gitignore'), '/node_modules\n');
  6501. fs.writeFileSync(path.join(tempDir, 'extension', '.gitignore'), 'node_modules/\n');
  6502. plantNodeModules('frontend', 40);
  6503. plantNodeModules('extension', 40);
  6504. const fsFiles = scanDirectory(tempDir);
  6505. expect(fsFiles.every((f) => !f.includes('node_modules'))).toBe(true);
  6506. expect(fsFiles.sort()).toEqual(['extension/src/ext.ts', 'frontend/src/app.ts', 'root.ts']);
  6507. initGitRepo();
  6508. const gitFiles = scanDirectory(tempDir);
  6509. expect(gitFiles.every((f) => !f.includes('node_modules'))).toBe(true);
  6510. expect(gitFiles.sort()).toEqual(['extension/src/ext.ts', 'frontend/src/app.ts', 'root.ts']);
  6511. });
  6512. });
  6513. describe('Git Submodules', () => {
  6514. let tempDir: string;
  6515. beforeEach(() => {
  6516. tempDir = createTempDir();
  6517. });
  6518. afterEach(() => {
  6519. cleanupTempDir(tempDir);
  6520. });
  6521. it('should index files inside git submodules (issue #147)', async () => {
  6522. const { execFileSync } = await import('child_process');
  6523. const git = (cwd: string, ...args: string[]) =>
  6524. execFileSync('git', args, { cwd, stdio: 'pipe' });
  6525. // Build a separate "library" repo to use as a submodule source.
  6526. const libDir = path.join(tempDir, '_lib');
  6527. fs.mkdirSync(libDir, { recursive: true });
  6528. git(libDir, 'init', '-q');
  6529. git(libDir, 'config', 'user.email', 'test@test.com');
  6530. git(libDir, 'config', 'user.name', 'Test');
  6531. fs.writeFileSync(path.join(libDir, 'lib.ts'), 'export const fromSubmodule = 1;');
  6532. git(libDir, 'add', '-A');
  6533. git(libDir, 'commit', '-q', '-m', 'lib init');
  6534. // Build the main repo and add the lib repo as a submodule.
  6535. const mainDir = path.join(tempDir, 'main');
  6536. fs.mkdirSync(mainDir, { recursive: true });
  6537. git(mainDir, 'init', '-q');
  6538. git(mainDir, 'config', 'user.email', 'test@test.com');
  6539. git(mainDir, 'config', 'user.name', 'Test');
  6540. fs.writeFileSync(path.join(mainDir, 'app.ts'), 'export const app = 1;');
  6541. git(mainDir, 'add', '-A');
  6542. git(mainDir, 'commit', '-q', '-m', 'app init');
  6543. // protocol.file.allow=always is required to add a local-path submodule on
  6544. // recent git versions (CVE-2022-39253 mitigation).
  6545. execFileSync(
  6546. 'git',
  6547. ['-c', 'protocol.file.allow=always', 'submodule', 'add', '-q', libDir, 'libs/lib'],
  6548. { cwd: mainDir, stdio: 'pipe' }
  6549. );
  6550. git(mainDir, 'commit', '-q', '-m', 'add submodule');
  6551. const files = scanDirectory(mainDir);
  6552. expect(files).toContain('app.ts');
  6553. expect(files).toContain('libs/lib/lib.ts');
  6554. });
  6555. });
  6556. describe('Nested gitlink repos (#1031, #1033)', () => {
  6557. let tempDir: string;
  6558. // Helper: make a self-contained git repo at `dir` with one committed TS file.
  6559. const makeRepo = async (dir: string, base: string) => {
  6560. const { execFileSync } = await import('child_process');
  6561. const git = (...args: string[]) => execFileSync('git', args, { cwd: dir, stdio: 'pipe' });
  6562. fs.mkdirSync(dir, { recursive: true });
  6563. git('init', '-q');
  6564. git('config', 'user.email', 'test@test.com');
  6565. git('config', 'user.name', 'Test');
  6566. fs.writeFileSync(path.join(dir, `${base}.ts`), `export const ${base} = 1;`);
  6567. git('add', '-A');
  6568. git('commit', '-q', '-m', `${base} init`);
  6569. };
  6570. beforeEach(() => {
  6571. tempDir = createTempDir();
  6572. });
  6573. afterEach(() => {
  6574. cleanupTempDir(tempDir);
  6575. });
  6576. // The #1031 case: a nested repo `git add`ed inside the super-repo becomes a
  6577. // gitlink (mode 160000) with NO `.gitmodules`. It is tracked (so it never shows
  6578. // in the untracked `-o` listing) yet not an active submodule (so
  6579. // `--recurse-submodules` won't expand it) — it used to fall through both passes
  6580. // and only the super-repo's own files got indexed.
  6581. it('indexes a bare gitlink (git add\'ed embedded repo, no .gitmodules), recursively', async () => {
  6582. const { execFileSync } = await import('child_process');
  6583. const git = (cwd: string, ...args: string[]) => execFileSync('git', args, { cwd, stdio: 'pipe' });
  6584. const root = path.join(tempDir, 'root');
  6585. await makeRepo(root, 'app');
  6586. // An embedded clone, itself holding a further nested clone (untracked inside it).
  6587. await makeRepo(path.join(root, 'embedded'), 'inner');
  6588. await makeRepo(path.join(root, 'embedded', 'deep'), 'deep');
  6589. // `git add embedded` records it as a 160000 gitlink (no fetch, no .gitmodules).
  6590. git(root, 'add', 'embedded');
  6591. git(root, 'commit', '-q', '-m', 'add embedded as gitlink');
  6592. expect(fs.existsSync(path.join(root, '.gitmodules'))).toBe(false);
  6593. const files = scanDirectory(root);
  6594. expect(files).toContain('app.ts');
  6595. expect(files).toContain('embedded/inner.ts'); // the gitlink's own source
  6596. expect(files).toContain('embedded/deep/deep.ts'); // recursion continues into its nested repo
  6597. });
  6598. // The -c → -s switch must not regress active submodules (#147): a repo can hold
  6599. // BOTH an active submodule (expanded by --recurse-submodules) and a bare gitlink
  6600. // (handled by the new pass), and the mixed 160000/100644 modes must parse right.
  6601. it('indexes a gitlink alongside an active submodule', async () => {
  6602. const { execFileSync } = await import('child_process');
  6603. const git = (cwd: string, ...args: string[]) => execFileSync('git', args, { cwd, stdio: 'pipe' });
  6604. const lib = path.join(tempDir, '_lib');
  6605. await makeRepo(lib, 'lib');
  6606. const root = path.join(tempDir, 'root');
  6607. await makeRepo(root, 'app');
  6608. // A proper, active submodule.
  6609. execFileSync('git', ['-c', 'protocol.file.allow=always', 'submodule', 'add', '-q', lib, 'libs/lib'], { cwd: root, stdio: 'pipe' });
  6610. git(root, 'commit', '-q', '-m', 'add submodule');
  6611. // A bare gitlink in the same repo (under a non-ignored dir name).
  6612. await makeRepo(path.join(root, 'external', 'tool'), 'tool');
  6613. git(root, 'add', 'external/tool');
  6614. git(root, 'commit', '-q', '-m', 'add gitlink');
  6615. const files = scanDirectory(root);
  6616. expect(files).toContain('app.ts');
  6617. expect(files).toContain('libs/lib/lib.ts'); // active submodule still expands (#147)
  6618. expect(files).toContain('external/tool/tool.ts'); // bare gitlink now indexed
  6619. });
  6620. // A gitlink under a built-in default-ignored directory (vendor/, node_modules/,
  6621. // …) stays excluded — a committed dependency doesn't become project code just
  6622. // because it's a nested repo. Mirrors how the untracked-embedded path treats
  6623. // the same dirs (#407), so the two passes agree.
  6624. it('does not index a gitlink under a default-ignored directory (e.g. vendor/)', async () => {
  6625. const { execFileSync } = await import('child_process');
  6626. const git = (cwd: string, ...args: string[]) => execFileSync('git', args, { cwd, stdio: 'pipe' });
  6627. const root = path.join(tempDir, 'root');
  6628. await makeRepo(root, 'app');
  6629. await makeRepo(path.join(root, 'vendor', 'pkg'), 'dep');
  6630. git(root, 'add', 'vendor/pkg');
  6631. git(root, 'commit', '-q', '-m', 'add vendored gitlink');
  6632. const files = scanDirectory(root);
  6633. expect(files).toContain('app.ts');
  6634. expect(files).not.toContain('vendor/pkg/dep.ts');
  6635. });
  6636. // A gitlink with NO working tree on disk (the common "cloned without
  6637. // --recurse-submodules" state) has nothing to index — we must leave it alone,
  6638. // not fabricate entries, and must not break the rest of the scan.
  6639. it('leaves an uninitialized submodule (no checkout on disk) alone', async () => {
  6640. const { execFileSync } = await import('child_process');
  6641. const lib = path.join(tempDir, '_lib');
  6642. await makeRepo(lib, 'lib');
  6643. const sup = path.join(tempDir, 'super');
  6644. await makeRepo(sup, 'app');
  6645. execFileSync('git', ['-c', 'protocol.file.allow=always', 'submodule', 'add', '-q', lib, 'libs/lib'], { cwd: sup, stdio: 'pipe' });
  6646. execFileSync('git', ['commit', '-q', '-m', 'add submodule'], { cwd: sup, stdio: 'pipe' });
  6647. // Clone the super-repo WITHOUT --recurse-submodules → libs/lib is an empty
  6648. // gitlink dir (mode 160000, no `.git` inside, no files).
  6649. const clone = path.join(tempDir, 'clone');
  6650. execFileSync('git', ['clone', '-q', sup, clone], { stdio: 'pipe' });
  6651. expect(fs.readdirSync(path.join(clone, 'libs', 'lib'))).toHaveLength(0);
  6652. const files = scanDirectory(clone);
  6653. expect(files).toContain('app.ts');
  6654. expect(files).not.toContain('libs/lib/lib.ts'); // not on disk → correctly absent
  6655. });
  6656. // #1065: a gitlink under a path the super-repo's OWN `.gitignore` covers is the
  6657. // tracked-gitlink twin of the untracked-ignored embedded repo (#514, #970). The
  6658. // gitlink-discovery pass must honor that `.gitignore` the same way — otherwise a
  6659. // gitignored reference/benchmark corpus full of `git add`ed clones gets pulled
  6660. // into the index (the 138k-file blow-up the reporter hit). Respect it by default;
  6661. // re-include only via `codegraph.json` `includeIgnored`.
  6662. it('does not index a gitlink under a gitignored directory by default (#1065)', async () => {
  6663. const { execFileSync } = await import('child_process');
  6664. const git = (cwd: string, ...args: string[]) => execFileSync('git', args, { cwd, stdio: 'pipe' });
  6665. const root = path.join(tempDir, 'root');
  6666. await makeRepo(root, 'app');
  6667. // An embedded clone under a path the super-repo gitignores (a benchmark corpus).
  6668. await makeRepo(path.join(root, 'benchmark', 'repos', 'ref'), 'ref');
  6669. git(root, 'add', 'benchmark/repos/ref'); // tracked as a 160000 gitlink
  6670. fs.writeFileSync(path.join(root, '.gitignore'), 'benchmark/repos/\n');
  6671. git(root, 'add', '.gitignore');
  6672. git(root, 'commit', '-q', '-m', 'add gitignored gitlink + ignore rule');
  6673. const files = scanDirectory(root);
  6674. expect(files).toContain('app.ts');
  6675. expect(files).not.toContain('benchmark/repos/ref/ref.ts'); // gitignored → excluded
  6676. // The watcher path agrees: the ignored root is never discovered, and the dir is
  6677. // pruned (the reporter's exact clue — `ignores('benchmark/repos/')` was false).
  6678. expect(discoverEmbeddedRepoRoots(root)).toEqual([]);
  6679. expect(buildScopeIgnore(root).ignores('benchmark/repos/')).toBe(true);
  6680. expect(buildScopeIgnore(root).ignores('benchmark/repos/ref/ref.ts')).toBe(true);
  6681. });
  6682. it('re-includes a gitignored gitlink when codegraph.json includeIgnored opts in (#1065)', async () => {
  6683. const { execFileSync } = await import('child_process');
  6684. const git = (cwd: string, ...args: string[]) => execFileSync('git', args, { cwd, stdio: 'pipe' });
  6685. const root = path.join(tempDir, 'root');
  6686. await makeRepo(root, 'app');
  6687. await makeRepo(path.join(root, 'benchmark', 'repos', 'ref'), 'ref');
  6688. git(root, 'add', 'benchmark/repos/ref');
  6689. fs.writeFileSync(path.join(root, '.gitignore'), 'benchmark/repos/\n');
  6690. fs.writeFileSync(path.join(root, 'codegraph.json'), JSON.stringify({ includeIgnored: ['benchmark/repos/'] }));
  6691. git(root, 'add', '.gitignore', 'codegraph.json');
  6692. git(root, 'commit', '-q', '-m', 'opt the gitignored gitlink back in');
  6693. const files = scanDirectory(root);
  6694. expect(files).toContain('app.ts');
  6695. expect(files).toContain('benchmark/repos/ref/ref.ts'); // opted in → indexed
  6696. expect(discoverEmbeddedRepoRoots(root)).toContain('benchmark/repos/ref/');
  6697. });
  6698. });
  6699. describe('Nested non-submodule git repos', () => {
  6700. let tempDir: string;
  6701. beforeEach(() => {
  6702. tempDir = createTempDir();
  6703. });
  6704. afterEach(() => {
  6705. cleanupTempDir(tempDir);
  6706. });
  6707. it('should index files in embedded git repos run from a git super-repo (issue #193)', async () => {
  6708. const { execFileSync } = await import('child_process');
  6709. const git = (cwd: string, ...args: string[]) =>
  6710. execFileSync('git', args, { cwd, stdio: 'pipe' });
  6711. // Top-level workspace is itself a git repo, holding no source directly —
  6712. // the CMake "super-repo" layout from the issue.
  6713. const root = path.join(tempDir, 'root');
  6714. fs.mkdirSync(path.join(root, 'coding'), { recursive: true });
  6715. git(root, 'init', '-q');
  6716. git(root, 'config', 'user.email', 'test@test.com');
  6717. git(root, 'config', 'user.name', 'Test');
  6718. fs.writeFileSync(path.join(root, 'CMakeLists.txt'), 'cmake_minimum_required(VERSION 3.10)\n');
  6719. // Two independent clones living inside the workspace (NOT submodules):
  6720. // one with committed source, one with only untracked source.
  6721. const sub1 = path.join(root, 'sub_repo1', 'src');
  6722. fs.mkdirSync(sub1, { recursive: true });
  6723. git(path.join(root, 'sub_repo1'), 'init', '-q');
  6724. git(path.join(root, 'sub_repo1'), 'config', 'user.email', 'test@test.com');
  6725. git(path.join(root, 'sub_repo1'), 'config', 'user.name', 'Test');
  6726. fs.writeFileSync(path.join(sub1, 'one.ts'), 'export const one = 1;');
  6727. git(path.join(root, 'sub_repo1'), 'add', '-A');
  6728. git(path.join(root, 'sub_repo1'), 'commit', '-q', '-m', 'sub1 init');
  6729. const sub2 = path.join(root, 'sub_repo2', 'src');
  6730. fs.mkdirSync(sub2, { recursive: true });
  6731. git(path.join(root, 'sub_repo2'), 'init', '-q');
  6732. fs.writeFileSync(path.join(sub2, 'two.ts'), 'export const two = 2;');
  6733. const files = scanDirectory(root);
  6734. // Both committed and untracked source from the nested repos must be found.
  6735. expect(files).toContain('sub_repo1/src/one.ts');
  6736. expect(files).toContain('sub_repo2/src/two.ts');
  6737. });
  6738. it('should respect each embedded repo\'s own .gitignore', async () => {
  6739. const { execFileSync } = await import('child_process');
  6740. const git = (cwd: string, ...args: string[]) =>
  6741. execFileSync('git', args, { cwd, stdio: 'pipe' });
  6742. const root = path.join(tempDir, 'root');
  6743. fs.mkdirSync(root, { recursive: true });
  6744. git(root, 'init', '-q');
  6745. const sub = path.join(root, 'sub_repo', 'src');
  6746. fs.mkdirSync(sub, { recursive: true });
  6747. git(path.join(root, 'sub_repo'), 'init', '-q');
  6748. fs.writeFileSync(path.join(root, 'sub_repo', '.gitignore'), 'src/generated.ts\n');
  6749. fs.writeFileSync(path.join(sub, 'real.ts'), 'export const real = 1;');
  6750. fs.writeFileSync(path.join(sub, 'generated.ts'), 'export const generated = 1;');
  6751. const files = scanDirectory(root);
  6752. expect(files).toContain('sub_repo/src/real.ts');
  6753. expect(files).not.toContain('sub_repo/src/generated.ts');
  6754. });
  6755. // A .gitignore the `ignore` library can't compile to a regex must not abort
  6756. // the whole scan — the bad pattern is dropped, valid ones still apply (#682).
  6757. it('does not crash on a .gitignore with an uncompilable pattern (#682)', () => {
  6758. fs.mkdirSync(path.join(tempDir, 'src'), { recursive: true });
  6759. fs.mkdirSync(path.join(tempDir, 'build'), { recursive: true });
  6760. fs.writeFileSync(path.join(tempDir, 'src', 'real.ts'), 'export const x = 1;');
  6761. fs.writeFileSync(path.join(tempDir, 'build', 'out.ts'), 'export const y = 2;');
  6762. // `\\[` makes the matcher build an unterminated character class — the throw
  6763. // is lazy (at match time), which is what escaped and killed sync.
  6764. fs.writeFileSync(path.join(tempDir, '.gitignore'), 'build/\n\\\\[\n');
  6765. let files: string[] = [];
  6766. expect(() => {
  6767. files = scanDirectory(tempDir);
  6768. }).not.toThrow();
  6769. expect(files).toContain('src/real.ts');
  6770. // The still-valid `build/` rule is honored; only the bad line was dropped.
  6771. expect(files.some((f) => f.startsWith('build/'))).toBe(false);
  6772. });
  6773. // A .gitignore that isn't valid UTF-8 — e.g. encrypted in place by corporate
  6774. // DLP / endpoint software (UTF-16 header + ciphertext) — is skipped whole,
  6775. // not fed to the matcher as garbage patterns (#682).
  6776. it('does not crash on a non-UTF-8 (DLP-encrypted) .gitignore (#682)', () => {
  6777. fs.mkdirSync(path.join(tempDir, 'src'), { recursive: true });
  6778. fs.writeFileSync(path.join(tempDir, 'src', 'real.ts'), 'export const x = 1;');
  6779. const header = Buffer.concat([
  6780. Buffer.from([0x00, 0x00]),
  6781. Buffer.from('[notice][user]', 'utf16le'),
  6782. ]);
  6783. const junk = Buffer.from([0x5b, 0x99, 0xc3, 0x28, 0x5c, 0x5b, 0xff, 0xfd]);
  6784. fs.writeFileSync(path.join(tempDir, '.gitignore'), Buffer.concat([header, junk]));
  6785. let files: string[] = [];
  6786. expect(() => {
  6787. files = scanDirectory(tempDir);
  6788. }).not.toThrow();
  6789. expect(files).toContain('src/real.ts');
  6790. });
  6791. it('buildDefaultIgnore survives a bad .gitignore and still applies valid rules (#682)', () => {
  6792. fs.writeFileSync(path.join(tempDir, '.gitignore'), 'dist/\n\\\\[\n');
  6793. const ig = buildDefaultIgnore(tempDir);
  6794. expect(() => ig.ignores('src/app.ts')).not.toThrow();
  6795. expect(ig.ignores('dist/')).toBe(true); // valid rule survives
  6796. expect(ig.ignores('src/app.ts')).toBe(false);
  6797. });
  6798. it('buildDefaultIgnore honors .git/info/exclude (#1728)', async () => {
  6799. const { execFileSync } = await import('child_process');
  6800. const git = (cwd: string, ...args: string[]) =>
  6801. execFileSync('git', args, { cwd, stdio: 'pipe' });
  6802. const root = path.join(tempDir, 'exclude-root');
  6803. fs.mkdirSync(root, { recursive: true });
  6804. git(root, 'init', '-q');
  6805. fs.writeFileSync(path.join(root, 'src.ts'), 'export const x = 1;\n');
  6806. fs.mkdirSync(path.join(root, '.claude', 'worktrees', 'agent-1'), { recursive: true });
  6807. fs.writeFileSync(
  6808. path.join(root, '.claude', 'worktrees', 'agent-1', 'src.ts'),
  6809. 'export const w = 1;\n',
  6810. );
  6811. // Not in .gitignore — only in info/exclude (the reporter's exact shape).
  6812. fs.writeFileSync(
  6813. path.join(root, '.git', 'info', 'exclude'),
  6814. '**/.claude/worktrees/\n',
  6815. );
  6816. const ig = buildDefaultIgnore(root);
  6817. expect(ig.ignores('src.ts')).toBe(false);
  6818. expect(ig.ignores('.claude/worktrees/agent-1/src.ts')).toBe(true);
  6819. expect(ig.ignores('.claude/worktrees/')).toBe(true);
  6820. // ScopeIgnore (watcher path) agrees, including via git ignored-dir seeding.
  6821. const scope = buildScopeIgnore(root);
  6822. expect(scope.ignores('src.ts')).toBe(false);
  6823. expect(scope.ignores('.claude/worktrees/agent-1/')).toBe(true);
  6824. expect(scope.ignores('.claude/worktrees/agent-1/src.ts')).toBe(true);
  6825. });
  6826. it('buildDefaultIgnore honors core.excludesFile (#1728)', async () => {
  6827. const { execFileSync } = await import('child_process');
  6828. const git = (cwd: string, ...args: string[]) =>
  6829. execFileSync('git', args, { cwd, stdio: 'pipe' });
  6830. const root = path.join(tempDir, 'excludesfile-root');
  6831. fs.mkdirSync(root, { recursive: true });
  6832. git(root, 'init', '-q');
  6833. const globalExcludes = path.join(tempDir, 'global-excludes');
  6834. fs.writeFileSync(globalExcludes, 'scratch/\n');
  6835. git(root, 'config', 'core.excludesFile', globalExcludes);
  6836. fs.mkdirSync(path.join(root, 'scratch'), { recursive: true });
  6837. fs.writeFileSync(path.join(root, 'scratch', 'tmp.ts'), 'export const t = 1;\n');
  6838. fs.writeFileSync(path.join(root, 'app.ts'), 'export const a = 1;\n');
  6839. const ig = buildDefaultIgnore(root);
  6840. expect(ig.ignores('app.ts')).toBe(false);
  6841. expect(ig.ignores('scratch/')).toBe(true);
  6842. expect(ig.ignores('scratch/tmp.ts')).toBe(true);
  6843. });
  6844. it('buildScopeIgnore prunes dirs ignored only by a nested .gitignore (#1728)', async () => {
  6845. const { execFileSync } = await import('child_process');
  6846. const git = (cwd: string, ...args: string[]) =>
  6847. execFileSync('git', args, { cwd, stdio: 'pipe' });
  6848. const root = path.join(tempDir, 'nested-gi-root');
  6849. fs.mkdirSync(path.join(root, 'pkg', 'build'), { recursive: true });
  6850. git(root, 'init', '-q');
  6851. git(root, 'config', 'user.email', 'test@test.com');
  6852. git(root, 'config', 'user.name', 'Test');
  6853. fs.writeFileSync(path.join(root, 'pkg', 'app.ts'), 'export const a = 1;\n');
  6854. fs.writeFileSync(path.join(root, 'pkg', 'build', 'out.ts'), 'export const o = 1;\n');
  6855. fs.writeFileSync(path.join(root, 'pkg', '.gitignore'), 'build/\n');
  6856. // Commit only the non-ignored file so git still reports build/ as ignored-other.
  6857. git(root, 'add', 'pkg/app.ts', 'pkg/.gitignore');
  6858. git(root, 'commit', '-q', '-m', 'init');
  6859. const scope = buildScopeIgnore(root);
  6860. expect(scope.ignores('pkg/app.ts')).toBe(false);
  6861. expect(scope.ignores('pkg/build/')).toBe(true);
  6862. expect(scope.ignores('pkg/build/out.ts')).toBe(true);
  6863. });
  6864. });
  6865. // =============================================================================
  6866. // Scala
  6867. // =============================================================================
  6868. describe('Scala Extraction', () => {
  6869. describe('Language detection', () => {
  6870. it('should detect Scala files', () => {
  6871. expect(detectLanguage('Main.scala')).toBe('scala');
  6872. expect(detectLanguage('script.sc')).toBe('scala');
  6873. expect(detectLanguage('src/UserService.scala')).toBe('scala');
  6874. });
  6875. it('should report Scala as supported', () => {
  6876. expect(isLanguageSupported('scala')).toBe(true);
  6877. expect(getSupportedLanguages()).toContain('scala');
  6878. });
  6879. });
  6880. describe('Class extraction', () => {
  6881. it('should extract class definitions', () => {
  6882. const code = `
  6883. class UserService(private val repo: UserRepository) {
  6884. def findUser(id: String): Option[String] = Some(id)
  6885. }
  6886. `;
  6887. const result = extractFromSource('UserService.scala', code);
  6888. const cls = result.nodes.find((n) => n.kind === 'class' && n.name === 'UserService');
  6889. expect(cls).toBeDefined();
  6890. expect(cls?.language).toBe('scala');
  6891. });
  6892. it('should extract object definitions as class kind', () => {
  6893. const code = `
  6894. object DatabaseConfig {
  6895. val url = "jdbc:postgresql://localhost/mydb"
  6896. }
  6897. `;
  6898. const result = extractFromSource('Config.scala', code);
  6899. const obj = result.nodes.find((n) => n.kind === 'class' && n.name === 'DatabaseConfig');
  6900. expect(obj).toBeDefined();
  6901. });
  6902. it('should extract trait definitions as trait kind', () => {
  6903. const code = `
  6904. trait Repository[A] {
  6905. def findById(id: String): Option[A]
  6906. def save(entity: A): Unit
  6907. }
  6908. `;
  6909. const result = extractFromSource('Repository.scala', code);
  6910. const trait_ = result.nodes.find((n) => n.kind === 'trait' && n.name === 'Repository');
  6911. expect(trait_).toBeDefined();
  6912. });
  6913. });
  6914. describe('Method and function extraction', () => {
  6915. it('should extract method definitions inside a class', () => {
  6916. const code = `
  6917. class Calculator {
  6918. def add(a: Int, b: Int): Int = a + b
  6919. def divide(a: Double, b: Double): Double = a / b
  6920. }
  6921. `;
  6922. const result = extractFromSource('Calculator.scala', code);
  6923. const methods = result.nodes.filter((n) => n.kind === 'method');
  6924. expect(methods.find((m) => m.name === 'add')).toBeDefined();
  6925. expect(methods.find((m) => m.name === 'divide')).toBeDefined();
  6926. });
  6927. it('should extract method signatures', () => {
  6928. const code = `
  6929. class Greeter {
  6930. def greet(name: String): String = s"Hello, \${name}!"
  6931. }
  6932. `;
  6933. const result = extractFromSource('Greeter.scala', code);
  6934. const method = result.nodes.find((n) => n.name === 'greet');
  6935. expect(method?.signature).toContain('name: String');
  6936. expect(method?.signature).toContain('String');
  6937. });
  6938. it('should extract top-level function definitions as functions', () => {
  6939. const code = `
  6940. def factorial(n: Int): Int = if (n <= 1) 1 else n * factorial(n - 1)
  6941. def greet(name: String): String = s"Hello, \${name}!"
  6942. `;
  6943. const result = extractFromSource('utils.scala', code);
  6944. const fns = result.nodes.filter((n) => n.kind === 'function');
  6945. expect(fns.find((f) => f.name === 'factorial')).toBeDefined();
  6946. expect(fns.find((f) => f.name === 'greet')).toBeDefined();
  6947. });
  6948. });
  6949. describe('Val and var extraction', () => {
  6950. it('should extract val inside a class as field', () => {
  6951. const code = `
  6952. class Config {
  6953. val timeout: Int = 30
  6954. val host: String = "localhost"
  6955. }
  6956. `;
  6957. const result = extractFromSource('Config.scala', code);
  6958. const fields = result.nodes.filter((n) => n.kind === 'field');
  6959. expect(fields.find((f) => f.name === 'timeout')).toBeDefined();
  6960. expect(fields.find((f) => f.name === 'host')).toBeDefined();
  6961. });
  6962. it('should extract var inside a class as field', () => {
  6963. const code = `
  6964. class Counter {
  6965. var count: Int = 0
  6966. }
  6967. `;
  6968. const result = extractFromSource('Counter.scala', code);
  6969. const field = result.nodes.find((n) => n.kind === 'field' && n.name === 'count');
  6970. expect(field).toBeDefined();
  6971. });
  6972. it('should extract top-level val as constant', () => {
  6973. const code = `
  6974. val MaxConnections: Int = 100
  6975. val DefaultTimeout = 30
  6976. `;
  6977. const result = extractFromSource('constants.scala', code);
  6978. const consts = result.nodes.filter((n) => n.kind === 'constant');
  6979. expect(consts.find((c) => c.name === 'MaxConnections')).toBeDefined();
  6980. });
  6981. it('should extract top-level var as variable', () => {
  6982. const code = `
  6983. var retries: Int = 3
  6984. `;
  6985. const result = extractFromSource('state.scala', code);
  6986. const v = result.nodes.find((n) => n.kind === 'variable' && n.name === 'retries');
  6987. expect(v).toBeDefined();
  6988. });
  6989. it('should include type in val/var signature', () => {
  6990. const code = `
  6991. class Service {
  6992. val timeout: Int = 30
  6993. }
  6994. `;
  6995. const result = extractFromSource('Service.scala', code);
  6996. const field = result.nodes.find((n) => n.name === 'timeout');
  6997. expect(field?.signature).toContain('timeout');
  6998. expect(field?.signature).toContain('Int');
  6999. });
  7000. });
  7001. describe('Enum extraction', () => {
  7002. it('should extract enum definitions', () => {
  7003. const code = `
  7004. enum Color:
  7005. case Red
  7006. case Green
  7007. case Blue
  7008. `;
  7009. const result = extractFromSource('Color.scala', code);
  7010. const enumNode = result.nodes.find((n) => n.kind === 'enum' && n.name === 'Color');
  7011. expect(enumNode).toBeDefined();
  7012. });
  7013. it('should extract enum cases as enum_member', () => {
  7014. const code = `
  7015. enum Direction:
  7016. case North
  7017. case South
  7018. case East
  7019. case West
  7020. `;
  7021. const result = extractFromSource('Direction.scala', code);
  7022. const members = result.nodes.filter((n) => n.kind === 'enum_member');
  7023. expect(members.find((m) => m.name === 'North')).toBeDefined();
  7024. expect(members.find((m) => m.name === 'South')).toBeDefined();
  7025. expect(members.length).toBeGreaterThanOrEqual(4);
  7026. });
  7027. });
  7028. describe('Type alias extraction', () => {
  7029. it('should extract type aliases', () => {
  7030. const code = `
  7031. type UserId = String
  7032. type UserMap = Map[String, String]
  7033. `;
  7034. const result = extractFromSource('types.scala', code);
  7035. const aliases = result.nodes.filter((n) => n.kind === 'type_alias');
  7036. expect(aliases.find((a) => a.name === 'UserId')).toBeDefined();
  7037. expect(aliases.find((a) => a.name === 'UserMap')).toBeDefined();
  7038. });
  7039. });
  7040. describe('Import extraction', () => {
  7041. it('should extract import declarations', () => {
  7042. const code = `
  7043. import scala.collection.mutable.ListBuffer
  7044. import scala.concurrent.Future
  7045. `;
  7046. const result = extractFromSource('imports.scala', code);
  7047. const imports = result.nodes.filter((n) => n.kind === 'import');
  7048. expect(imports.length).toBeGreaterThanOrEqual(2);
  7049. });
  7050. });
  7051. describe('Visibility modifiers', () => {
  7052. it('should extract private visibility', () => {
  7053. const code = `
  7054. class Service {
  7055. private val secret: String = "abc"
  7056. private def helper(): Unit = {}
  7057. }
  7058. `;
  7059. const result = extractFromSource('Service.scala', code);
  7060. const secretField = result.nodes.find((n) => n.name === 'secret');
  7061. expect(secretField?.visibility).toBe('private');
  7062. const helperMethod = result.nodes.find((n) => n.name === 'helper');
  7063. expect(helperMethod?.visibility).toBe('private');
  7064. });
  7065. it('should extract protected visibility', () => {
  7066. const code = `
  7067. class Base {
  7068. protected def helperMethod(): Unit = {}
  7069. }
  7070. `;
  7071. const result = extractFromSource('Base.scala', code);
  7072. const method = result.nodes.find((n) => n.name === 'helperMethod');
  7073. expect(method?.visibility).toBe('protected');
  7074. });
  7075. it('should default to public visibility', () => {
  7076. const code = `
  7077. class Greeter {
  7078. def hello(): Unit = {}
  7079. }
  7080. `;
  7081. const result = extractFromSource('Greeter.scala', code);
  7082. const method = result.nodes.find((n) => n.name === 'hello');
  7083. expect(method?.visibility).toBe('public');
  7084. });
  7085. });
  7086. describe('Inheritance', () => {
  7087. it('should extract extends relationships', () => {
  7088. const code = `
  7089. class AdminUser extends User {
  7090. def adminAction(): Unit = {}
  7091. }
  7092. `;
  7093. const result = extractFromSource('AdminUser.scala', code);
  7094. const extendsRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'extends');
  7095. expect(extendsRefs.find((r) => r.referenceName === 'User')).toBeDefined();
  7096. });
  7097. });
  7098. describe('Call extraction', () => {
  7099. it('should extract function call expressions', () => {
  7100. const code = `
  7101. def processData(): Unit = {
  7102. val result = computeResult()
  7103. println(result)
  7104. }
  7105. `;
  7106. const result = extractFromSource('processor.scala', code);
  7107. const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls');
  7108. expect(calls.length).toBeGreaterThan(0);
  7109. });
  7110. });
  7111. });
  7112. describe('Vue Extraction', () => {
  7113. it('should detect Vue files', () => {
  7114. expect(detectLanguage('App.vue')).toBe('vue');
  7115. expect(detectLanguage('components/Button.vue')).toBe('vue');
  7116. expect(isLanguageSupported('vue')).toBe(true);
  7117. });
  7118. it('should extract component node from a Vue SFC', () => {
  7119. const code = `<template>
  7120. <div>{{ message }}</div>
  7121. </template>
  7122. <script>
  7123. export default {
  7124. data() {
  7125. return { message: 'Hello' };
  7126. }
  7127. }
  7128. </script>
  7129. `;
  7130. const result = extractFromSource('HelloWorld.vue', code);
  7131. const componentNode = result.nodes.find((n) => n.kind === 'component');
  7132. expect(componentNode).toBeDefined();
  7133. expect(componentNode?.name).toBe('HelloWorld');
  7134. expect(componentNode?.language).toBe('vue');
  7135. expect(componentNode?.isExported).toBe(true);
  7136. });
  7137. it('should extract functions from <script> block', () => {
  7138. const code = `<template>
  7139. <button @click="handleClick">Click</button>
  7140. </template>
  7141. <script>
  7142. function handleClick() {
  7143. console.log('clicked');
  7144. }
  7145. const count = 0;
  7146. </script>
  7147. `;
  7148. const result = extractFromSource('Button.vue', code);
  7149. const componentNode = result.nodes.find((n) => n.kind === 'component');
  7150. expect(componentNode).toBeDefined();
  7151. expect(componentNode?.name).toBe('Button');
  7152. const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'handleClick');
  7153. expect(funcNode).toBeDefined();
  7154. expect(funcNode?.language).toBe('vue');
  7155. });
  7156. it('should extract from <script setup lang="ts"> block', () => {
  7157. const code = `<template>
  7158. <div>{{ count }}</div>
  7159. </template>
  7160. <script setup lang="ts">
  7161. import { ref } from 'vue';
  7162. const count = ref(0);
  7163. function increment(): void {
  7164. count.value++;
  7165. }
  7166. </script>
  7167. `;
  7168. const result = extractFromSource('Counter.vue', code);
  7169. const componentNode = result.nodes.find((n) => n.kind === 'component');
  7170. expect(componentNode).toBeDefined();
  7171. expect(componentNode?.name).toBe('Counter');
  7172. const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'increment');
  7173. expect(funcNode).toBeDefined();
  7174. expect(funcNode?.language).toBe('vue');
  7175. // All nodes should be marked as vue language
  7176. for (const node of result.nodes) {
  7177. expect(node.language).toBe('vue');
  7178. }
  7179. });
  7180. it('should extract calls from top-level <script setup> initializers', () => {
  7181. const code = `<template>
  7182. <div>{{ token }}</div>
  7183. </template>
  7184. <script setup lang="ts">
  7185. import { getTokenMp } from './api/upload';
  7186. const token = getTokenMp();
  7187. </script>
  7188. `;
  7189. const result = extractFromSource('Issue425Setup.vue', code);
  7190. const call = result.unresolvedReferences.find(
  7191. (ref) => ref.referenceKind === 'calls' && ref.referenceName === 'getTokenMp'
  7192. );
  7193. expect(call).toBeDefined();
  7194. });
  7195. it('should extract calls from Vue Options API object methods', () => {
  7196. const code = `<template>
  7197. <button @click="save">Save</button>
  7198. </template>
  7199. <script>
  7200. import { getTokenMp } from './api/upload';
  7201. export default {
  7202. methods: {
  7203. save() {
  7204. return getTokenMp();
  7205. }
  7206. },
  7207. setup() {
  7208. return getTokenMp();
  7209. }
  7210. }
  7211. </script>
  7212. `;
  7213. const result = extractFromSource('Issue425Options.vue', code);
  7214. const calls = result.unresolvedReferences.filter(
  7215. (ref) => ref.referenceKind === 'calls' && ref.referenceName === 'getTokenMp'
  7216. );
  7217. expect(calls).toHaveLength(2);
  7218. });
  7219. it('should extract component usages from the Vue template (PascalCase + kebab, skipping built-ins) (#629)', () => {
  7220. const code = `<template>
  7221. <div class="wrap">
  7222. <UserCard :user="u" />
  7223. <my-button>Click</my-button>
  7224. <Transition><span>x</span></Transition>
  7225. </div>
  7226. </template>
  7227. <script setup lang="ts">
  7228. import UserCard from './UserCard.vue';
  7229. import MyButton from './MyButton.vue';
  7230. </script>
  7231. `;
  7232. const result = extractFromSource('Host.vue', code);
  7233. const refs = result.unresolvedReferences
  7234. .filter((r) => r.referenceKind === 'references')
  7235. .map((r) => r.referenceName);
  7236. expect(refs).toContain('UserCard'); // PascalCase tag
  7237. expect(refs).toContain('MyButton'); // kebab <my-button> → MyButton
  7238. expect(refs).not.toContain('Transition'); // Vue built-in skipped
  7239. expect(refs).not.toContain('Div'); // native HTML element skipped
  7240. expect(refs).not.toContain('Span');
  7241. });
  7242. it('should extract from both <script> and <script setup> blocks', () => {
  7243. const code = `<template>
  7244. <div>{{ msg }}</div>
  7245. </template>
  7246. <script>
  7247. export default {
  7248. name: 'DualScript'
  7249. }
  7250. </script>
  7251. <script setup>
  7252. const msg = 'hello';
  7253. function greet() {
  7254. return msg;
  7255. }
  7256. </script>
  7257. `;
  7258. const result = extractFromSource('DualScript.vue', code);
  7259. const componentNode = result.nodes.find((n) => n.kind === 'component');
  7260. expect(componentNode).toBeDefined();
  7261. const greetFunc = result.nodes.find((n) => n.kind === 'function' && n.name === 'greet');
  7262. expect(greetFunc).toBeDefined();
  7263. });
  7264. it('should create component node for template-only Vue file', () => {
  7265. const code = `<template>
  7266. <div>Static content</div>
  7267. </template>
  7268. `;
  7269. const result = extractFromSource('Static.vue', code);
  7270. const componentNode = result.nodes.find((n) => n.kind === 'component');
  7271. expect(componentNode).toBeDefined();
  7272. expect(componentNode?.name).toBe('Static');
  7273. expect(componentNode?.language).toBe('vue');
  7274. // Only the component node should exist (no script nodes)
  7275. expect(result.nodes.length).toBe(1);
  7276. });
  7277. it('should create containment edges from component to script nodes', () => {
  7278. const code = `<template>
  7279. <div>{{ value }}</div>
  7280. </template>
  7281. <script setup lang="ts">
  7282. const value = 42;
  7283. </script>
  7284. `;
  7285. const result = extractFromSource('Contained.vue', code);
  7286. const componentNode = result.nodes.find((n) => n.kind === 'component');
  7287. expect(componentNode).toBeDefined();
  7288. // Should have containment edges from component to child nodes
  7289. const containEdges = result.edges.filter(
  7290. (e) => e.source === componentNode!.id && e.kind === 'contains'
  7291. );
  7292. expect(containEdges.length).toBeGreaterThan(0);
  7293. });
  7294. });
  7295. describe('Astro Extraction', () => {
  7296. it('should detect Astro files', () => {
  7297. expect(detectLanguage('src/pages/index.astro')).toBe('astro');
  7298. expect(detectLanguage('Layout.astro')).toBe('astro');
  7299. expect(isLanguageSupported('astro')).toBe(true);
  7300. });
  7301. it('should extract component node from an .astro file', () => {
  7302. const code = `---
  7303. const title = 'Hello';
  7304. ---
  7305. <h1>{title}</h1>
  7306. `;
  7307. const result = extractFromSource('Card.astro', code);
  7308. const componentNode = result.nodes.find((n) => n.kind === 'component');
  7309. expect(componentNode).toBeDefined();
  7310. expect(componentNode?.name).toBe('Card');
  7311. expect(componentNode?.language).toBe('astro');
  7312. expect(componentNode?.isExported).toBe(true);
  7313. });
  7314. it('should extract frontmatter symbols with correct line numbers (#768)', () => {
  7315. const code = `---
  7316. import { formatDate } from '../utils/format';
  7317. function getIconNode(name: string): string {
  7318. return name;
  7319. }
  7320. const { title } = Astro.props;
  7321. ---
  7322. <span>{title}</span>
  7323. `;
  7324. const result = extractFromSource('navs.astro', code);
  7325. // The #768 repro: a function defined in frontmatter must be found
  7326. const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'getIconNode');
  7327. expect(fn).toBeDefined();
  7328. expect(fn?.language).toBe('astro');
  7329. expect(fn?.startLine).toBe(4);
  7330. const imp = result.nodes.find((n) => n.kind === 'import');
  7331. expect(imp).toBeDefined();
  7332. expect(imp?.startLine).toBe(2);
  7333. });
  7334. it('should extract exported getStaticPaths from frontmatter', () => {
  7335. const code = `---
  7336. export async function getStaticPaths() {
  7337. return [];
  7338. }
  7339. const { slug } = Astro.params;
  7340. ---
  7341. <p>{slug}</p>
  7342. `;
  7343. const result = extractFromSource('[slug].astro', code);
  7344. const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'getStaticPaths');
  7345. expect(fn).toBeDefined();
  7346. expect(fn?.isExported).toBe(true);
  7347. });
  7348. it('should extract calls from template expressions', () => {
  7349. const code = `---
  7350. import { formatDate } from '../utils/format';
  7351. const date = new Date();
  7352. ---
  7353. <time>{formatDate(date)}</time>
  7354. `;
  7355. const result = extractFromSource('Stamp.astro', code);
  7356. const call = result.unresolvedReferences.find(
  7357. (ref) => ref.referenceKind === 'calls' && ref.referenceName === 'formatDate' && ref.line === 5
  7358. );
  7359. expect(call).toBeDefined();
  7360. });
  7361. it('should extract calls from a multiline expression opening line', () => {
  7362. const code = `---
  7363. const posts = [];
  7364. ---
  7365. <ul>
  7366. {posts.map((post) => (
  7367. <li>{render(post)}</li>
  7368. ))}
  7369. </ul>
  7370. `;
  7371. const result = extractFromSource('List.astro', code);
  7372. const mapCall = result.unresolvedReferences.find(
  7373. (ref) => ref.referenceKind === 'calls' && ref.referenceName === 'posts.map'
  7374. );
  7375. expect(mapCall).toBeDefined();
  7376. const innerCall = result.unresolvedReferences.find(
  7377. (ref) => ref.referenceKind === 'calls' && ref.referenceName === 'render'
  7378. );
  7379. expect(innerCall).toBeDefined();
  7380. });
  7381. it('should extract PascalCase component usages from the template', () => {
  7382. const code = `---
  7383. import Layout from '../layouts/Layout.astro';
  7384. import PostCard from '../components/PostCard.astro';
  7385. ---
  7386. <Layout title="Home">
  7387. <PostCard />
  7388. <Fragment slot="head" />
  7389. <div class="plain-html" />
  7390. </Layout>
  7391. `;
  7392. const result = extractFromSource('index.astro', code);
  7393. const refs = result.unresolvedReferences.filter((r) => r.referenceKind === 'references');
  7394. const names = refs.map((r) => r.referenceName);
  7395. expect(names).toContain('Layout');
  7396. expect(names).toContain('PostCard');
  7397. // Astro built-ins and lowercase HTML are not component references
  7398. expect(names).not.toContain('Fragment');
  7399. expect(names).not.toContain('div');
  7400. });
  7401. it('should not extract template patterns from frontmatter, script, or style content', () => {
  7402. const code = `---
  7403. // <FakeComponent /> inside frontmatter comment
  7404. const x = { y: maybeCall(1) };
  7405. ---
  7406. <div>real</div>
  7407. <script>
  7408. const z = { w: scriptCall(2) };
  7409. </script>
  7410. <style>
  7411. .a { color: red; }
  7412. </style>
  7413. `;
  7414. const result = extractFromSource('Guard.astro', code);
  7415. const templateRefs = result.unresolvedReferences.filter(
  7416. (r) => r.referenceKind === 'references' && r.referenceName === 'FakeComponent'
  7417. );
  7418. expect(templateRefs).toHaveLength(0);
  7419. // maybeCall/scriptCall come from the delegated TS extraction (once),
  7420. // not double-counted by the template scanner
  7421. const maybeCalls = result.unresolvedReferences.filter(
  7422. (r) => r.referenceName === 'maybeCall' && r.referenceKind === 'calls'
  7423. );
  7424. expect(maybeCalls.length).toBeLessThanOrEqual(1);
  7425. });
  7426. it('should extract <script> block symbols with correct line numbers', () => {
  7427. const code = `---
  7428. const a = 1;
  7429. ---
  7430. <div>hi</div>
  7431. <script>
  7432. function trackView(page: string) {
  7433. console.log(page);
  7434. }
  7435. </script>
  7436. `;
  7437. const result = extractFromSource('Tracker.astro', code);
  7438. const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'trackView');
  7439. expect(fn).toBeDefined();
  7440. expect(fn?.startLine).toBe(6);
  7441. expect(fn?.language).toBe('astro');
  7442. });
  7443. it('should create component node for a frontmatter-less template-only file', () => {
  7444. const code = `<div>Static content</div>
  7445. `;
  7446. const result = extractFromSource('Static.astro', code);
  7447. const componentNode = result.nodes.find((n) => n.kind === 'component');
  7448. expect(componentNode).toBeDefined();
  7449. expect(componentNode?.name).toBe('Static');
  7450. expect(componentNode?.language).toBe('astro');
  7451. });
  7452. it('should treat an unclosed frontmatter fence as no frontmatter', () => {
  7453. const code = `---
  7454. const broken = true;
  7455. <div>never closed</div>
  7456. `;
  7457. const result = extractFromSource('Broken.astro', code);
  7458. // No TS delegation happened (the fence never closes), but the component
  7459. // node still exists and nothing throws.
  7460. const componentNode = result.nodes.find((n) => n.kind === 'component');
  7461. expect(componentNode).toBeDefined();
  7462. expect(result.nodes.find((n) => n.name === 'broken')).toBeUndefined();
  7463. });
  7464. it('should create containment edges from component to frontmatter nodes', () => {
  7465. const code = `---
  7466. const value = 42;
  7467. ---
  7468. <div>{value}</div>
  7469. `;
  7470. const result = extractFromSource('Contained.astro', code);
  7471. const componentNode = result.nodes.find((n) => n.kind === 'component');
  7472. expect(componentNode).toBeDefined();
  7473. const containEdges = result.edges.filter(
  7474. (e) => e.source === componentNode!.id && e.kind === 'contains'
  7475. );
  7476. expect(containEdges.length).toBeGreaterThan(0);
  7477. });
  7478. });
  7479. describe('Instantiates + Decorates edge extraction', () => {
  7480. it('emits an instantiates ref for `new Foo()`', () => {
  7481. const code = `
  7482. class Foo {}
  7483. function bootstrap() { return new Foo(); }
  7484. `;
  7485. const result = extractFromSource('app.ts', code);
  7486. const ref = result.unresolvedReferences.find(
  7487. (r) => r.referenceKind === 'instantiates' && r.referenceName === 'Foo'
  7488. );
  7489. expect(ref).toBeDefined();
  7490. });
  7491. it('strips type-argument suffix from generic constructors', () => {
  7492. const code = `
  7493. class Container<T> { constructor(_: T) {} }
  7494. function go() { return new Container<string>('x'); }
  7495. `;
  7496. const result = extractFromSource('app.ts', code);
  7497. const ref = result.unresolvedReferences.find(
  7498. (r) => r.referenceKind === 'instantiates'
  7499. );
  7500. expect(ref).toBeDefined();
  7501. // Container<string> must be normalised to "Container" — otherwise
  7502. // resolution can never match the class node.
  7503. expect(ref!.referenceName).toBe('Container');
  7504. });
  7505. it('keeps trailing identifier from qualified `new ns.Foo()`', () => {
  7506. const code = `
  7507. const ns = { Foo: class {} };
  7508. function go() { return new ns.Foo(); }
  7509. `;
  7510. const result = extractFromSource('app.ts', code);
  7511. const ref = result.unresolvedReferences.find(
  7512. (r) => r.referenceKind === 'instantiates'
  7513. );
  7514. // We can't always resolve which Foo, but the name should be the
  7515. // simple identifier so name-matching has a chance.
  7516. expect(ref?.referenceName).toBe('Foo');
  7517. });
  7518. it('emits a decorates ref for `@Foo class X {}`', () => {
  7519. const code = `
  7520. function Foo(_arg: string) { return (cls: any) => cls; }
  7521. @Foo('x')
  7522. class X {}
  7523. `;
  7524. const result = extractFromSource('app.ts', code);
  7525. const decorClass = result.unresolvedReferences.find(
  7526. (r) => r.referenceKind === 'decorates' && r.referenceName === 'Foo'
  7527. );
  7528. expect(decorClass).toBeDefined();
  7529. });
  7530. it('does NOT attribute a prior class\'s decorator to the next class', () => {
  7531. // Regression: the sibling-walk must stop at the first non-
  7532. // decorator separator. `@A class Foo {} @B class Bar {}` must
  7533. // produce `decorates(Foo, A)` and `decorates(Bar, B)` — never
  7534. // `decorates(Bar, A)`.
  7535. const code = `
  7536. function A(cls: any) { return cls; }
  7537. function B(cls: any) { return cls; }
  7538. @A
  7539. class Foo {}
  7540. @B
  7541. class Bar {}
  7542. `;
  7543. const result = extractFromSource('app.ts', code);
  7544. const decoratesEdges = result.unresolvedReferences.filter(
  7545. (r) => r.referenceKind === 'decorates'
  7546. );
  7547. // Exactly one decorates ref per decorated class, no cross-attribution.
  7548. const fromBar = decoratesEdges.filter((r) =>
  7549. result.nodes.find((n) => n.id === r.fromNodeId && n.name === 'Bar')
  7550. );
  7551. expect(fromBar.length).toBe(1);
  7552. expect(fromBar[0]!.referenceName).toBe('B');
  7553. });
  7554. it('emits a decorates ref for `@Foo method() {}`', () => {
  7555. const code = `
  7556. function Get(p: string) { return (t: any, k: string) => t; }
  7557. class Svc {
  7558. @Get('/x') method() { return 1; }
  7559. }
  7560. `;
  7561. const result = extractFromSource('app.ts', code);
  7562. const decorMethod = result.unresolvedReferences.find(
  7563. (r) => r.referenceKind === 'decorates' && r.referenceName === 'Get'
  7564. );
  7565. expect(decorMethod).toBeDefined();
  7566. // The decorated symbol must be `method`, not the constructor or class.
  7567. const decoratedNode = result.nodes.find((n) => n.id === decorMethod!.fromNodeId);
  7568. expect(decoratedNode?.name).toBe('method');
  7569. });
  7570. });
  7571. // =============================================================================
  7572. // Lua
  7573. // =============================================================================
  7574. describe('Lua Extraction', () => {
  7575. describe('Language detection', () => {
  7576. it('should detect Lua files', () => {
  7577. expect(detectLanguage('init.lua')).toBe('lua');
  7578. expect(detectLanguage('src/util.lua')).toBe('lua');
  7579. });
  7580. it('should report Lua as supported', () => {
  7581. expect(isLanguageSupported('lua')).toBe(true);
  7582. expect(getSupportedLanguages()).toContain('lua');
  7583. });
  7584. });
  7585. describe('Function extraction', () => {
  7586. it('should extract global and local functions', () => {
  7587. const code = `
  7588. function configure(opts) return opts end
  7589. local function helper(x) return x * 2 end
  7590. `;
  7591. const result = extractFromSource('init.lua', code);
  7592. const funcs = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name);
  7593. expect(funcs).toContain('configure');
  7594. expect(funcs).toContain('helper');
  7595. const configure = result.nodes.find((n) => n.name === 'configure');
  7596. expect(configure?.language).toBe('lua');
  7597. expect(configure?.signature).toBe('(opts)');
  7598. });
  7599. it('should split table/method functions into a receiver and method name', () => {
  7600. const code = `
  7601. function M.connect(host, port) return host end
  7602. function M:send(data) return self end
  7603. `;
  7604. const result = extractFromSource('init.lua', code);
  7605. const methods = result.nodes.filter((n) => n.kind === 'method');
  7606. const connect = methods.find((m) => m.name === 'connect');
  7607. expect(connect?.qualifiedName).toBe('M::connect');
  7608. const send = methods.find((m) => m.name === 'send');
  7609. expect(send?.qualifiedName).toBe('M::send');
  7610. });
  7611. it('should name function expressions from local, member, and table-field bindings', () => {
  7612. const code = `
  7613. local function helper() return 1 end
  7614. local localFn = function() return helper() end
  7615. local M = {
  7616. callbacks = {
  7617. onStart = function() return helper() end,
  7618. ["onStop"] = function() return helper() end,
  7619. [DYNAMIC] = function() return helper() end,
  7620. },
  7621. }
  7622. M.assignedFn = function() return helper() end
  7623. M["bracketFn"] = function() return helper() end
  7624. localFn()
  7625. `;
  7626. const result = extractFromSource('handlers.lua', code);
  7627. const localFn = result.nodes.find((n) => n.kind === 'function' && n.name === 'localFn');
  7628. const assignedFn = result.nodes.find(
  7629. (n) => n.kind === 'method' && n.qualifiedName === 'M::assignedFn'
  7630. );
  7631. const onStart = result.nodes.find(
  7632. (n) => n.kind === 'method' && n.qualifiedName === 'M.callbacks::onStart'
  7633. );
  7634. const onStop = result.nodes.find(
  7635. (n) => n.kind === 'method' && n.qualifiedName === 'M.callbacks::onStop'
  7636. );
  7637. const bracketFn = result.nodes.find(
  7638. (n) => n.kind === 'method' && n.qualifiedName === 'M::bracketFn'
  7639. );
  7640. expect(localFn).toBeDefined();
  7641. expect(assignedFn).toBeDefined();
  7642. expect(onStart).toBeDefined();
  7643. expect(onStop).toBeDefined();
  7644. expect(bracketFn).toBeDefined();
  7645. expect(result.nodes.some((n) => n.name === 'DYNAMIC')).toBe(false);
  7646. expect(result.nodes.some((n) => n.kind === 'variable' && n.name === 'localFn')).toBe(false);
  7647. for (const callable of [localFn, assignedFn, onStart, onStop, bracketFn]) {
  7648. expect(
  7649. result.unresolvedReferences.some(
  7650. (r) => r.fromNodeId === callable!.id && r.referenceKind === 'calls' && r.referenceName === 'helper'
  7651. )
  7652. ).toBe(true);
  7653. }
  7654. expect(
  7655. result.unresolvedReferences.some(
  7656. (r) => r.referenceKind === 'calls' && r.referenceName === 'localFn'
  7657. )
  7658. ).toBe(true);
  7659. });
  7660. });
  7661. describe('Variable extraction', () => {
  7662. it('should extract local variable declarations', () => {
  7663. const code = `
  7664. local M = {}
  7665. local count = 0
  7666. `;
  7667. const result = extractFromSource('mod.lua', code);
  7668. const vars = result.nodes.filter((n) => n.kind === 'variable').map((n) => n.name);
  7669. expect(vars).toContain('M');
  7670. expect(vars).toContain('count');
  7671. });
  7672. });
  7673. describe('Import extraction (require)', () => {
  7674. it('should extract require() in local declarations and bare calls', () => {
  7675. const code = `
  7676. local socket = require("socket")
  7677. local http = require "resty.http"
  7678. require("side.effect")
  7679. `;
  7680. const result = extractFromSource('net.lua', code);
  7681. const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name);
  7682. expect(imports).toContain('socket');
  7683. expect(imports).toContain('resty.http');
  7684. expect(imports).toContain('side.effect');
  7685. const ref = result.unresolvedReferences.find(
  7686. (r) => r.referenceKind === 'imports' && r.referenceName === 'socket'
  7687. );
  7688. expect(ref).toBeDefined();
  7689. });
  7690. // Regression: the tree-sitter-wasms Lua grammar (ABI 13) corrupts the shared
  7691. // WASM heap under web-tree-sitter 0.25, dropping nested calls/imports on every
  7692. // parse after the first. We vendor the ABI-15 grammar instead — this guards it
  7693. // by extracting several sources in sequence and asserting the LAST still works.
  7694. it('should keep extracting require across many sequential parses', () => {
  7695. let last;
  7696. for (let i = 0; i < 8; i++) {
  7697. last = extractFromSource(`f${i}.lua`, `local m = require("module.${i}")\nreturn m\n`);
  7698. }
  7699. const imports = last!.nodes.filter((n) => n.kind === 'import').map((n) => n.name);
  7700. expect(imports).toContain('module.7');
  7701. });
  7702. });
  7703. describe('Call extraction', () => {
  7704. it('should record intra-file calls as resolvable references', () => {
  7705. const code = `
  7706. local function helper(x) return x end
  7707. local function run(y) return helper(y) end
  7708. `;
  7709. const result = extractFromSource('calls.lua', code);
  7710. const call = result.unresolvedReferences.find(
  7711. (r) => r.referenceKind === 'calls' && r.referenceName === 'helper'
  7712. );
  7713. expect(call).toBeDefined();
  7714. });
  7715. });
  7716. });
  7717. // =============================================================================
  7718. // Luau (typed superset of Lua — https://luau.org)
  7719. // =============================================================================
  7720. describe('Luau Extraction', () => {
  7721. describe('Language detection', () => {
  7722. it('should detect Luau files', () => {
  7723. expect(detectLanguage('init.luau')).toBe('luau');
  7724. expect(detectLanguage('src/Client.luau')).toBe('luau');
  7725. });
  7726. it('should report Luau as supported', () => {
  7727. expect(isLanguageSupported('luau')).toBe(true);
  7728. expect(getSupportedLanguages()).toContain('luau');
  7729. });
  7730. });
  7731. describe('Type aliases', () => {
  7732. it('should extract `type` and `export type` definitions', () => {
  7733. const code = `
  7734. export type Vector = { x: number, y: number }
  7735. type Handler = (msg: string) -> boolean
  7736. `;
  7737. const result = extractFromSource('types.luau', code);
  7738. const aliases = result.nodes.filter((n) => n.kind === 'type_alias');
  7739. const vector = aliases.find((a) => a.name === 'Vector');
  7740. expect(vector).toBeDefined();
  7741. expect(vector?.isExported).toBe(true);
  7742. const handler = aliases.find((a) => a.name === 'Handler');
  7743. expect(handler).toBeDefined();
  7744. expect(handler?.isExported).toBe(false);
  7745. });
  7746. });
  7747. describe('Typed functions and methods', () => {
  7748. it('should capture typed signatures and split methods by receiver', () => {
  7749. const code = `
  7750. function configure(opts: { debug: boolean }): boolean
  7751. return opts.debug
  7752. end
  7753. function Client:fetch(path: string): Response
  7754. return path
  7755. end
  7756. `;
  7757. const result = extractFromSource('client.luau', code);
  7758. const configure = result.nodes.find((n) => n.kind === 'function' && n.name === 'configure');
  7759. expect(configure?.language).toBe('luau');
  7760. expect(configure?.signature).toBe('(opts: { debug: boolean }): boolean');
  7761. const fetch = result.nodes.find((n) => n.kind === 'method' && n.name === 'fetch');
  7762. expect(fetch?.qualifiedName).toBe('Client::fetch');
  7763. });
  7764. });
  7765. describe('Imports and variables', () => {
  7766. it('should extract string and Roblox instance-path require imports', () => {
  7767. const code = `
  7768. local http = require("http")
  7769. local Signal = require(script.Parent.Signal)
  7770. local count = 0
  7771. `;
  7772. const result = extractFromSource('mod.luau', code);
  7773. const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name);
  7774. expect(imports).toContain('http'); // string require
  7775. expect(imports).toContain('Signal'); // Roblox instance-path require
  7776. const vars = result.nodes.filter((n) => n.kind === 'variable').map((n) => n.name);
  7777. expect(vars).toContain('count');
  7778. });
  7779. });
  7780. });
  7781. // =============================================================================
  7782. // Objective-C
  7783. // =============================================================================
  7784. describe('Objective-C Extraction', () => {
  7785. const sample = `
  7786. #import <Foundation/Foundation.h>
  7787. #import "MyClass.h"
  7788. @interface MyClass : NSObject <NSCopying>
  7789. @property (nonatomic, copy) NSString *name;
  7790. - (void)greet;
  7791. - (void)doThing:(id)x with:(id)y;
  7792. + (instancetype)shared;
  7793. @end
  7794. @implementation MyClass
  7795. - (void)greet {
  7796. NSLog(@"Hello");
  7797. [self doWork];
  7798. }
  7799. - (void)doThing:(id)x with:(id)y {
  7800. [self notify:x];
  7801. }
  7802. + (instancetype)shared {
  7803. return [[MyClass alloc] init];
  7804. }
  7805. @end
  7806. void helperFunction(int count) {
  7807. MyClass *obj = [MyClass shared];
  7808. [obj greet];
  7809. }
  7810. `;
  7811. it('should extract classes, methods, functions, and imports', () => {
  7812. const result = extractFromSource('App.m', sample);
  7813. const classes = result.nodes.filter((n) => n.kind === 'class');
  7814. expect(classes.filter((c) => c.name === 'MyClass')).toHaveLength(1);
  7815. const methods = result.nodes.filter((n) => n.kind === 'method');
  7816. expect(methods.map((m) => m.name).sort()).toEqual(['doThing:with:', 'greet', 'shared']);
  7817. const shared = methods.find((m) => m.name === 'shared');
  7818. expect(shared?.isStatic).toBe(true);
  7819. const properties = result.nodes.filter((n) => n.kind === 'property');
  7820. expect(properties.some((p) => p.name === 'name')).toBe(true);
  7821. const functions = result.nodes.filter((n) => n.kind === 'function');
  7822. expect(functions.some((f) => f.name === 'helperFunction')).toBe(true);
  7823. const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name);
  7824. expect(imports).toContain('Foundation/Foundation.h');
  7825. expect(imports).toContain('MyClass.h');
  7826. });
  7827. it('extracts union declarations as first-class union nodes', () => {
  7828. const code = `
  7829. typedef union {
  7830. unsigned int raw;
  7831. float value;
  7832. } NumberBits;
  7833. union opaque_bits;
  7834. `;
  7835. const result = extractFromSource('NumberBits.m', code);
  7836. const numberBits = result.nodes.find((n) => n.name === 'NumberBits');
  7837. expect(numberBits?.kind).toBe('union');
  7838. expect(result.nodes.some((n) => n.name === 'opaque_bits')).toBe(false);
  7839. });
  7840. it('should record inheritance and protocol conformance', () => {
  7841. const result = extractFromSource('App.m', sample);
  7842. const extendsRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'extends');
  7843. const implementsRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'implements');
  7844. expect(extendsRefs.map((r) => r.referenceName)).toContain('NSObject');
  7845. expect(implementsRefs.map((r) => r.referenceName)).toContain('NSCopying');
  7846. });
  7847. it('should record message sends and C calls', () => {
  7848. const result = extractFromSource('App.m', sample);
  7849. const calls = result.unresolvedReferences
  7850. .filter((r) => r.referenceKind === 'calls')
  7851. .map((r) => r.referenceName);
  7852. expect(calls).toEqual(expect.arrayContaining(['NSLog', 'doWork', 'MyClass.shared', 'obj.greet']));
  7853. });
  7854. it('should reconstruct multi-keyword selectors at the call site so they resolve to the method definition', () => {
  7855. // Regression for the gap discovered post-#165: message_expression's
  7856. // multi-keyword form `[obj a:1 b:2]` was only emitting the first keyword,
  7857. // so calls never resolved to multi-part method definitions like
  7858. // `GET:parameters:headers:progress:success:failure:`. The call-site name
  7859. // must match the method-definition name with full keywords + trailing colons.
  7860. const code = `
  7861. @implementation Caller
  7862. - (void)demo {
  7863. NSMutableDictionary *d = [NSMutableDictionary new];
  7864. [d setObject:@"v" forKey:@"k"];
  7865. [d setObject:@"v2" forKey:@"k2" withRetry:@YES];
  7866. [self touchesBegan:nil withEvent:nil];
  7867. }
  7868. @end
  7869. `;
  7870. const result = extractFromSource('Caller.m', code);
  7871. const calls = result.unresolvedReferences
  7872. .filter((r) => r.referenceKind === 'calls')
  7873. .map((r) => r.referenceName);
  7874. expect(calls).toEqual(
  7875. expect.arrayContaining([
  7876. 'd.setObject:forKey:',
  7877. 'd.setObject:forKey:withRetry:',
  7878. 'touchesBegan:withEvent:',
  7879. ])
  7880. );
  7881. });
  7882. it('should not classify pure C headers with @end in comments as objc', () => {
  7883. const cHeader = '/* @end of file */\n#ifndef STDIO_H\nvoid printf(const char *);\n#endif\n';
  7884. expect(detectLanguage('stdio.h', cHeader)).toBe('c');
  7885. });
  7886. it('should extract protocol declarations', () => {
  7887. const code = `
  7888. @protocol DataSource <NSObject>
  7889. - (NSInteger)numberOfItems;
  7890. @end
  7891. `;
  7892. const result = extractFromSource('DataSource.h', code);
  7893. const protocol = result.nodes.find((n) => n.kind === 'protocol' && n.name === 'DataSource');
  7894. expect(protocol).toBeDefined();
  7895. });
  7896. it('should report Objective-C as supported', () => {
  7897. expect(isLanguageSupported('objc')).toBe(true);
  7898. expect(getSupportedLanguages()).toContain('objc');
  7899. });
  7900. });
  7901. describe('Solidity Extraction', () => {
  7902. const code = `// SPDX-License-Identifier: MIT
  7903. pragma solidity ^0.8.20;
  7904. import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
  7905. interface IVault {
  7906. function deposit(uint256 amount) external returns (bool);
  7907. event Deposited(address indexed user, uint256 amount);
  7908. }
  7909. library SafeMath {
  7910. function add(uint256 a, uint256 b) internal pure returns (uint256) {
  7911. return a + b;
  7912. }
  7913. }
  7914. contract Vault is IVault {
  7915. using SafeMath for uint256;
  7916. enum Status { Active, Frozen, Closed }
  7917. struct UserInfo {
  7918. uint256 balance;
  7919. uint256 lastDeposit;
  7920. }
  7921. IERC20 public immutable token;
  7922. mapping(address => UserInfo) public users;
  7923. address public owner;
  7924. event Withdrawn(address indexed user, uint256 amount);
  7925. error NotOwner();
  7926. modifier onlyOwner() {
  7927. if (msg.sender != owner) revert NotOwner();
  7928. _;
  7929. }
  7930. constructor(address _token) {
  7931. token = IERC20(_token);
  7932. owner = msg.sender;
  7933. }
  7934. function deposit(uint256 amount) external override returns (bool) {
  7935. users[msg.sender].balance = users[msg.sender].balance.add(amount);
  7936. emit Deposited(msg.sender, amount);
  7937. return true;
  7938. }
  7939. function withdraw(uint256 amount) external onlyOwner {
  7940. emit Withdrawn(msg.sender, amount);
  7941. }
  7942. }
  7943. `;
  7944. describe('Language detection', () => {
  7945. it('should detect Solidity files', () => {
  7946. expect(detectLanguage('contracts/Vault.sol')).toBe('solidity');
  7947. });
  7948. it('should report Solidity as supported', () => {
  7949. expect(isLanguageSupported('solidity')).toBe(true);
  7950. expect(getSupportedLanguages()).toContain('solidity');
  7951. });
  7952. });
  7953. describe('Container extraction', () => {
  7954. it('should extract contract / interface / library as class-likes', () => {
  7955. const result = extractFromSource('Vault.sol', code);
  7956. // interface_declaration → interface
  7957. const iface = result.nodes.find((n) => n.kind === 'interface' && n.name === 'IVault');
  7958. expect(iface).toBeDefined();
  7959. expect(iface?.language).toBe('solidity');
  7960. // contract and library both map to 'class' (library has no special semantics
  7961. // a class node doesn't already cover — they share methodTypes/inheritance).
  7962. expect(result.nodes.find((n) => n.kind === 'class' && n.name === 'Vault')).toBeDefined();
  7963. expect(result.nodes.find((n) => n.kind === 'class' && n.name === 'SafeMath')).toBeDefined();
  7964. });
  7965. it('should emit extends references for `is X, Y` inheritance', () => {
  7966. // `Vault is IVault` — Solidity uses one keyword (`is`) for both class
  7967. // extension and interface implementation, so the extractor emits `extends`
  7968. // and the resolver's interface-impl synthesizer reclassifies to
  7969. // `implements` based on the target node kind.
  7970. const result = extractFromSource('Vault.sol', code);
  7971. const extendsRefs = result.unresolvedReferences.filter(
  7972. (r) => r.referenceKind === 'extends' && r.referenceName === 'IVault'
  7973. );
  7974. expect(extendsRefs).toHaveLength(1);
  7975. const vaultNode = result.nodes.find((n) => n.kind === 'class' && n.name === 'Vault');
  7976. expect(extendsRefs[0]?.fromNodeId).toBe(vaultNode?.id);
  7977. });
  7978. });
  7979. describe('Method extraction', () => {
  7980. it('should extract methods, modifiers, and constructor with signatures', () => {
  7981. const result = extractFromSource('Vault.sol', code);
  7982. const methods = result.nodes.filter((n) => n.kind === 'method');
  7983. const names = methods.map((n) => n.name);
  7984. expect(names).toContain('deposit');
  7985. expect(names).toContain('withdraw');
  7986. expect(names).toContain('add');
  7987. expect(names).toContain('onlyOwner'); // modifier_definition
  7988. expect(names).toContain('constructor'); // constructor_definition (synthetic name)
  7989. // Signature should capture parameters + visibility + state mutability + return type.
  7990. const add = methods.find((m) => m.name === 'add');
  7991. expect(add?.signature).toContain('uint256 a');
  7992. expect(add?.signature).toContain('internal');
  7993. expect(add?.signature).toContain('pure');
  7994. expect(add?.signature).toContain('returns (uint256)');
  7995. // `external` visibility should map to 'public' (callable from outside the contract).
  7996. const deposit = methods.find((m) => m.name === 'deposit');
  7997. expect(deposit?.visibility).toBe('public');
  7998. });
  7999. });
  8000. describe('Struct, enum, and field extraction', () => {
  8001. it('should extract struct, enum, and enum members', () => {
  8002. const result = extractFromSource('Vault.sol', code);
  8003. expect(result.nodes.find((n) => n.kind === 'struct' && n.name === 'UserInfo')).toBeDefined();
  8004. expect(result.nodes.find((n) => n.kind === 'enum' && n.name === 'Status')).toBeDefined();
  8005. const enumMembers = result.nodes.filter((n) => n.kind === 'enum_member').map((n) => n.name);
  8006. expect(enumMembers).toEqual(expect.arrayContaining(['Active', 'Frozen', 'Closed']));
  8007. });
  8008. it('should extract state variables, struct members, events, errors as fields', () => {
  8009. const result = extractFromSource('Vault.sol', code);
  8010. const fieldNames = result.nodes.filter((n) => n.kind === 'field').map((n) => n.name);
  8011. // state variables
  8012. expect(fieldNames).toEqual(expect.arrayContaining(['token', 'users', 'owner']));
  8013. // struct members
  8014. expect(fieldNames).toEqual(expect.arrayContaining(['balance', 'lastDeposit']));
  8015. // event + error
  8016. expect(fieldNames).toEqual(expect.arrayContaining(['Deposited', 'Withdrawn', 'NotOwner']));
  8017. });
  8018. it('should treat `constant_variable_declaration` as a constant, not a variable', () => {
  8019. const constCode = `// SPDX-License-Identifier: MIT
  8020. pragma solidity ^0.8.20;
  8021. uint256 constant FILE_CONST = 42;
  8022. `;
  8023. const result = extractFromSource('consts.sol', constCode);
  8024. const node = result.nodes.find((n) => n.name === 'FILE_CONST');
  8025. expect(node?.kind).toBe('constant');
  8026. });
  8027. });
  8028. describe('Import and call extraction', () => {
  8029. it('should extract import directives with the source path as the module name', () => {
  8030. const result = extractFromSource('Vault.sol', code);
  8031. const imp = result.nodes.find((n) => n.kind === 'import');
  8032. expect(imp).toBeDefined();
  8033. expect(imp?.name).toBe('@openzeppelin/contracts/token/ERC20/IERC20.sol');
  8034. });
  8035. it('should produce calls refs for emit, revert, and library/method calls', () => {
  8036. const result = extractFromSource('Vault.sol', code);
  8037. const calls = result.unresolvedReferences
  8038. .filter((r) => r.referenceKind === 'calls')
  8039. .map((r) => r.referenceName);
  8040. // emit Deposited(...)
  8041. expect(calls).toContain('Deposited');
  8042. // revert NotOwner()
  8043. expect(calls).toContain('NotOwner');
  8044. // library call: balance.add(amount) — receiver-qualified
  8045. expect(calls.some((c) => c === 'add' || c === 'balance.add')).toBe(true);
  8046. });
  8047. it('should produce calls refs for modifier invocations and base-constructor invocations', () => {
  8048. // `withdraw(...) external onlyOwner` — the modifier sits in the function
  8049. // header, outside the body: field the call walker descends, so it goes
  8050. // through the decorator-position walk. It must emit `calls` (not
  8051. // `decorates`) so flow traversal rides the withdraw → onlyOwner →
  8052. // NotOwner audit path.
  8053. const result = extractFromSource('Vault.sol', code);
  8054. const withdrawNode = result.nodes.find((n) => n.kind === 'method' && n.name === 'withdraw');
  8055. const modifierCall = result.unresolvedReferences.find(
  8056. (r) => r.referenceKind === 'calls' && r.referenceName === 'onlyOwner'
  8057. );
  8058. expect(modifierCall).toBeDefined();
  8059. expect(modifierCall?.fromNodeId).toBe(withdrawNode?.id);
  8060. // Base-constructor invocation parses as the same modifier_invocation
  8061. // node: `constructor(address o) ERC20("T", "TOK") Ownable(o)` — the
  8062. // constructor-chain hop.
  8063. const ctorCode = `pragma solidity ^0.8.20;
  8064. contract MyToken is ERC20, Ownable {
  8065. constructor(address o) ERC20("Tok", "TOK") Ownable(o) {}
  8066. function grab(bytes32 r) external onlyRole(ADMIN_ROLE) returns (uint256) { return 1; }
  8067. }
  8068. `;
  8069. const ctorResult = extractFromSource('MyToken.sol', ctorCode);
  8070. const ctorCalls = ctorResult.unresolvedReferences
  8071. .filter((r) => r.referenceKind === 'calls')
  8072. .map((r) => r.referenceName);
  8073. expect(ctorCalls).toEqual(expect.arrayContaining(['ERC20', 'Ownable']));
  8074. // modifier WITH arguments still resolves to the bare modifier name
  8075. expect(ctorCalls).toContain('onlyRole');
  8076. });
  8077. });
  8078. });
  8079. describe('Regression: issue-specific extraction fixes', () => {
  8080. it('indexes inner functions of an anonymous AMD/CommonJS module wrapper (#528)', () => {
  8081. const code = `
  8082. define(['dep'], function (dep) {
  8083. function innerHelper(x) { return x + 1; }
  8084. function compute(y) { return innerHelper(y); }
  8085. return { compute: compute };
  8086. });
  8087. `;
  8088. const result = extractFromSource('amd-module.js', code);
  8089. const fns = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name);
  8090. expect(fns).toContain('innerHelper');
  8091. expect(fns).toContain('compute');
  8092. });
  8093. it('attaches Go methods on generic receivers to their type (#583)', () => {
  8094. const code = `
  8095. package main
  8096. type Stack[T any] struct { items []T }
  8097. func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }
  8098. func (s Stack[T]) Len() int { return len(s.items) }
  8099. `;
  8100. const result = extractFromSource('stack.go', code);
  8101. const methods = result.nodes.filter((n) => n.kind === 'method');
  8102. expect(methods.find((m) => m.name === 'Push')?.qualifiedName).toBe('Stack::Push');
  8103. expect(methods.find((m) => m.name === 'Len')?.qualifiedName).toBe('Stack::Len');
  8104. });
  8105. it('indexes new module extensions: .mts/.cts (TS) and .xsjs/.xsjslib (JS) (#366, #556)', () => {
  8106. expect(isSourceFile('mod.mts')).toBe(true);
  8107. expect(isSourceFile('mod.cts')).toBe(true);
  8108. expect(isSourceFile('service.xsjs')).toBe(true);
  8109. expect(isSourceFile('lib.xsjslib')).toBe(true);
  8110. expect(detectLanguage('mod.mts')).toBe('typescript');
  8111. expect(detectLanguage('service.xsjs')).toBe('javascript');
  8112. // End-to-end: a .mts file is parsed as TS, a .xsjs file as JS.
  8113. const ts = extractFromSource('mod.mts', 'export function hello(): number { return 1; }');
  8114. expect(ts.nodes.find((n) => n.name === 'hello' && n.kind === 'function')).toBeDefined();
  8115. const js = extractFromSource('service.xsjs', 'function handleRequest() { return 1; }');
  8116. expect(js.nodes.find((n) => n.name === 'handleRequest' && n.kind === 'function')).toBeDefined();
  8117. });
  8118. });
  8119. describe('Import / re-export dependency linking (blast-radius recall)', () => {
  8120. // An import IS a dependency, but extraction only emits references for calls,
  8121. // instantiations, type annotations, and inheritance — so a symbol imported and
  8122. // then merely re-exported, placed in a registry array, passed as an argument,
  8123. // or used in JSX produced no cross-file edge, leaving the providing file with a
  8124. // false "0 dependents". These tests pin the import/re-export binding linking.
  8125. it('emits an imports reference per named, aliased, and default import binding', () => {
  8126. const code = `
  8127. import { widget, helper as h } from './foo';
  8128. import Thing from './thing';
  8129. import * as NS from './ns';
  8130. export const registry = [widget];
  8131. `;
  8132. const result = extractFromSource('bar.ts', code);
  8133. const names = result.unresolvedReferences
  8134. .filter((r) => r.referenceKind === 'imports')
  8135. .map((r) => r.referenceName);
  8136. expect(names).toContain('widget'); // named import → local name
  8137. expect(names).toContain('h'); // aliased import → local alias
  8138. expect(names).toContain('Thing'); // default import
  8139. expect(names).toContain('NS'); // namespace import → linked to the module file as a dependency
  8140. });
  8141. it('emits an imports reference per re-exported binding', () => {
  8142. const result = extractFromSource('barrel.ts', `export { alpha, beta as b } from './source';`);
  8143. const names = result.unresolvedReferences
  8144. .filter((r) => r.referenceKind === 'imports')
  8145. .map((r) => r.referenceName);
  8146. // Re-export links the SOURCE-side name, not the local alias.
  8147. expect(names).toContain('alpha');
  8148. expect(names).toContain('beta');
  8149. });
  8150. it('a value imported/re-exported but never called still makes the importer a dependent', async () => {
  8151. const dir = createTempDir();
  8152. try {
  8153. fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
  8154. fs.writeFileSync(
  8155. path.join(dir, 'src', 'foo.ts'),
  8156. `export const widget = { n: 1 };\nexport function helper(): void {}\n`
  8157. );
  8158. // bar uses widget ONLY in an array and re-exports helper — neither is
  8159. // called/typed, so before import-linking bar had no edge to foo at all.
  8160. fs.writeFileSync(
  8161. path.join(dir, 'src', 'bar.ts'),
  8162. `import { widget } from './foo';\nexport { helper } from './foo';\nexport const registry = [widget];\n`
  8163. );
  8164. const cg = CodeGraph.initSync(dir, { config: { include: ['src/**/*.ts'], exclude: [] } });
  8165. await cg.indexAll();
  8166. cg.resolveReferences();
  8167. expect(cg.getFileDependents('src/foo.ts')).toContain('src/bar.ts');
  8168. cg.destroy();
  8169. } finally {
  8170. cleanupTempDir(dir);
  8171. }
  8172. });
  8173. it('a namespace import touched only via a value-member read still links the module file', async () => {
  8174. const dir = createTempDir();
  8175. try {
  8176. fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
  8177. fs.writeFileSync(path.join(dir, 'src', 'foo.ts'), `export const SOME_CONST = 42;\n`);
  8178. // `foo` is imported as a namespace and used ONLY via a value-member read
  8179. // (no call, no type) — `foo.helper()` would link on its own, but a bare
  8180. // `foo.SOME_CONST` would not, so the module-import backstop must link it.
  8181. fs.writeFileSync(path.join(dir, 'src', 'bar.ts'), `import * as foo from './foo';\nexport const x = foo.SOME_CONST;\n`);
  8182. const cg = CodeGraph.initSync(dir, { config: { include: ['src/**/*.ts'], exclude: [] } });
  8183. await cg.indexAll();
  8184. cg.resolveReferences();
  8185. expect(cg.getFileDependents('src/foo.ts')).toContain('src/bar.ts');
  8186. cg.destroy();
  8187. } finally {
  8188. cleanupTempDir(dir);
  8189. }
  8190. });
  8191. });
  8192. describe('Python import dependency linking (blast-radius recall)', () => {
  8193. // Same recall gap as TS: Python only linked called/instantiated imports, so a
  8194. // name brought in with `from module import X` and then merely stored, used as
  8195. // a decorator/argument, or re-exported through an `__init__.py` produced no
  8196. // cross-file edge — the providing module showed a false "0 dependents".
  8197. it('emits an imports reference per name in a `from module import ...` (incl. value/aliased)', () => {
  8198. const code = [
  8199. 'from foo import helper, widget',
  8200. 'from foo import Thing as T',
  8201. 'from . import sibling',
  8202. 'from bar import *',
  8203. ].join('\n');
  8204. const names = extractFromSource('mod.py', code)
  8205. .unresolvedReferences.filter((r) => r.referenceKind === 'imports')
  8206. .map((r) => r.referenceName);
  8207. expect(names).toContain('helper');
  8208. expect(names).toContain('widget'); // value import
  8209. expect(names).toContain('T'); // aliased import → local name
  8210. expect(names).toContain('sibling'); // `from . import <name>`
  8211. expect(names).not.toContain('*'); // wildcard import has no names
  8212. });
  8213. it('a Python value imported but never called still makes the importer a dependent', async () => {
  8214. const dir = createTempDir();
  8215. try {
  8216. fs.mkdirSync(path.join(dir, 'pkg'), { recursive: true });
  8217. fs.writeFileSync(path.join(dir, 'pkg', 'foo.py'), `widget = {"n": 1}\ndef helper():\n return 1\n`);
  8218. // bar imports widget+helper but only stores widget in a list — nothing is
  8219. // called, so before import-linking bar had no edge to foo.
  8220. fs.writeFileSync(path.join(dir, 'pkg', 'bar.py'), `from foo import widget, helper\nregistry = [widget]\n`);
  8221. const cg = CodeGraph.initSync(dir, { config: { include: ['pkg/**/*.py'], exclude: [] } });
  8222. await cg.indexAll();
  8223. cg.resolveReferences();
  8224. expect(cg.getFileDependents('pkg/foo.py')).toContain('pkg/bar.py');
  8225. cg.destroy();
  8226. } finally {
  8227. cleanupTempDir(dir);
  8228. }
  8229. });
  8230. it('resolves `from . import submodule` + `submodule.func()` to the submodule', async () => {
  8231. const dir = createTempDir();
  8232. try {
  8233. fs.mkdirSync(path.join(dir, 'pkg'), { recursive: true });
  8234. fs.writeFileSync(path.join(dir, 'pkg', '__init__.py'), '');
  8235. fs.writeFileSync(path.join(dir, 'pkg', 'certs.py'), `def where():\n return "/ca.pem"\n`);
  8236. // certs is an imported MODULE (a file), and certs.where() is a qualified
  8237. // call through it — the receiver isn't a symbol, so plain name-matching
  8238. // can't link it. Also exercises the Python relative-dot path fix (`.certs`).
  8239. fs.writeFileSync(path.join(dir, 'pkg', 'utils.py'), `from . import certs\ndef go():\n return certs.where()\n`);
  8240. const cg = CodeGraph.initSync(dir, { config: { include: ['pkg/**/*.py'], exclude: [] } });
  8241. await cg.indexAll();
  8242. cg.resolveReferences();
  8243. expect(cg.getFileDependents('pkg/certs.py')).toContain('pkg/utils.py');
  8244. cg.destroy();
  8245. } finally {
  8246. cleanupTempDir(dir);
  8247. }
  8248. });
  8249. it('a module import is a dependency even when the used member is re-exported elsewhere', async () => {
  8250. const dir = createTempDir();
  8251. try {
  8252. fs.mkdirSync(path.join(dir, 'pkg'), { recursive: true });
  8253. fs.writeFileSync(path.join(dir, 'pkg', '__init__.py'), '');
  8254. // `where` is NOT defined in certs.py (re-exported from a 3rd-party pkg), so
  8255. // member resolution can't find it — the module-import backstop must still
  8256. // record utils -> certs. (Mirrors requests' real `certs.where`.)
  8257. fs.writeFileSync(path.join(dir, 'pkg', 'certs.py'), `from external_ca import where\n`);
  8258. fs.writeFileSync(path.join(dir, 'pkg', 'utils.py'), `from . import certs\nCA = certs.where()\n`);
  8259. const cg = CodeGraph.initSync(dir, { config: { include: ['pkg/**/*.py'], exclude: [] } });
  8260. await cg.indexAll();
  8261. cg.resolveReferences();
  8262. expect(cg.getFileDependents('pkg/certs.py')).toContain('pkg/utils.py');
  8263. cg.destroy();
  8264. } finally {
  8265. cleanupTempDir(dir);
  8266. }
  8267. });
  8268. });
  8269. describe('Go cross-package composite literals (blast-radius recall)', () => {
  8270. // Go function calls and type references across packages already resolved, but
  8271. // struct composite literals — `render.XML{...}` / `pkga.Widget{...}` — were not
  8272. // extracted at all, so a package whose types are only INSTANTIATED elsewhere
  8273. // (gin's render/binding implementations) showed 0 dependents.
  8274. it('links a cross-package struct composite literal to the defining package', async () => {
  8275. const dir = createTempDir();
  8276. try {
  8277. fs.writeFileSync(path.join(dir, 'go.mod'), 'module example.com/proj\n\ngo 1.21\n');
  8278. fs.mkdirSync(path.join(dir, 'render'), { recursive: true });
  8279. fs.writeFileSync(path.join(dir, 'render', 'xml.go'), `package render\n\ntype XML struct { Data any }\n`);
  8280. fs.writeFileSync(path.join(dir, 'app.go'), `package main\n\nimport "example.com/proj/render"\n\nfunc handle() any { return render.XML{} }\n`);
  8281. const cg = CodeGraph.initSync(dir, { config: { include: ['**/*.go'], exclude: [] } });
  8282. await cg.indexAll();
  8283. cg.resolveReferences();
  8284. expect(cg.getFileDependents('render/xml.go')).toContain('app.go');
  8285. cg.destroy();
  8286. } finally {
  8287. cleanupTempDir(dir);
  8288. }
  8289. });
  8290. it('links a composite literal in a package-level var registry', async () => {
  8291. const dir = createTempDir();
  8292. try {
  8293. fs.writeFileSync(path.join(dir, 'go.mod'), 'module example.com/proj\n\ngo 1.21\n');
  8294. fs.mkdirSync(path.join(dir, 'render'), { recursive: true });
  8295. fs.writeFileSync(path.join(dir, 'render', 'xml.go'), `package render\n\ntype XML struct {}\nfunc (XML) Render() {}\n`);
  8296. // The implementation is registered only in a top-level `var registry = {...}`
  8297. // map literal — the body walker doesn't cover top-level declarations, so this
  8298. // exercises the var-initializer walking added for Go.
  8299. fs.writeFileSync(path.join(dir, 'reg.go'), `package main\n\nimport "example.com/proj/render"\n\ntype R interface { Render() }\n\nvar registry = map[string]R{ "xml": render.XML{} }\n`);
  8300. const cg = CodeGraph.initSync(dir, { config: { include: ['**/*.go'], exclude: [] } });
  8301. await cg.indexAll();
  8302. cg.resolveReferences();
  8303. expect(cg.getFileDependents('render/xml.go')).toContain('reg.go');
  8304. cg.destroy();
  8305. } finally {
  8306. cleanupTempDir(dir);
  8307. }
  8308. });
  8309. it('attributes a call inside a top-level closure (cobra RunE) to the var, not the file (#693)', async () => {
  8310. const dir = createTempDir();
  8311. try {
  8312. fs.writeFileSync(path.join(dir, 'go.mod'), 'module example.com/proj\n\ngo 1.21\n');
  8313. // Wire is called ONLY from the anonymous RunE closure inside a top-level
  8314. // `var rootCmd = &Cmd{...}` — previously the call leaked to the file node,
  8315. // so `callers(Wire)` surfaced a file (or read as "no caller"). It must now
  8316. // attribute to the enclosing var.
  8317. fs.writeFileSync(path.join(dir, 'factory.go'), `package main\n\nfunc Wire() error { return nil }\n`);
  8318. fs.writeFileSync(
  8319. path.join(dir, 'root.go'),
  8320. `package main\n\ntype Cmd struct{ RunE func() error }\n\nvar rootCmd = &Cmd{\n\tRunE: func() error { return Wire() },\n}\n`
  8321. );
  8322. const cg = CodeGraph.initSync(dir, { config: { include: ['**/*.go'], exclude: [] } });
  8323. await cg.indexAll();
  8324. cg.resolveReferences();
  8325. const wire = cg.getNodesByName('Wire').find((n) => n.kind === 'function');
  8326. expect(wire).toBeDefined();
  8327. const callers = cg.getCallers(wire!.id).map((c) => c.node);
  8328. expect(callers.some((n) => n.kind === 'variable' && n.name === 'rootCmd')).toBe(true);
  8329. expect(callers.some((n) => n.kind === 'file')).toBe(false);
  8330. cg.destroy();
  8331. } finally {
  8332. cleanupTempDir(dir);
  8333. }
  8334. });
  8335. it('links a parenthesized pointer type conversion `(*T)(x)` to the type', async () => {
  8336. const dir = createTempDir();
  8337. try {
  8338. fs.writeFileSync(path.join(dir, 'go.mod'), 'module example.com/proj\n\ngo 1.21\n');
  8339. fs.writeFileSync(path.join(dir, 'types.go'), `package main\n\ntype Wrapped struct { N int }\n`);
  8340. // `(*Wrapped)(x)` parses as a call whose callee is the parenthesized type
  8341. // `(*Wrapped)` — without normalization it dropped on the floor.
  8342. fs.writeFileSync(path.join(dir, 'use.go'), `package main\n\nfunc run(x *int) { _ = (*Wrapped)(x) }\n`);
  8343. const cg = CodeGraph.initSync(dir, { config: { include: ['**/*.go'], exclude: [] } });
  8344. await cg.indexAll();
  8345. cg.resolveReferences();
  8346. expect(cg.getFileDependents('types.go')).toContain('use.go');
  8347. cg.destroy();
  8348. } finally {
  8349. cleanupTempDir(dir);
  8350. }
  8351. });
  8352. it('links an implementation reached only through a Go interface (implicit satisfaction, #584)', async () => {
  8353. const dir = createTempDir();
  8354. try {
  8355. fs.writeFileSync(path.join(dir, 'go.mod'), 'module example.com/proj\n\ngo 1.21\n');
  8356. fs.mkdirSync(path.join(dir, 'codec'), { recursive: true });
  8357. fs.writeFileSync(path.join(dir, 'codec', 'api.go'), `package codec\n\ntype Core interface {\n\tMarshal(v any) ([]byte, error)\n}\n\nvar API Core\n`);
  8358. // jsonApi satisfies Core structurally (no `implements` keyword) and is
  8359. // reached ONLY through the interface (API.Marshal). Without implicit
  8360. // interface satisfaction + dispatch, json.go shows 0 dependents.
  8361. fs.writeFileSync(path.join(dir, 'codec', 'json.go'), `package codec\n\ntype jsonApi struct{}\n\nfunc (j jsonApi) Marshal(v any) ([]byte, error) { return nil, nil }\n\nfunc init() { API = jsonApi{} }\n`);
  8362. const cg = CodeGraph.initSync(dir, { config: { include: ['**/*.go'], exclude: [] } });
  8363. await cg.indexAll();
  8364. cg.resolveReferences();
  8365. expect(cg.getFileDependents('codec/json.go')).toContain('codec/api.go');
  8366. cg.destroy();
  8367. } finally {
  8368. cleanupTempDir(dir);
  8369. }
  8370. });
  8371. });
  8372. describe('C# records (blast-radius recall)', () => {
  8373. // Records are ubiquitous in modern C# (DTOs, value objects, CQRS messages),
  8374. // but `record` / `record struct` declarations weren't extracted as types — so
  8375. // every reference, generic-type-argument, and `new` of a record dropped on the
  8376. // floor and the defining file showed 0 dependents. (#237)
  8377. it('extracts a record as a graph node (record class + record struct)', () => {
  8378. const r = extractFromSource('r.cs', `namespace P;\npublic record Box(int N);\npublic record struct Pt(int X);\n`);
  8379. expect(r.nodes.find((n) => n.name === 'Box' && (n.kind === 'class' || n.kind === 'struct'))).toBeDefined();
  8380. expect(r.nodes.find((n) => n.name === 'Pt' && (n.kind === 'class' || n.kind === 'struct'))).toBeDefined();
  8381. });
  8382. it('resolves references / instantiations of a record across files', async () => {
  8383. const dir = createTempDir();
  8384. try {
  8385. fs.writeFileSync(path.join(dir, 'types.cs'), `namespace P;\npublic record Box(int N);\n`);
  8386. // Box is used as a generic type argument and instantiated — both require
  8387. // Box to be a node to resolve.
  8388. fs.writeFileSync(
  8389. path.join(dir, 'use.cs'),
  8390. `using System.Collections.Generic;\nnamespace P;\npublic class User {\n public IEnumerable<Box> Boxes { get; }\n public Box Make() => new Box(1);\n}\n`
  8391. );
  8392. const cg = CodeGraph.initSync(dir, { config: { include: ['**/*.cs'], exclude: [] } });
  8393. await cg.indexAll();
  8394. cg.resolveReferences();
  8395. expect(cg.getFileDependents('types.cs')).toContain('use.cs');
  8396. cg.destroy();
  8397. } finally {
  8398. cleanupTempDir(dir);
  8399. }
  8400. });
  8401. });
  8402. describe('Rust cross-module recall', () => {
  8403. function rustProject(files: Record<string, string>): string {
  8404. const dir = createTempDir();
  8405. fs.writeFileSync(path.join(dir, 'Cargo.toml'), '[package]\nname = "proj"\nversion = "0.1.0"\nedition = "2021"\n');
  8406. fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
  8407. for (const [rel, content] of Object.entries(files)) {
  8408. const full = path.join(dir, 'src', rel);
  8409. fs.mkdirSync(path.dirname(full), { recursive: true });
  8410. fs.writeFileSync(full, content);
  8411. }
  8412. return dir;
  8413. }
  8414. it('extracts a struct literal `Foo { .. }` as an instantiation across modules', async () => {
  8415. const dir = rustProject({
  8416. 'lib.rs': 'pub mod types;\npub mod consumer;\n',
  8417. 'types.rs': 'pub struct Widget { pub n: i32 }\n',
  8418. 'consumer.rs': 'use crate::types::Widget;\npub fn build() -> Widget { Widget { n: 1 } }\n',
  8419. });
  8420. try {
  8421. const cg = CodeGraph.initSync(dir, { config: { include: ['src/**/*.rs'], exclude: [] } });
  8422. await cg.indexAll();
  8423. cg.resolveReferences();
  8424. expect(cg.getFileDependents('src/types.rs')).toContain('src/consumer.rs');
  8425. cg.destroy();
  8426. } finally { cleanupTempDir(dir); }
  8427. });
  8428. it('extracts trait method declarations and bridges trait dispatch to the impl', async () => {
  8429. const dir = rustProject({
  8430. 'lib.rs': 'pub mod types;\npub mod consumer;\n',
  8431. 'types.rs': 'pub trait Render { fn render(&self) -> i32; }\n',
  8432. // Mine implements Render structurally; reached via &dyn Render dispatch.
  8433. 'consumer.rs': 'use crate::types::Render;\npub struct Mine { pub x: i32 }\nimpl Render for Mine { fn render(&self) -> i32 { self.x } }\n',
  8434. });
  8435. try {
  8436. const cg = CodeGraph.initSync(dir, { config: { include: ['src/**/*.rs'], exclude: [] } });
  8437. await cg.indexAll();
  8438. cg.resolveReferences();
  8439. // implements edge (Mine -> Render) makes types.rs a dependent of consumer.rs's struct.
  8440. expect(cg.getFileDependents('src/types.rs')).toContain('src/consumer.rs');
  8441. cg.destroy();
  8442. } finally { cleanupTempDir(dir); }
  8443. });
  8444. it('links `pub use` re-export hubs to the modules they re-export', async () => {
  8445. const dir = rustProject({
  8446. 'lib.rs': 'pub mod api;\n',
  8447. 'api/mod.rs': 'mod widget;\npub use self::widget::Widget;\n',
  8448. 'api/widget.rs': 'pub struct Widget { pub n: i32 }\n',
  8449. });
  8450. try {
  8451. const cg = CodeGraph.initSync(dir, { config: { include: ['src/**/*.rs'], exclude: [] } });
  8452. await cg.indexAll();
  8453. cg.resolveReferences();
  8454. // The re-export hub depends on the module it re-exports from.
  8455. expect(cg.getFileDependents('src/api/widget.rs')).toContain('src/api/mod.rs');
  8456. cg.destroy();
  8457. } finally { cleanupTempDir(dir); }
  8458. });
  8459. it('resolves a qualified path to the correct module when the leaf name collides', async () => {
  8460. const dir = rustProject({
  8461. 'lib.rs': 'pub mod fast;\npub mod slow;\npub mod hub;\n',
  8462. 'fast.rs': 'pub fn read() -> i32 { 1 }\n',
  8463. 'slow.rs': 'pub fn read() -> i32 { 2 }\n',
  8464. // `read` exists in BOTH fast.rs and slow.rs — module-path resolution must
  8465. // send this re-export to fast.rs specifically, not name-match either.
  8466. 'hub.rs': 'pub use crate::fast::read;\n',
  8467. });
  8468. try {
  8469. const cg = CodeGraph.initSync(dir, { config: { include: ['src/**/*.rs'], exclude: [] } });
  8470. await cg.indexAll();
  8471. cg.resolveReferences();
  8472. expect(cg.getFileDependents('src/fast.rs')).toContain('src/hub.rs');
  8473. expect(cg.getFileDependents('src/slow.rs')).not.toContain('src/hub.rs');
  8474. cg.destroy();
  8475. } finally { cleanupTempDir(dir); }
  8476. });
  8477. });
  8478. describe('Java annotations (blast-radius recall)', () => {
  8479. it('indexes @interface definitions and links @Annotation usages to them', async () => {
  8480. const dir = createTempDir();
  8481. try {
  8482. fs.mkdirSync(path.join(dir, 'p'), { recursive: true });
  8483. // The annotation DEFINITION must be a node, and the @MyAnno usages (which
  8484. // live inside a `modifiers` node on the class/field/method) must extract.
  8485. fs.writeFileSync(path.join(dir, 'p', 'MyAnno.java'), `package p;\npublic @interface MyAnno { String value() default ""; }\n`);
  8486. fs.writeFileSync(
  8487. path.join(dir, 'p', 'User.java'),
  8488. `package p;\n@MyAnno("c")\npublic class User {\n @MyAnno("f") int field;\n @MyAnno("m") void go() {}\n}\n`
  8489. );
  8490. const cg = CodeGraph.initSync(dir, { config: { include: ['**/*.java'], exclude: [] } });
  8491. await cg.indexAll();
  8492. cg.resolveReferences();
  8493. expect(cg.getFileDependents('p/MyAnno.java')).toContain('p/User.java');
  8494. cg.destroy();
  8495. } finally { cleanupTempDir(dir); }
  8496. });
  8497. });
  8498. describe('Swift property wrappers / attributes (blast-radius recall)', () => {
  8499. it('links a @propertyWrapper usage to the wrapper type', async () => {
  8500. const dir = createTempDir();
  8501. try {
  8502. fs.mkdirSync(path.join(dir, 'Sources', 'M'), { recursive: true });
  8503. fs.writeFileSync(path.join(dir, 'Sources', 'M', 'Wrap.swift'), `@propertyWrapper\npublic struct Argument<T> { public var wrappedValue: T }\n`);
  8504. // `@Argument` is a Swift attribute on a stored property — it lives in the
  8505. // property's `modifiers` and Swift doesn't extract instance properties as
  8506. // their own nodes, so without the fix the wrapper type has no users.
  8507. fs.writeFileSync(path.join(dir, 'Sources', 'M', 'Cmd.swift'), `public struct MyCommand {\n @Argument var name: String\n @Argument var count: Int\n}\n`);
  8508. const cg = CodeGraph.initSync(dir, { config: { include: ['Sources/**/*.swift'], exclude: [] } });
  8509. await cg.indexAll();
  8510. cg.resolveReferences();
  8511. expect(cg.getFileDependents('Sources/M/Wrap.swift')).toContain('Sources/M/Cmd.swift');
  8512. cg.destroy();
  8513. } finally { cleanupTempDir(dir); }
  8514. });
  8515. });
  8516. describe('R Extraction', () => {
  8517. describe('Language detection', () => {
  8518. it('should detect R files (both extension cases)', () => {
  8519. expect(detectLanguage('analysis.R')).toBe('r');
  8520. expect(detectLanguage('scripts/clean.r')).toBe('r');
  8521. });
  8522. it('should report R as supported', () => {
  8523. expect(isLanguageSupported('r')).toBe(true);
  8524. expect(getSupportedLanguages()).toContain('r');
  8525. });
  8526. });
  8527. describe('Function extraction', () => {
  8528. it('extracts every assignment form, lambdas, and nested functions', () => {
  8529. const code = `
  8530. clean_data <- function(df, threshold = 0.5) {
  8531. helper <- function(d) scale(d)
  8532. helper(df)
  8533. }
  8534. normalize = function(v) (v - mean(v)) / sd(v)
  8535. double_it <- \\(x) x * 2
  8536. `;
  8537. const result = extractFromSource('analysis.R', code);
  8538. const funcs = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name);
  8539. expect(funcs).toContain('clean_data');
  8540. expect(funcs).toContain('normalize');
  8541. expect(funcs).toContain('double_it');
  8542. expect(funcs).toContain('helper'); // nested, inside clean_data's scope
  8543. const cleanData = result.nodes.find((n) => n.name === 'clean_data');
  8544. expect(cleanData?.language).toBe('r');
  8545. expect(cleanData?.signature).toBe('(df, threshold = 0.5)');
  8546. });
  8547. it('attributes body calls to the enclosing function', () => {
  8548. const code = `
  8549. prep <- function(d) scale(d)
  8550. fit_model <- function(data) {
  8551. lm(y ~ x, data = prep(data))
  8552. }
  8553. `;
  8554. const result = extractFromSource('models.R', code);
  8555. const prepCall = result.unresolvedReferences.find(
  8556. (r) => r.referenceName === 'prep' && r.referenceKind === 'calls'
  8557. );
  8558. expect(prepCall).toBeDefined();
  8559. const fitModel = result.nodes.find((n) => n.name === 'fit_model');
  8560. expect(prepCall?.fromNodeId).toBe(fitModel?.id);
  8561. });
  8562. });
  8563. describe('Imports', () => {
  8564. it('extracts library/require/source as imports, not calls', () => {
  8565. const code = `
  8566. library(dplyr)
  8567. require(stats)
  8568. requireNamespace("jsonlite")
  8569. source("helpers.R")
  8570. `;
  8571. const result = extractFromSource('main.R', code);
  8572. const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name);
  8573. expect(imports).toContain('dplyr');
  8574. expect(imports).toContain('stats');
  8575. expect(imports).toContain('jsonlite');
  8576. expect(imports).toContain('helpers.R');
  8577. // Claimed by the hook — no call references to the import machinery.
  8578. const libCalls = result.unresolvedReferences.filter(
  8579. (r) => r.referenceKind === 'calls' && (r.referenceName === 'library' || r.referenceName === 'source')
  8580. );
  8581. expect(libCalls).toHaveLength(0);
  8582. });
  8583. });
  8584. describe('Variables and constants', () => {
  8585. it('extracts top-level assignments; ALL_CAPS as constants; right-assign too', () => {
  8586. const code = `
  8587. ALPHA <- 0.05
  8588. max_iter = 100
  8589. compute_stats(df) -> stats_result
  8590. inner <- function() {
  8591. local_var <- 1
  8592. }
  8593. `;
  8594. const result = extractFromSource('config.R', code);
  8595. const constant = result.nodes.find((n) => n.name === 'ALPHA');
  8596. expect(constant?.kind).toBe('constant');
  8597. const variable = result.nodes.find((n) => n.name === 'max_iter');
  8598. expect(variable?.kind).toBe('variable');
  8599. const rightAssigned = result.nodes.find((n) => n.name === 'stats_result');
  8600. expect(rightAssigned?.kind).toBe('variable');
  8601. // Locals inside functions are deliberately NOT extracted.
  8602. expect(result.nodes.find((n) => n.name === 'local_var')).toBeUndefined();
  8603. });
  8604. });
  8605. describe('Classes', () => {
  8606. it('extracts S4/R5/R6 class calls as classes with their list methods', () => {
  8607. const code = `
  8608. setClass("Patient", representation(id = "character"))
  8609. Account <- setRefClass("Account",
  8610. fields = list(balance = "numeric"),
  8611. methods = list(deposit = function(x) { balance <<- balance + x })
  8612. )
  8613. Stack <- R6Class("Stack",
  8614. public = list(push = function(v) invisible(v))
  8615. )
  8616. setGeneric("describe", function(obj) standardGeneric("describe"))
  8617. setMethod("describe", "Patient", function(obj) paste(obj@id))
  8618. `;
  8619. const result = extractFromSource('classes.R', code);
  8620. const classes = result.nodes.filter((n) => n.kind === 'class').map((n) => n.name);
  8621. expect(classes).toContain('Patient');
  8622. expect(classes).toContain('Account');
  8623. expect(classes).toContain('Stack');
  8624. const methods = result.nodes.filter((n) => n.kind === 'method').map((n) => n.name);
  8625. expect(methods).toContain('deposit');
  8626. expect(methods).toContain('push');
  8627. // setGeneric/setMethod produce functions named by their string argument.
  8628. const describes = result.nodes.filter((n) => n.name === 'describe' && n.kind === 'function');
  8629. expect(describes.length).toBeGreaterThanOrEqual(2);
  8630. // The class-assignment idiom must not ALSO produce a variable node.
  8631. expect(result.nodes.find((n) => n.name === 'Account' && n.kind === 'variable')).toBeUndefined();
  8632. });
  8633. it('extracts ggproto classes with direct-arg methods and the parent as extends', () => {
  8634. // ggplot2's OO system — every Geom/Stat/Scale in the ecosystem uses it.
  8635. const code = `
  8636. GeomPoint <- ggproto("GeomPoint", Geom,
  8637. required_aes = c("x", "y"),
  8638. draw_panel = function(data, panel_params, coord) {
  8639. coords <- coord$transform(data, panel_params)
  8640. grid::pointsGrob(coords$x, coords$y)
  8641. },
  8642. draw_key = draw_key_point
  8643. )
  8644. `;
  8645. const result = extractFromSource('geom-point.R', code);
  8646. const cls = result.nodes.find((n) => n.name === 'GeomPoint' && n.kind === 'class');
  8647. expect(cls).toBeDefined();
  8648. const method = result.nodes.find((n) => n.name === 'draw_panel' && n.kind === 'method');
  8649. expect(method).toBeDefined();
  8650. const ext = result.unresolvedReferences.find(
  8651. (r) => r.referenceKind === 'extends' && r.referenceName === 'Geom'
  8652. );
  8653. expect(ext?.fromNodeId).toBe(cls?.id);
  8654. // No twin variable for the assignment.
  8655. expect(result.nodes.find((n) => n.name === 'GeomPoint' && n.kind === 'variable')).toBeUndefined();
  8656. });
  8657. });
  8658. });
  8659. // =============================================================================
  8660. // CFML (ColdFusion Markup Language — .cfc/.cfm tag-based and bare-script, .cfs)
  8661. // =============================================================================
  8662. describe('CFML Extraction', () => {
  8663. describe('Language detection', () => {
  8664. it('should detect .cfc/.cfm as cfml and .cfs as cfscript', () => {
  8665. expect(detectLanguage('Service.cfc')).toBe('cfml');
  8666. expect(detectLanguage('index.cfm')).toBe('cfml');
  8667. expect(detectLanguage('Helper.cfs')).toBe('cfscript');
  8668. });
  8669. it('should report cfml and cfscript as supported', () => {
  8670. expect(isLanguageSupported('cfml')).toBe(true);
  8671. expect(isLanguageSupported('cfscript')).toBe(true);
  8672. expect(getSupportedLanguages()).toContain('cfml');
  8673. expect(getSupportedLanguages()).toContain('cfscript');
  8674. });
  8675. });
  8676. describe('Bare-script .cfc (component { ... })', () => {
  8677. const code = `
  8678. component extends="BaseService" implements="IService" {
  8679. property name="name" type="string";
  8680. function init(required string name) {
  8681. variables.name = arguments.name;
  8682. return this;
  8683. }
  8684. public string function getName() {
  8685. return variables.name;
  8686. }
  8687. private void function logSomething(required string msg) {
  8688. writeLog(text=msg);
  8689. }
  8690. }
  8691. `;
  8692. it('should name the component from the file name (the grammar has no name field)', () => {
  8693. const result = extractFromSource('SampleService.cfc', code);
  8694. const cls = result.nodes.find((n) => n.kind === 'class');
  8695. expect(cls).toBeDefined();
  8696. expect(cls?.name).toBe('SampleService');
  8697. expect(cls?.language).toBe('cfml');
  8698. });
  8699. it('should extract methods with visibility and contains edges to the class', () => {
  8700. const result = extractFromSource('SampleService.cfc', code);
  8701. const cls = result.nodes.find((n) => n.kind === 'class');
  8702. const methods = result.nodes.filter((n) => n.kind === 'method');
  8703. expect(methods.map((m) => m.name)).toEqual(
  8704. expect.arrayContaining(['init', 'getName', 'logSomething'])
  8705. );
  8706. const logSomething = methods.find((m) => m.name === 'logSomething');
  8707. expect(logSomething?.visibility).toBe('private');
  8708. const containsLog = result.edges.find(
  8709. (e) => e.source === cls?.id && e.target === logSomething?.id && e.kind === 'contains'
  8710. );
  8711. expect(containsLog).toBeDefined();
  8712. });
  8713. it('should extract extends/implements as unresolved references from the class', () => {
  8714. const result = extractFromSource('SampleService.cfc', code);
  8715. const cls = result.nodes.find((n) => n.kind === 'class');
  8716. const extendsRef = result.unresolvedReferences.find((r) => r.referenceKind === 'extends');
  8717. expect(extendsRef?.referenceName).toBe('BaseService');
  8718. expect(extendsRef?.fromNodeId).toBe(cls?.id);
  8719. const implRef = result.unresolvedReferences.find((r) => r.referenceKind === 'implements');
  8720. expect(implRef?.referenceName).toBe('IService');
  8721. expect(implRef?.fromNodeId).toBe(cls?.id);
  8722. });
  8723. });
  8724. describe('Standalone .cfs (pure CFScript)', () => {
  8725. it('should also name an anonymous component from the file name', () => {
  8726. const code = `
  8727. component {
  8728. function ping() {
  8729. return "pong";
  8730. }
  8731. }
  8732. `;
  8733. const result = extractFromSource('Sample.cfs', code);
  8734. const cls = result.nodes.find((n) => n.kind === 'class');
  8735. expect(cls).toBeDefined();
  8736. expect(cls?.name).toBe('Sample');
  8737. expect(cls?.language).toBe('cfscript');
  8738. });
  8739. it('should extract top-level imports with no enclosing component', () => {
  8740. const code = `
  8741. import com.foo.Bar;
  8742. import foo.cfm;
  8743. `;
  8744. const result = extractFromSource('Includes.cfs', code);
  8745. const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name);
  8746. expect(imports).toContain('com.foo.Bar');
  8747. expect(imports).toContain('foo.cfm');
  8748. });
  8749. });
  8750. describe('Tag-based .cfc (<cfcomponent>/<cffunction>)', () => {
  8751. const code = `<cfcomponent extends="Base" implements="IFoo,IBar" output="false">
  8752. \t<cffunction name="getName" access="public" returntype="string">
  8753. \t\t<cfreturn this.name>
  8754. \t</cffunction>
  8755. \t<cffunction name="doWork" access="private" returntype="void">
  8756. \t\t<cfscript>
  8757. \t\t\tvar x = helper();
  8758. \t\t\tanotherCall(x);
  8759. \t\t</cfscript>
  8760. \t</cffunction>
  8761. </cfcomponent>
  8762. `;
  8763. it('should name the component from the file name when the tag has no name attribute', () => {
  8764. const result = extractFromSource('TagStyle.cfc', code);
  8765. const cls = result.nodes.find((n) => n.kind === 'class');
  8766. expect(cls?.name).toBe('TagStyle');
  8767. expect(cls?.language).toBe('cfml');
  8768. });
  8769. it('should prefer an explicit name attribute on the cfcomponent tag', () => {
  8770. const named = `<cfcomponent name="ExplicitName">\n<cffunction name="a"><cfreturn 1></cffunction>\n</cfcomponent>`;
  8771. const result = extractFromSource('File.cfc', named);
  8772. expect(result.nodes.find((n) => n.kind === 'class')?.name).toBe('ExplicitName');
  8773. });
  8774. it('should extract cffunction tags as methods with access-derived visibility', () => {
  8775. const result = extractFromSource('TagStyle.cfc', code);
  8776. const methods = result.nodes.filter((n) => n.kind === 'method');
  8777. expect(methods.map((m) => m.name)).toEqual(expect.arrayContaining(['getName', 'doWork']));
  8778. const getName = methods.find((m) => m.name === 'getName');
  8779. expect(getName?.visibility).toBe('public');
  8780. expect(getName?.returnType).toBe('string');
  8781. const doWork = methods.find((m) => m.name === 'doWork');
  8782. expect(doWork?.visibility).toBe('private');
  8783. });
  8784. it('should not double-extract symbols from the component body (implicit-end-tag walk)', () => {
  8785. const result = extractFromSource('TagStyle.cfc', code);
  8786. const methods = result.nodes.filter((n) => n.kind === 'method' && n.name === 'getName');
  8787. expect(methods).toHaveLength(1);
  8788. const doWorkMethods = result.nodes.filter((n) => n.kind === 'method' && n.name === 'doWork');
  8789. expect(doWorkMethods).toHaveLength(1);
  8790. });
  8791. it('should delegate <cfscript> tag bodies to the cfscript grammar and attribute calls to the enclosing method', () => {
  8792. const result = extractFromSource('TagStyle.cfc', code);
  8793. const doWork = result.nodes.find((n) => n.kind === 'method' && n.name === 'doWork');
  8794. const helperCall = result.unresolvedReferences.find(
  8795. (r) => r.referenceKind === 'calls' && r.referenceName === 'helper'
  8796. );
  8797. expect(helperCall?.fromNodeId).toBe(doWork?.id);
  8798. });
  8799. it('should produce exactly one correctly-ranged file node, not a leaked snippet-scoped one', () => {
  8800. const result = extractFromSource('TagStyle.cfc', code);
  8801. const fileNodes = result.nodes.filter((n) => n.kind === 'file');
  8802. expect(fileNodes).toHaveLength(1);
  8803. expect(fileNodes[0].startLine).toBe(1);
  8804. const cls = result.nodes.find((n) => n.kind === 'class');
  8805. const containsClass = result.edges.find(
  8806. (e) => e.source === fileNodes[0].id && e.target === cls?.id && e.kind === 'contains'
  8807. );
  8808. expect(containsClass).toBeDefined();
  8809. });
  8810. });
  8811. describe('Top-level cffunction with no enclosing cfcomponent (.cfm template)', () => {
  8812. it('should extract as a top-level function contained by the file', () => {
  8813. const code = `<cffunction name="helper" access="public" returntype="string">
  8814. \t<cfreturn "hi">
  8815. </cffunction>
  8816. `;
  8817. const result = extractFromSource('helper.cfm', code);
  8818. const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'helper');
  8819. expect(fn).toBeDefined();
  8820. const fileNode = result.nodes.find((n) => n.kind === 'file');
  8821. const containsFn = result.edges.find(
  8822. (e) => e.source === fileNode?.id && e.target === fn?.id && e.kind === 'contains'
  8823. );
  8824. expect(containsFn).toBeDefined();
  8825. });
  8826. });
  8827. describe('<cfscript> nested inside control-flow tags (<cfif>/<cfloop>/<cftry>)', () => {
  8828. it('should delegate a <cfscript> body nested inside <cfif> within a <cffunction>', () => {
  8829. const code = `<cfcomponent>
  8830. <cffunction name="doStuff">
  8831. <cfif true>
  8832. <cfscript>
  8833. helper();
  8834. </cfscript>
  8835. </cfif>
  8836. </cffunction>
  8837. </cfcomponent>
  8838. `;
  8839. const result = extractFromSource('Nested.cfc', code);
  8840. const doStuff = result.nodes.find((n) => n.kind === 'method' && n.name === 'doStuff');
  8841. expect(doStuff).toBeDefined();
  8842. const helperCall = result.unresolvedReferences.find(
  8843. (r) => r.referenceKind === 'calls' && r.referenceName === 'helper'
  8844. );
  8845. expect(helperCall?.fromNodeId).toBe(doStuff?.id);
  8846. });
  8847. it('should delegate a <cfscript> body nested inside <cfif> at top-level component scope', () => {
  8848. const code = `<cfcomponent>
  8849. <cfif true>
  8850. <cfscript>
  8851. topLevelHelper();
  8852. </cfscript>
  8853. </cfif>
  8854. </cfcomponent>
  8855. `;
  8856. const result = extractFromSource('Nested2.cfc', code);
  8857. const cls = result.nodes.find((n) => n.kind === 'class');
  8858. expect(cls).toBeDefined();
  8859. const helperCall = result.unresolvedReferences.find(
  8860. (r) => r.referenceKind === 'calls' && r.referenceName === 'topLevelHelper'
  8861. );
  8862. expect(helperCall?.fromNodeId).toBe(cls?.id);
  8863. });
  8864. });
  8865. describe('<cfquery> SQL bodies (cfquery grammar)', () => {
  8866. it('should extract a call expression embedded in a #hash# inside the SQL body', () => {
  8867. const code = `<cfcomponent>
  8868. <cffunction name="getUsers">
  8869. <cfquery name="qUsers" datasource="#variables.dsn#">
  8870. SELECT id, name FROM users WHERE owner = #getCurrentUser().getId()#
  8871. </cfquery>
  8872. <cfreturn qUsers>
  8873. </cffunction>
  8874. </cfcomponent>
  8875. `;
  8876. const result = extractFromSource('Query.cfc', code);
  8877. const getUsers = result.nodes.find((n) => n.kind === 'method' && n.name === 'getUsers');
  8878. expect(getUsers).toBeDefined();
  8879. const getCurrentUserCall = result.unresolvedReferences.find(
  8880. (r) => r.referenceKind === 'calls' && r.referenceName === 'getCurrentUser'
  8881. );
  8882. expect(getCurrentUserCall?.fromNodeId).toBe(getUsers?.id);
  8883. const getIdCall = result.unresolvedReferences.find(
  8884. (r) => r.referenceKind === 'calls' && r.referenceName === 'getId'
  8885. );
  8886. expect(getIdCall?.fromNodeId).toBe(getUsers?.id);
  8887. });
  8888. });
  8889. describe('UTF-8 BOM handling (common in CFML saved by Windows editors)', () => {
  8890. it('should route a BOM-prefixed tag-based .cfc to the tag grammar, not the script grammar', () => {
  8891. const code = `\uFEFF<cfcomponent output="false">\n<cffunction name="configure" access="public">\n<cfreturn 1>\n</cffunction>\n</cfcomponent>\n`;
  8892. const result = extractFromSource('ModuleConfig.cfc', code);
  8893. const cls = result.nodes.find((n) => n.kind === 'class');
  8894. expect(cls?.name).toBe('ModuleConfig');
  8895. const configure = result.nodes.find((n) => n.kind === 'method' && n.name === 'configure');
  8896. expect(configure).toBeDefined();
  8897. });
  8898. it('should still treat a BOM-prefixed bare-script .cfc as script', () => {
  8899. const code = `\uFEFFcomponent {\n function ping() { return "pong"; }\n}\n`;
  8900. const result = extractFromSource('Ping.cfc', code);
  8901. expect(result.nodes.find((n) => n.kind === 'class')?.name).toBe('Ping');
  8902. expect(result.nodes.find((n) => n.kind === 'method')?.name).toBe('ping');
  8903. });
  8904. });
  8905. describe('Unquoted tag attribute values (legal in older CFML)', () => {
  8906. it('should extract functions and inheritance from unquoted attributes', () => {
  8907. const code = `<cfcomponent extends=Base>\n<cffunction name=doThing access=private>\n<cfreturn 1>\n</cffunction>\n</cfcomponent>\n`;
  8908. const result = extractFromSource('Unquoted.cfc', code);
  8909. const doThing = result.nodes.find((n) => n.kind === 'method' && n.name === 'doThing');
  8910. expect(doThing).toBeDefined();
  8911. expect(doThing?.visibility).toBe('private');
  8912. const extendsRef = result.unresolvedReferences.find((r) => r.referenceKind === 'extends');
  8913. expect(extendsRef?.referenceName).toBe('Base');
  8914. });
  8915. });
  8916. describe('Functions in a component-level <cfscript> block', () => {
  8917. it('should classify them as methods of the component (ColdBox ModuleConfig shape)', () => {
  8918. const code = `<cfcomponent output="false">\n<cfscript>\nfunction configure() {\n return settings();\n}\nfunction onLoad() {\n return 1;\n}\n</cfscript>\n</cfcomponent>\n`;
  8919. const result = extractFromSource('ModuleConfig.cfc', code);
  8920. const cls = result.nodes.find((n) => n.kind === 'class');
  8921. const methods = result.nodes.filter((n) => n.kind === 'method').map((n) => n.name);
  8922. expect(methods).toEqual(expect.arrayContaining(['configure', 'onLoad']));
  8923. expect(result.nodes.filter((n) => n.kind === 'function')).toHaveLength(0);
  8924. const configure = result.nodes.find((n) => n.kind === 'method' && n.name === 'configure');
  8925. const containsEdge = result.edges.find(
  8926. (e) => e.source === cls?.id && e.target === configure?.id && e.kind === 'contains'
  8927. );
  8928. expect(containsEdge).toBeDefined();
  8929. });
  8930. it('should keep kind function for a <cfscript> inside a cffunction body', () => {
  8931. const code = `<cfcomponent>\n<cffunction name="outer">\n<cfscript>\nfunction innerHelper() { return 1; }\n</cfscript>\n</cffunction>\n</cfcomponent>\n`;
  8932. const result = extractFromSource('Outer.cfc', code);
  8933. expect(result.nodes.find((n) => n.name === 'outer')?.kind).toBe('method');
  8934. expect(result.nodes.find((n) => n.name === 'innerHelper')?.kind).toBe('function');
  8935. });
  8936. });
  8937. });
  8938. describe('COBOL Extraction', () => {
  8939. it('should detect .cbl/.cob/.cpy as cobol (case-insensitive)', () => {
  8940. expect(detectLanguage('app/cbl/CBACT01C.cbl')).toBe('cobol');
  8941. expect(detectLanguage('app/cbl/CBSTM03A.CBL')).toBe('cobol');
  8942. expect(detectLanguage('prog.cob')).toBe('cobol');
  8943. expect(detectLanguage('app/cpy/CVACT01Y.cpy')).toBe('cobol');
  8944. expect(isSourceFile('CBACT01C.cbl')).toBe(true);
  8945. });
  8946. const FIXED = (body: string) =>
  8947. body
  8948. .split('\n')
  8949. .map((l) => (l.length > 0 ? ' ' + l : l))
  8950. .join('\n');
  8951. const PROGRAM = FIXED(`IDENTIFICATION DIVISION.
  8952. PROGRAM-ID. TESTPROG.
  8953. DATA DIVISION.
  8954. WORKING-STORAGE SECTION.
  8955. 01 WS-TOTALS.
  8956. 05 WS-COUNT PIC 9(4) VALUE ZERO.
  8957. 88 WS-DONE VALUE 'Y'.
  8958. 77 WS-FLAG PIC X.
  8959. COPY CVACT01Y.
  8960. PROCEDURE DIVISION.
  8961. MAIN-SECTION SECTION.
  8962. 0000-MAIN.
  8963. PERFORM 1000-INIT
  8964. PERFORM 2000-PROCESS THRU 2000-EXIT
  8965. CALL 'CBACT01C' USING WS-TOTALS
  8966. CALL WS-FLAG
  8967. GO TO 9999-END
  8968. .
  8969. 1000-INIT.
  8970. MOVE ZERO TO WS-COUNT.
  8971. 2000-PROCESS.
  8972. EXEC CICS LINK PROGRAM('COCOM01C') COMMAREA(WS-TOTALS)
  8973. END-EXEC.
  8974. 2000-EXIT.
  8975. EXIT.
  8976. 9999-END.
  8977. GOBACK.
  8978. `);
  8979. it('should extract the program as a module node', () => {
  8980. const result = extractFromSource('TESTPROG.cbl', PROGRAM);
  8981. const moduleNode = result.nodes.find((n) => n.kind === 'module');
  8982. expect(moduleNode).toBeDefined();
  8983. expect(moduleNode?.name).toBe('TESTPROG');
  8984. });
  8985. it('should extract sections and paragraphs as functions with reconstructed extents', () => {
  8986. const result = extractFromSource('TESTPROG.cbl', PROGRAM);
  8987. const fns = result.nodes.filter((n) => n.kind === 'function');
  8988. const names = fns.map((f) => f.name);
  8989. expect(names).toContain('MAIN-SECTION');
  8990. expect(names).toContain('0000-MAIN');
  8991. expect(names).toContain('2000-PROCESS');
  8992. // A paragraph spans from its header to the next header, not just one line.
  8993. const main = fns.find((f) => f.name === '0000-MAIN');
  8994. expect(main).toBeDefined();
  8995. expect(main!.endLine).toBeGreaterThan(main!.startLine + 3);
  8996. // Paragraphs are contained in their section (qualified name includes it).
  8997. expect(main!.qualifiedName).toContain('MAIN-SECTION');
  8998. });
  8999. it('should extract PERFORM, PERFORM THRU, GO TO, and CALL literal as calls references', () => {
  9000. const result = extractFromSource('TESTPROG.cbl', PROGRAM);
  9001. const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls');
  9002. const targets = calls.map((c) => c.referenceName);
  9003. expect(targets).toContain('1000-INIT');
  9004. expect(targets).toContain('2000-PROCESS'); // PERFORM ... THRU start
  9005. expect(targets).toContain('2000-EXIT'); // PERFORM ... THRU end
  9006. expect(targets).toContain('9999-END'); // GO TO
  9007. expect(targets).toContain('CBACT01C'); // CALL 'literal'
  9008. expect(targets).toContain('COCOM01C'); // EXEC CICS LINK PROGRAM('...')
  9009. // Dynamic CALL through a data name is skipped — announce, don't guess.
  9010. expect(targets).not.toContain('WS-FLAG');
  9011. });
  9012. it('should extract COPY as an import node and imports reference', () => {
  9013. const result = extractFromSource('TESTPROG.cbl', PROGRAM);
  9014. const importNode = result.nodes.find((n) => n.kind === 'import');
  9015. expect(importNode).toBeDefined();
  9016. expect(importNode?.name).toBe('CVACT01Y');
  9017. const importRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'imports');
  9018. expect(importRefs.map((r) => r.referenceName)).toContain('CVACT01Y');
  9019. });
  9020. it('should extract data items as variables, fields, and 88-level constants', () => {
  9021. const result = extractFromSource('TESTPROG.cbl', PROGRAM);
  9022. const group = result.nodes.find((n) => n.name === 'WS-TOTALS');
  9023. expect(group?.kind).toBe('variable');
  9024. const nested = result.nodes.find((n) => n.name === 'WS-COUNT');
  9025. expect(nested?.kind).toBe('field');
  9026. expect(nested?.qualifiedName).toContain('WS-TOTALS');
  9027. const condition = result.nodes.find((n) => n.name === 'WS-DONE');
  9028. expect(condition?.kind).toBe('constant');
  9029. const standalone = result.nodes.find((n) => n.name === 'WS-FLAG');
  9030. expect(standalone?.kind).toBe('variable');
  9031. });
  9032. it('should extract EXEC SQL INCLUDE as an import', () => {
  9033. const code = FIXED(`IDENTIFICATION DIVISION.
  9034. PROGRAM-ID. SQLPROG.
  9035. DATA DIVISION.
  9036. WORKING-STORAGE SECTION.
  9037. EXEC SQL INCLUDE SQLCA END-EXEC.
  9038. 01 WS-X PIC X.
  9039. PROCEDURE DIVISION.
  9040. P1.
  9041. GOBACK.
  9042. `);
  9043. const result = extractFromSource('SQLPROG.cbl', code);
  9044. const importNode = result.nodes.find((n) => n.kind === 'import' && n.name === 'SQLCA');
  9045. expect(importNode).toBeDefined();
  9046. // The EXEC block must not break the rest of the file.
  9047. expect(result.nodes.find((n) => n.name === 'WS-X')).toBeDefined();
  9048. });
  9049. it('should extract a standalone data copybook (.cpy fragment)', () => {
  9050. const code = FIXED(`01 ACCOUNT-RECORD.
  9051. 05 ACCT-ID PIC 9(11).
  9052. 05 ACCT-CURR-BAL PIC S9(10)V99.
  9053. `);
  9054. const result = extractFromSource('CVACT01Y.cpy', code);
  9055. const record = result.nodes.find((n) => n.name === 'ACCOUNT-RECORD');
  9056. expect(record?.kind).toBe('variable');
  9057. const field = result.nodes.find((n) => n.name === 'ACCT-ID');
  9058. expect(field?.kind).toBe('field');
  9059. });
  9060. it('should extract a procedure copybook (.cpy fragment with paragraphs)', () => {
  9061. const code = FIXED(`EDIT-DATE.
  9062. MOVE 1 TO WS-X
  9063. PERFORM VALIDATE-YEAR.
  9064. VALIDATE-YEAR.
  9065. CONTINUE.
  9066. `);
  9067. const result = extractFromSource('CSUTLDPY.cpy', code);
  9068. const fns = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name);
  9069. expect(fns).toContain('EDIT-DATE');
  9070. expect(fns).toContain('VALIDATE-YEAR');
  9071. const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls');
  9072. expect(calls.map((c) => c.referenceName)).toContain('VALIDATE-YEAR');
  9073. });
  9074. it('should emit write-site references for MOVE/ADD/COMPUTE targets', () => {
  9075. const code = FIXED(`IDENTIFICATION DIVISION.
  9076. PROGRAM-ID. WRITEREF.
  9077. DATA DIVISION.
  9078. WORKING-STORAGE SECTION.
  9079. 01 WS-TOTAL PIC S9(7)V99.
  9080. 01 WS-COUNT PIC 9(4).
  9081. PROCEDURE DIVISION.
  9082. P1.
  9083. MOVE ZERO TO WS-TOTAL
  9084. ADD 1 TO WS-COUNT
  9085. COMPUTE WS-TOTAL = WS-TOTAL + 1
  9086. SUBTRACT 1 FROM WS-COUNT
  9087. MOVE 1 TO RETURN-CODE.
  9088. `);
  9089. const result = extractFromSource('WRITEREF.cbl', code);
  9090. const writes = result.unresolvedReferences.filter((r) => r.referenceKind === 'references');
  9091. const names = writes.map((w) => w.referenceName);
  9092. expect(names.filter((n) => n === 'WS-TOTAL').length).toBeGreaterThanOrEqual(2); // MOVE + COMPUTE
  9093. expect(names).toContain('WS-COUNT'); // ADD and SUBTRACT targets
  9094. // Special registers carry no declaration — never referenced.
  9095. expect(names).not.toContain('RETURN-CODE');
  9096. });
  9097. it('should emit cics-transid references for RETURN TRANSID, literal and via same-file VALUE', () => {
  9098. const code = FIXED(`IDENTIFICATION DIVISION.
  9099. PROGRAM-ID. TXPROG.
  9100. DATA DIVISION.
  9101. WORKING-STORAGE SECTION.
  9102. 01 WS-TRANID PIC X(04) VALUE 'CB00'.
  9103. PROCEDURE DIVISION.
  9104. P1.
  9105. EXEC CICS RETURN TRANSID('CC00') COMMAREA(WS-X) END-EXEC
  9106. .
  9107. P2.
  9108. EXEC CICS RETURN TRANSID(WS-TRANID) END-EXEC
  9109. .
  9110. P3.
  9111. EXEC CICS XCTL PROGRAM(WS-UNKNOWN-VAR) END-EXEC
  9112. .
  9113. `);
  9114. const result = extractFromSource('TXPROG.cbl', code);
  9115. const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls');
  9116. const names = calls.map((c) => c.referenceName);
  9117. expect(names).toContain('cics-transid:CC00'); // literal
  9118. expect(names).toContain('cics-transid:CB00'); // dereferenced through WS-TRANID
  9119. // Un-derefable program variable: dynamic dispatch, no guessed edge.
  9120. expect(names.filter((n) => n.startsWith('cics-transid:')).length).toBe(2);
  9121. });
  9122. it('should shift free-format source so it still extracts (preParse)', () => {
  9123. const code = `IDENTIFICATION DIVISION.
  9124. PROGRAM-ID. FREEPROG.
  9125. PROCEDURE DIVISION.
  9126. DO-WORK.
  9127. DISPLAY 'HI'.
  9128. `;
  9129. const result = extractFromSource('freeprog.cbl', code);
  9130. expect(result.nodes.find((n) => n.kind === 'module')?.name).toBe('FREEPROG');
  9131. expect(result.nodes.find((n) => n.kind === 'function')?.name).toBe('DO-WORK');
  9132. });
  9133. });
  9134. // =============================================================================
  9135. // VB.NET (.vb) — vendored patched govindbanura/tree-sitter-vbnet grammar
  9136. // =============================================================================
  9137. describe('VB.NET Extraction', () => {
  9138. it('should detect .vb as vbnet', () => {
  9139. expect(detectLanguage('Service.vb')).toBe('vbnet');
  9140. expect(detectLanguage('app/Forms/MainForm.vb')).toBe('vbnet');
  9141. expect(isSourceFile('Service.vb')).toBe(true);
  9142. });
  9143. const SAMPLE = `Imports System
  9144. Imports System.Collections.Generic
  9145. Namespace Acme.Billing
  9146. Public Interface IRepository
  9147. Function GetById(ByVal id As Integer) As Invoice
  9148. End Interface
  9149. Public Enum InvoiceState
  9150. Draft = 0
  9151. Sent
  9152. Paid
  9153. End Enum
  9154. Public Structure Money
  9155. Public Amount As Decimal
  9156. End Structure
  9157. Public MustInherit Class EntityBase
  9158. Public Property Id As Integer
  9159. End Class
  9160. Public Class Invoice
  9161. Inherits EntityBase
  9162. Implements IRepository
  9163. Private ReadOnly _lines As New List(Of String)
  9164. Public Const MaxLines As Integer = 100
  9165. Public Event Paid(ByVal amount As Decimal)
  9166. Public Property State As InvoiceState
  9167. Public Sub New(ByVal id As Integer)
  9168. Me.Id = id
  9169. End Sub
  9170. Public Function GetById(ByVal id As Integer) As Invoice Implements IRepository.GetById
  9171. Return New Invoice(id)
  9172. End Function
  9173. Public Sub AddLine(ByVal description As String)
  9174. _lines.Add(description)
  9175. Validate(description)
  9176. End Sub
  9177. Private Sub Validate(ByVal text As String)
  9178. If text.Length > MaxLines Then Throw New ArgumentException("too long")
  9179. End Sub
  9180. End Class
  9181. ' lowercase keywords: VB is case-insensitive
  9182. public module Helpers
  9183. public function Twice(byval n as integer) as integer
  9184. return n * 2
  9185. end function
  9186. Public Sub Run()
  9187. Dim inv = New Invoice(1)
  9188. inv.AddLine("widget")
  9189. Dim d As New Dictionary(Of String, Integer)
  9190. Helpers.Twice(21)
  9191. End Sub
  9192. end module
  9193. End Namespace
  9194. `;
  9195. it('should extract classes, modules, interfaces, structures, and enums', () => {
  9196. const result = extractFromSource('Invoice.vb', SAMPLE);
  9197. const kinds = (kind: string) => result.nodes.filter((n) => n.kind === kind).map((n) => n.name);
  9198. expect(kinds('class')).toEqual(expect.arrayContaining(['EntityBase', 'Invoice', 'Helpers']));
  9199. expect(kinds('interface')).toContain('IRepository');
  9200. expect(kinds('struct')).toContain('Money');
  9201. expect(kinds('enum')).toContain('InvoiceState');
  9202. expect(kinds('enum_member')).toEqual(expect.arrayContaining(['Draft', 'Sent', 'Paid']));
  9203. });
  9204. it('should extract methods, constructors, properties, fields, and events (case-insensitive keywords)', () => {
  9205. const result = extractFromSource('Invoice.vb', SAMPLE);
  9206. const methods = result.nodes.filter((n) => n.kind === 'method').map((n) => n.name);
  9207. expect(methods).toEqual(expect.arrayContaining(['GetById', 'AddLine', 'Validate', 'Twice', 'Run']));
  9208. const props = result.nodes.filter((n) => n.kind === 'property').map((n) => n.name);
  9209. expect(props).toEqual(expect.arrayContaining(['Id', 'State']));
  9210. const fields = result.nodes.filter((n) => n.kind === 'field' || n.kind === 'constant').map((n) => n.name);
  9211. expect(fields).toEqual(expect.arrayContaining(['_lines', 'MaxLines']));
  9212. // Event declarations index as findable members
  9213. expect(fields).toContain('Paid');
  9214. });
  9215. it('should qualify types with their namespace', () => {
  9216. const result = extractFromSource('Invoice.vb', SAMPLE);
  9217. const invoice = result.nodes.find((n) => n.kind === 'class' && n.name === 'Invoice');
  9218. expect(invoice?.qualifiedName).toContain('Acme.Billing');
  9219. });
  9220. it('should emit Inherits as extends and Implements as implements references', () => {
  9221. const result = extractFromSource('Invoice.vb', SAMPLE);
  9222. const extendsRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'extends');
  9223. expect(extendsRefs.map((r) => r.referenceName)).toContain('EntityBase');
  9224. const implementsRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'implements');
  9225. expect(implementsRefs.map((r) => r.referenceName)).toContain('IRepository');
  9226. });
  9227. it('should extract calls through both invocation and index-shaped parens', () => {
  9228. const result = extractFromSource('Invoice.vb', SAMPLE);
  9229. const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName);
  9230. // `_lines.Add(description)` parses as array_access (non-empty parens) — still a call site
  9231. expect(calls).toContain('_lines.Add');
  9232. // bare call with args
  9233. expect(calls).toContain('Validate');
  9234. // qualified module call
  9235. expect(calls).toContain('Helpers.Twice');
  9236. });
  9237. it('should emit instantiates for New, with VB generic syntax stripped', () => {
  9238. const result = extractFromSource('Invoice.vb', SAMPLE);
  9239. const insts = result.unresolvedReferences.filter((r) => r.referenceKind === 'instantiates').map((r) => r.referenceName);
  9240. expect(insts).toContain('Invoice');
  9241. // `As New Dictionary(Of String, Integer)` → bare type name, not `Dictionary(Of ...)`
  9242. expect(insts.some((n) => n.includes('(') || /\bOf\b/.test(n))).toBe(false);
  9243. });
  9244. it('should extract Imports as import nodes', () => {
  9245. const result = extractFromSource('Invoice.vb', SAMPLE);
  9246. const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name);
  9247. expect(imports).toEqual(expect.arrayContaining(['System', 'System.Collections.Generic']));
  9248. });
  9249. it('should parse a file without a trailing newline (preParse guard)', () => {
  9250. const code = 'Class Tail\n Sub Go()\n Log("x")\n End Sub\nEnd Class';
  9251. const result = extractFromSource('Tail.vb', code);
  9252. expect(result.nodes.find((n) => n.kind === 'class')?.name).toBe('Tail');
  9253. expect(result.nodes.find((n) => n.kind === 'method')?.name).toBe('Go');
  9254. });
  9255. });
  9256. describe('VB.NET Extraction — scanner-backed constructs', () => {
  9257. it('should parse XML literals as opaque literals without breaking siblings', () => {
  9258. const code = `Class Muxer
  9259. Function WriteTags() As Object
  9260. Dim xml = <Tags>
  9261. <%= From tag In Tags Select <Tag><Name><%= tag.Name %></Name></Tag> %>
  9262. </Tags>
  9263. Return xml
  9264. End Function
  9265. Sub After()
  9266. Log("still extracted")
  9267. End Sub
  9268. End Class
  9269. `;
  9270. const result = extractFromSource('Muxer.vb', code);
  9271. const methods = result.nodes.filter((n) => n.kind === 'method').map((n) => n.name);
  9272. expect(methods).toEqual(expect.arrayContaining(['WriteTags', 'After']));
  9273. });
  9274. it('should parse multi-line LINQ query clauses', () => {
  9275. const code = `Class T
  9276. Function Big() As Integer
  9277. Dim big = From l In _lines
  9278. Where l.Length > 3
  9279. Select l.Length
  9280. Return big.Sum()
  9281. End Function
  9282. End Class
  9283. `;
  9284. const result = extractFromSource('Linq.vb', code);
  9285. const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName);
  9286. expect(calls).toContain('big.Sum');
  9287. expect(result.nodes.find((n) => n.kind === 'method')?.name).toBe('Big');
  9288. });
  9289. it('should extract MustOverride members without derailing following members', () => {
  9290. const code = `MustInherit Class VideoEncoder
  9291. MustOverride ReadOnly Property OutputExt As String
  9292. Public MustOverride Sub ShowConfigDialog(Optional param As Object = Nothing)
  9293. MustOverride Function GetError() As String
  9294. Sub New()
  9295. CanEdit = True
  9296. End Sub
  9297. End Class
  9298. `;
  9299. const result = extractFromSource('VideoEncoder.vb', code);
  9300. const methods = result.nodes.filter((n) => n.kind === 'method').map((n) => n.name);
  9301. expect(methods).toEqual(expect.arrayContaining(['ShowConfigDialog', 'GetError', 'New']));
  9302. const props = result.nodes.filter((n) => n.kind === 'property').map((n) => n.name);
  9303. expect(props).toContain('OutputExt');
  9304. });
  9305. it('should parse nullable declarator shorthand (Dim x? = expr)', () => {
  9306. const code = `Class T
  9307. Sub M(folderInfo As Object)
  9308. Dim SteamFolderData? = Parser.GetSteamNameAndID(folderInfo)
  9309. Use(SteamFolderData)
  9310. End Sub
  9311. End Class
  9312. `;
  9313. const result = extractFromSource('Factory.vb', code);
  9314. const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName);
  9315. expect(calls).toContain('Parser.GetSteamNameAndID');
  9316. });
  9317. });
  9318. // =============================================================================
  9319. // Erlang (vendored WhatsApp/tree-sitter-erlang grammar — the ELP grammar)
  9320. // =============================================================================
  9321. describe('Erlang Extraction', () => {
  9322. describe('Language detection', () => {
  9323. it('should report Erlang as supported', () => {
  9324. expect(isLanguageSupported('erlang')).toBe(true);
  9325. expect(getSupportedLanguages()).toContain('erlang');
  9326. expect(isSourceFile('apps/app/src/foo.erl')).toBe(true);
  9327. expect(isSourceFile('include/foo.hrl')).toBe(true);
  9328. });
  9329. });
  9330. describe('Function extraction', () => {
  9331. it('should merge multi-clause functions into one node spanning all clauses', () => {
  9332. const code = `-module(m).
  9333. -export([classify/1]).
  9334. classify(X) when is_atom(X) ->
  9335. atom;
  9336. classify(X) when is_binary(X) ->
  9337. binary;
  9338. classify(_X) ->
  9339. other.
  9340. `;
  9341. const result = extractFromSource('src/m.erl', code);
  9342. const fns = result.nodes.filter((n) => n.kind === 'function' && n.name === 'classify');
  9343. expect(fns).toHaveLength(1);
  9344. expect(fns[0]!.startLine).toBe(4);
  9345. expect(fns[0]!.endLine).toBe(9);
  9346. expect(fns[0]!.language).toBe('erlang');
  9347. });
  9348. it('should qualify functions with the module namespace', () => {
  9349. const code = `-module(my_server).
  9350. -export([start/0]).
  9351. start() -> ok.
  9352. helper() -> ok.
  9353. `;
  9354. const result = extractFromSource('src/my_server.erl', code);
  9355. const ns = result.nodes.find((n) => n.kind === 'namespace');
  9356. expect(ns?.name).toBe('my_server');
  9357. const start = result.nodes.find((n) => n.kind === 'function' && n.name === 'start');
  9358. // Arity is part of an Erlang function's identity — qualifiedName carries it (#1610).
  9359. expect(start?.qualifiedName).toBe('my_server::start/0');
  9360. });
  9361. it('should give same-name different-arity functions separate arity-qualified nodes (#1610)', () => {
  9362. const code = `-module(gap).
  9363. -export([f/1, f/2]).
  9364. f(X) -> X + 1.
  9365. f(X, Y) -> X + Y.
  9366. `;
  9367. const result = extractFromSource('src/gap.erl', code);
  9368. const fns = result.nodes.filter((n) => n.kind === 'function' && n.name === 'f');
  9369. expect(fns).toHaveLength(2);
  9370. expect(fns.map((n) => n.qualifiedName).sort()).toEqual(['gap::f/1', 'gap::f/2']);
  9371. const f1 = fns.find((n) => n.qualifiedName === 'gap::f/1')!;
  9372. const f2 = fns.find((n) => n.qualifiedName === 'gap::f/2')!;
  9373. expect([f1.startLine, f1.endLine]).toEqual([4, 4]);
  9374. expect([f2.startLine, f2.endLine]).toEqual([5, 5]);
  9375. expect(f1.signature).toBe('f(X)');
  9376. expect(f2.signature).toBe('f(X, Y)');
  9377. });
  9378. it('should split interleaved same-name defs by arity with distinct qualified names', () => {
  9379. const code = `-module(inter).
  9380. f(X) -> X + 1;
  9381. f(Y) -> Y.
  9382. g() -> ok.
  9383. f(X, Y) -> X + Y.
  9384. `;
  9385. const result = extractFromSource('src/inter.erl', code);
  9386. const fs = result.nodes.filter((n) => n.kind === 'function' && n.name === 'f');
  9387. expect(fs).toHaveLength(2);
  9388. expect(fs.map((n) => n.qualifiedName).sort()).toEqual(['inter::f/1', 'inter::f/2']);
  9389. // Clauses of the same arity still merge into one span.
  9390. const f1 = fs.find((n) => n.qualifiedName === 'inter::f/1')!;
  9391. expect([f1.startLine, f1.endLine]).toEqual([3, 4]);
  9392. });
  9393. it('should flag exported per arity (#1610)', () => {
  9394. const code = `-module(m).
  9395. -export([f/1]).
  9396. f(X) -> X.
  9397. f(X, Y) -> {X, Y}.
  9398. `;
  9399. const result = extractFromSource('src/m.erl', code);
  9400. expect(result.nodes.find((n) => n.qualifiedName === 'm::f/1')?.isExported).toBe(true);
  9401. expect(result.nodes.find((n) => n.qualifiedName === 'm::f/2')?.isExported).toBe(false);
  9402. });
  9403. it('should attach a -spec sitting between two arities to the arity it names (#1610)', () => {
  9404. const code = `-module(deleg).
  9405. -export([header/2, header/3]).
  9406. header(Name, Req) ->
  9407. header(Name, Req, undefined).
  9408. -spec header(binary(), map(), any()) -> any().
  9409. header(Name, Headers, Default) ->
  9410. maps:get(Name, Headers, Default).
  9411. `;
  9412. const result = extractFromSource('src/deleg.erl', code);
  9413. const h2 = result.nodes.find((n) => n.qualifiedName === 'deleg::header/2')!;
  9414. const h3 = result.nodes.find((n) => n.qualifiedName === 'deleg::header/3')!;
  9415. expect(h2.signature).toBe('header(Name, Req)');
  9416. expect(h3.signature).toBe('-spec header(binary(), map(), any()) -> any().');
  9417. expect([h2.startLine, h2.endLine]).toEqual([4, 5]);
  9418. expect(h3.startLine).toBe(8);
  9419. // The delegation call carries the callee's arity — no more self-loop.
  9420. const calls = result.unresolvedReferences
  9421. .filter((r) => r.referenceKind === 'calls')
  9422. .map((r) => r.referenceName);
  9423. expect(calls).toContain('header/3');
  9424. });
  9425. it('should flag exported functions and honor -compile(export_all)', () => {
  9426. const code = `-module(m).
  9427. -export([api/0]).
  9428. api() -> internal().
  9429. internal() -> ok.
  9430. `;
  9431. const result = extractFromSource('src/m.erl', code);
  9432. const api = result.nodes.find((n) => n.name === 'api');
  9433. const internal = result.nodes.find((n) => n.name === 'internal');
  9434. expect(api?.isExported).toBe(true);
  9435. expect(internal?.isExported).toBe(false);
  9436. const all = extractFromSource('src/all.erl', `-module(all).
  9437. -compile(export_all).
  9438. anything() -> ok.
  9439. `);
  9440. expect(all.nodes.find((n) => n.name === 'anything')?.isExported).toBe(true);
  9441. });
  9442. it('should use the preceding -spec as the signature and capture doc comments', () => {
  9443. const code = `-module(m).
  9444. %% Fetches a value by key.
  9445. -spec fetch(binary()) -> {ok, term()} | not_found.
  9446. fetch(Key) ->
  9447. lookup(Key).
  9448. lookup(_K) -> not_found.
  9449. `;
  9450. const result = extractFromSource('src/m.erl', code);
  9451. const fetch = result.nodes.find((n) => n.name === 'fetch');
  9452. expect(fetch?.signature).toBe('-spec fetch(binary()) -> {ok, term()} | not_found.');
  9453. expect(fetch?.docstring).toBe('Fetches a value by key.');
  9454. });
  9455. it('should fall back to the clause header as the signature', () => {
  9456. const code = `-module(m).
  9457. resize(W, H) when W > 0, H > 0 ->
  9458. {W, H}.
  9459. `;
  9460. const result = extractFromSource('src/m.erl', code);
  9461. const resize = result.nodes.find((n) => n.name === 'resize');
  9462. expect(resize?.signature).toBe('resize(W, H) when W > 0, H > 0');
  9463. });
  9464. });
  9465. describe('Record and type extraction', () => {
  9466. it('should extract records as structs with fields', () => {
  9467. const code = `-module(m).
  9468. -record(state, {
  9469. store = #{} :: map(),
  9470. counter = 0 :: non_neg_integer()
  9471. }).
  9472. `;
  9473. const result = extractFromSource('src/m.erl', code);
  9474. const rec = result.nodes.find((n) => n.kind === 'struct');
  9475. expect(rec?.name).toBe('state');
  9476. const fields = result.nodes.filter((n) => n.kind === 'field').map((n) => n.name);
  9477. expect(fields).toContain('store');
  9478. expect(fields).toContain('counter');
  9479. });
  9480. it('should extract -type and -opaque as type aliases, without bogus type-call refs', () => {
  9481. const code = `-module(m).
  9482. -type key() :: atom() | binary().
  9483. -opaque handle() :: reference().
  9484. -spec noop(key()) -> ok.
  9485. noop(_K) -> ok.
  9486. `;
  9487. const result = extractFromSource('src/m.erl', code);
  9488. const aliases = result.nodes.filter((n) => n.kind === 'type_alias').map((n) => n.name);
  9489. expect(aliases).toContain('key');
  9490. expect(aliases).toContain('handle');
  9491. // Type-position expressions parse as `call` nodes — the spec/type subtrees
  9492. // must not leak `calls` refs to type names like atom()/binary().
  9493. const bogus = result.unresolvedReferences.filter(
  9494. (r) => r.referenceKind === 'calls' && ['atom', 'binary', 'reference', 'key'].includes(r.referenceName)
  9495. );
  9496. expect(bogus).toHaveLength(0);
  9497. });
  9498. it('should extract -define macros as constants', () => {
  9499. const code = `-module(m).
  9500. -define(TIMEOUT, 5000).
  9501. -define(WRAP(X), {ok, X}).
  9502. `;
  9503. const result = extractFromSource('src/m.erl', code);
  9504. const consts = result.nodes.filter((n) => n.kind === 'constant').map((n) => n.name);
  9505. expect(consts).toContain('TIMEOUT');
  9506. expect(consts).toContain('WRAP');
  9507. });
  9508. });
  9509. describe('Import extraction', () => {
  9510. it('should extract -include/-include_lib and -import', () => {
  9511. const code = `-module(m).
  9512. -include("records.hrl").
  9513. -include_lib("kernel/include/logger.hrl").
  9514. -import(lists, [map/2]).
  9515. `;
  9516. const result = extractFromSource('src/m.erl', code);
  9517. const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name);
  9518. expect(imports).toContain('records.hrl');
  9519. expect(imports).toContain('kernel/include/logger.hrl');
  9520. expect(imports).toContain('lists');
  9521. const ref = result.unresolvedReferences.find(
  9522. (r) => r.referenceKind === 'imports' && r.referenceName === 'records.hrl'
  9523. );
  9524. expect(ref).toBeDefined();
  9525. });
  9526. });
  9527. describe('Call extraction', () => {
  9528. it('should record local calls bare and remote calls module-qualified', () => {
  9529. const code = `-module(m).
  9530. -export([run/1]).
  9531. run(X) ->
  9532. Y = prepare(X),
  9533. other_mod:process(Y).
  9534. prepare(X) -> X.
  9535. `;
  9536. const result = extractFromSource('src/m.erl', code);
  9537. const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName);
  9538. expect(calls).toContain('prepare/1');
  9539. // `mod:fn(...)` is emitted as `mod::fn/arity` — the same shape the
  9540. // module namespace + arity suffix gives every function's qualifiedName,
  9541. // so it resolves via the qualified-name matcher (#1610).
  9542. expect(calls).toContain('other_mod::process/1');
  9543. });
  9544. it('should carry written arity on fun references and static MFA lists (#1610)', () => {
  9545. const code = `-module(m).
  9546. -export([go/0]).
  9547. go() ->
  9548. lists:map(fun bump/1, [1]),
  9549. Prod = fun other_mod:produce/2,
  9550. proc_lib:spawn_link(?MODULE, work, [a, b]),
  9551. Prod.
  9552. bump(X) -> X + 1.
  9553. work(_A, _B) -> ok.
  9554. `;
  9555. const result = extractFromSource('src/m.erl', code);
  9556. const refs = result.unresolvedReferences;
  9557. expect(refs.some((r) => r.referenceKind === 'references' && r.referenceName === 'bump/1')).toBe(true);
  9558. expect(refs.some((r) => r.referenceKind === 'references' && r.referenceName === 'other_mod::produce/2')).toBe(true);
  9559. expect(refs.some((r) => r.referenceKind === 'calls' && r.referenceName === 'work/2')).toBe(true);
  9560. });
  9561. it('should not emit calls for dynamic dispatch (var module / var fun)', () => {
  9562. const code = `-module(m).
  9563. -export([run/2]).
  9564. run(Mod, F) ->
  9565. Mod:handle(x),
  9566. F(y).
  9567. `;
  9568. const result = extractFromSource('src/m.erl', code);
  9569. const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName);
  9570. expect(calls.some((c) => c.startsWith('handle'))).toBe(false);
  9571. expect(calls.some((c) => c.startsWith('Mod::'))).toBe(false);
  9572. expect(calls.some((c) => c === 'F' || c.startsWith('F/'))).toBe(false);
  9573. });
  9574. it('should connect gen_server self-calls to the module handlers', () => {
  9575. const code = `-module(kv_store).
  9576. -behaviour(gen_server).
  9577. -export([get/1, put/2, drop/1]).
  9578. -export([init/1, handle_call/3, handle_cast/2]).
  9579. -define(SERVER, ?MODULE).
  9580. get(Key) ->
  9581. gen_server:call(?SERVER, {get, Key}).
  9582. put(Key, Value) ->
  9583. gen_server:cast(?MODULE, {put, Key, Value}).
  9584. drop(Key) ->
  9585. gen_server:call(kv_store, {drop, Key}).
  9586. init(_) -> {ok, #{}}.
  9587. handle_call({get, K}, _From, S) -> {reply, maps:find(K, S), S};
  9588. handle_call({drop, K}, _From, S) -> {reply, ok, maps:remove(K, S)}.
  9589. handle_cast({put, K, V}, S) -> {noreply, maps:put(K, V, S)}.
  9590. `;
  9591. const result = extractFromSource('src/kv_store.erl', code);
  9592. const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName);
  9593. // ?SERVER (defined as ?MODULE), ?MODULE, and the module's own atom all
  9594. // count as self — public API wrappers connect to their handlers, at
  9595. // OTP's fixed handler arities (#1610).
  9596. expect(calls.filter((c) => c === 'kv_store::handle_call/3')).toHaveLength(2);
  9597. expect(calls).toContain('kv_store::handle_cast/2');
  9598. });
  9599. it('should connect gen_server calls to a registered-name module, directly or via an atom macro', () => {
  9600. const code = `-module(kv_client).
  9601. -export([fetch/1, evict/1]).
  9602. -define(STORE, kv_store).
  9603. fetch(Key) ->
  9604. gen_server:call(kv_store, {get, Key}).
  9605. evict(Key) ->
  9606. gen_server:cast(?STORE, {evict, Key}).
  9607. `;
  9608. const result = extractFromSource('src/kv_client.erl', code);
  9609. const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName);
  9610. // OTP's {local, ?MODULE} convention names a server after its module —
  9611. // a cross-module registered name targets that module's handlers. A name
  9612. // matching no module simply never resolves downstream.
  9613. expect(calls).toContain('kv_store::handle_call/3');
  9614. expect(calls).toContain('kv_store::handle_cast/2');
  9615. });
  9616. it('should not connect gen_server calls with dynamic targets', () => {
  9617. const code = `-module(m).
  9618. -export([go/2]).
  9619. go(Pid, Msg) ->
  9620. gen_server:call(Pid, Msg),
  9621. gen_server:cast({global, some_name}, Msg),
  9622. gen_server:call({some_name, node()}, Msg).
  9623. `;
  9624. const result = extractFromSource('src/m.erl', code);
  9625. const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName);
  9626. expect(calls.filter((c) => c.includes('handle_call') || c.includes('handle_cast'))).toHaveLength(0);
  9627. });
  9628. it('should lift static MFA arguments of the spawn/apply family into call refs', () => {
  9629. const code = `-module(m).
  9630. -export([boot/2]).
  9631. boot(Req, Env) ->
  9632. Pid = proc_lib:spawn_link(?MODULE, request_process, [Req, Env]),
  9633. spawn(?MODULE, monitor_loop, [Pid]),
  9634. apply(other_mod, handle, [Req]),
  9635. timer:apply_after(500, other_mod, tick, []),
  9636. Pid.
  9637. request_process(_R, _E) -> ok.
  9638. monitor_loop(_P) -> ok.
  9639. `;
  9640. const result = extractFromSource('src/m.erl', code);
  9641. const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName);
  9642. expect(calls).toContain('request_process/2'); // ?MODULE → bare-with-arity, same-file resolution
  9643. expect(calls).toContain('monitor_loop/1');
  9644. expect(calls).toContain('other_mod::handle/1');
  9645. expect(calls).toContain('other_mod::tick/0');
  9646. });
  9647. it('should stay silent on dynamic spawn/apply (var module, fun value, or plain fun)', () => {
  9648. const code = `-module(m).
  9649. -export([go/3]).
  9650. go(M, F, A) ->
  9651. spawn(M, F, A),
  9652. spawn(fun() -> helper() end),
  9653. apply(M, F, A).
  9654. helper() -> ok.
  9655. `;
  9656. const result = extractFromSource('src/m.erl', code);
  9657. const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName);
  9658. // The fun body's call is still walked; no phantom MFA targets appear.
  9659. expect(calls).toContain('helper/0');
  9660. expect(calls.filter((c) => !['spawn/3', 'spawn/1', 'apply/3', 'helper/0'].includes(c))).toHaveLength(0);
  9661. });
  9662. it('should treat ?MODULE:fn calls as local calls', () => {
  9663. const code = `-module(m).
  9664. -export([kick/0]).
  9665. kick() ->
  9666. ?MODULE:work().
  9667. work() -> ok.
  9668. `;
  9669. const result = extractFromSource('src/m.erl', code);
  9670. const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName);
  9671. expect(calls).toContain('work/0');
  9672. });
  9673. it('should capture fun name/arity values as function references', () => {
  9674. const code = `-module(m).
  9675. -export([wire/1]).
  9676. wire(Pids) ->
  9677. lists:foreach(fun notify/1, Pids),
  9678. lists:map(fun m:notify/1, Pids).
  9679. notify(_P) -> ok.
  9680. `;
  9681. const result = extractFromSource('src/m.erl', code);
  9682. const refs = result.unresolvedReferences.filter((r) => r.referenceKind === 'references').map((r) => r.referenceName);
  9683. expect(refs).toContain('notify/1');
  9684. expect(refs).toContain('m::notify/1');
  9685. });
  9686. it('should reference records used in bodies and argument patterns', () => {
  9687. const code = `-module(m).
  9688. -export([mk/1, get_id/1]).
  9689. -record(req, {id, payload}).
  9690. mk(Id) -> #req{id = Id}.
  9691. get_id(#req{id = Id}) -> Id.
  9692. `;
  9693. const result = extractFromSource('src/m.erl', code);
  9694. const refs = result.unresolvedReferences.filter(
  9695. (r) => r.referenceKind === 'references' && r.referenceName === 'req'
  9696. );
  9697. expect(refs.length).toBeGreaterThanOrEqual(2);
  9698. });
  9699. it('should attribute calls from every clause of a multi-clause function', () => {
  9700. const code = `-module(m).
  9701. -export([handle/1]).
  9702. handle({a, X}) ->
  9703. first(X);
  9704. handle({b, X}) ->
  9705. second(X).
  9706. first(X) -> X.
  9707. second(X) -> X.
  9708. `;
  9709. const result = extractFromSource('src/m.erl', code);
  9710. const handle = result.nodes.find((n) => n.kind === 'function' && n.name === 'handle');
  9711. const calls = result.unresolvedReferences.filter(
  9712. (r) => r.referenceKind === 'calls' && r.fromNodeId === handle?.id
  9713. ).map((r) => r.referenceName);
  9714. expect(calls).toContain('first/1');
  9715. expect(calls).toContain('second/1');
  9716. });
  9717. });
  9718. describe('escript and app resource files', () => {
  9719. it('should extract functions and calls from an escript behind a shebang', () => {
  9720. const code = `#!/usr/bin/env escript
  9721. %%! -smp enable
  9722. main([Path]) ->
  9723. Result = analyze(Path),
  9724. io:format("~p~n", [Result]).
  9725. analyze(Path) ->
  9726. {ok, Bin} = file:read_file(Path),
  9727. byte_size(Bin).
  9728. `;
  9729. const result = extractFromSource('bin/tool.escript', code);
  9730. const fns = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name);
  9731. expect(fns).toContain('main');
  9732. expect(fns).toContain('analyze');
  9733. const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName);
  9734. expect(calls).toContain('analyze/1');
  9735. expect(calls).toContain('io::format/2');
  9736. });
  9737. it('should link an app resource file to its callback module and dependency apps', () => {
  9738. const code = `{application, sample, [
  9739. {description, "Sample application"},
  9740. {vsn, "1.0.0"},
  9741. {registered, [sample_server]},
  9742. {mod, {sample_app, []}},
  9743. {applications, [kernel, stdlib, sample_core]},
  9744. {included_applications, [sample_extra]},
  9745. {env, [{limit, 100}]},
  9746. {modules, []}
  9747. ]}.
  9748. `;
  9749. const result = extractFromSource('src/sample.app.src', code);
  9750. const refs = result.unresolvedReferences.map((r) => `${r.referenceKind}:${r.referenceName}`);
  9751. // The application-callback module is the app's entry point.
  9752. expect(refs).toContain('references:sample_app');
  9753. // Dependencies resolve to umbrella siblings; kernel/stdlib just drop.
  9754. expect(refs).toContain('imports:kernel');
  9755. expect(refs).toContain('imports:sample_core');
  9756. expect(refs).toContain('imports:sample_extra');
  9757. // Registered names, env values, and the like carry no graph structure.
  9758. expect(refs.filter((r) => r.endsWith(':sample_server'))).toHaveLength(0);
  9759. expect(refs.filter((r) => r.endsWith(':limit'))).toHaveLength(0);
  9760. });
  9761. });
  9762. describe('Macro linkage', () => {
  9763. it('should attribute macro-body calls to the macro and link function-like uses into the chain', () => {
  9764. const code = `-module(m).
  9765. -export([do_thing/1]).
  9766. -define(LOG_AUDIT(Event), audit_logger:log(Event, ?MODULE)).
  9767. do_thing(X) ->
  9768. ?LOG_AUDIT({thing, X}),
  9769. ok.
  9770. `;
  9771. const result = extractFromSource('src/m.erl', code);
  9772. const macro = result.nodes.find((n) => n.kind === 'constant' && n.name === 'LOG_AUDIT');
  9773. const doThing = result.nodes.find((n) => n.kind === 'function' && n.name === 'do_thing');
  9774. const refsFrom = (id?: string) =>
  9775. result.unresolvedReferences.filter((r) => r.fromNodeId === id).map((r) => `${r.referenceKind}:${r.referenceName}`);
  9776. // The body's remote call belongs to the macro node — true exactly once.
  9777. expect(refsFrom(macro?.id)).toContain('calls:audit_logger::log/2');
  9778. // The use site joins the call chain: do_thing -calls→ LOG_AUDIT.
  9779. expect(refsFrom(doThing?.id)).toContain('calls:LOG_AUDIT');
  9780. });
  9781. it('should reference bare macro reads without polluting call chains', () => {
  9782. const code = `-module(m).
  9783. -export([wait/0]).
  9784. -define(TIMEOUT, 5000).
  9785. wait() ->
  9786. receive after ?TIMEOUT -> ok end.
  9787. `;
  9788. const result = extractFromSource('src/m.erl', code);
  9789. const refs = result.unresolvedReferences.map((r) => `${r.referenceKind}:${r.referenceName}`);
  9790. expect(refs).toContain('references:TIMEOUT');
  9791. expect(refs).not.toContain('calls:TIMEOUT');
  9792. });
  9793. it('should skip compiler-predefined macros and keep walking macro-use arguments', () => {
  9794. const code = `-module(m).
  9795. -export([check/0]).
  9796. check() ->
  9797. ?assertEqual(ok, prepare()),
  9798. {?MODULE, ?LINE, ?FUNCTION_NAME}.
  9799. prepare() -> ok.
  9800. `;
  9801. const result = extractFromSource('src/m.erl', code);
  9802. const refs = result.unresolvedReferences.map((r) => r.referenceName);
  9803. // The nested call inside the macro's arguments still attributes to check/0.
  9804. expect(refs).toContain('prepare/0');
  9805. // ?assertEqual (an OTP header macro) is emitted and simply never resolves…
  9806. expect(refs).toContain('assertEqual');
  9807. // …but predefined macros have no definition to link.
  9808. expect(refs).not.toContain('MODULE');
  9809. expect(refs).not.toContain('LINE');
  9810. expect(refs).not.toContain('FUNCTION_NAME');
  9811. });
  9812. it('should chain macro-to-macro uses', () => {
  9813. const code = `-module(m).
  9814. -define(TARGET, target_fn()).
  9815. -define(ALIAS, ?TARGET).
  9816. `;
  9817. const result = extractFromSource('src/m.erl', code);
  9818. const target = result.nodes.find((n) => n.kind === 'constant' && n.name === 'TARGET');
  9819. const alias = result.nodes.find((n) => n.kind === 'constant' && n.name === 'ALIAS');
  9820. const refsFrom = (id?: string) =>
  9821. result.unresolvedReferences.filter((r) => r.fromNodeId === id).map((r) => `${r.referenceKind}:${r.referenceName}`);
  9822. expect(refsFrom(target?.id)).toContain('calls:target_fn/0');
  9823. expect(refsFrom(alias?.id)).toContain('references:TARGET');
  9824. });
  9825. });
  9826. describe('Behaviour extraction', () => {
  9827. it('should emit an implements reference for -behaviour', () => {
  9828. const code = `-module(m).
  9829. -behaviour(gen_server).
  9830. init(_) -> {ok, #{}}.
  9831. `;
  9832. const result = extractFromSource('src/m.erl', code);
  9833. const impl = result.unresolvedReferences.find((r) => r.referenceKind === 'implements');
  9834. expect(impl?.referenceName).toBe('gen_server');
  9835. });
  9836. it('should not create symbols from -callback declarations', () => {
  9837. const code = `-module(b).
  9838. -callback handle_thing(term()) -> ok.
  9839. -callback init(list()) -> {ok, term()}.
  9840. `;
  9841. const result = extractFromSource('src/b.erl', code);
  9842. const fns = result.nodes.filter((n) => n.kind === 'function');
  9843. expect(fns).toHaveLength(0);
  9844. });
  9845. });
  9846. });
  9847. describe('Terraform Extraction', () => {
  9848. describe('Language detection', () => {
  9849. it('should detect Terraform files', () => {
  9850. expect(detectLanguage('main.tf')).toBe('terraform');
  9851. expect(detectLanguage('terraform.tfvars')).toBe('terraform');
  9852. expect(detectLanguage('versions.tofu')).toBe('terraform');
  9853. });
  9854. it('should report Terraform as supported', () => {
  9855. expect(isLanguageSupported('terraform')).toBe(true);
  9856. expect(getSupportedLanguages()).toContain('terraform');
  9857. });
  9858. });
  9859. describe('Block extraction', () => {
  9860. it('should extract a resource block as a class with qualified type.name', () => {
  9861. const code = `
  9862. resource "aws_s3_bucket" "my_bucket" {
  9863. bucket = "example"
  9864. }
  9865. `;
  9866. const result = extractFromSource('main.tf', code);
  9867. const res = result.nodes.find((n) => n.name === 'aws_s3_bucket.my_bucket');
  9868. expect(res).toBeDefined();
  9869. expect(res?.kind).toBe('class');
  9870. expect(res?.qualifiedName).toBe('aws_s3_bucket.my_bucket');
  9871. expect(res?.signature).toBe('resource "aws_s3_bucket" "my_bucket"');
  9872. expect(res?.language).toBe('terraform');
  9873. });
  9874. it('should extract a data block under the data.* qualified name', () => {
  9875. const code = `
  9876. data "aws_caller_identity" "current" {}
  9877. `;
  9878. const result = extractFromSource('main.tf', code);
  9879. const node = result.nodes.find((n) => n.qualifiedName === 'data.aws_caller_identity.current');
  9880. expect(node).toBeDefined();
  9881. expect(node?.kind).toBe('class');
  9882. });
  9883. it('should extract a variable block as variable with qualified name var.X', () => {
  9884. const code = `
  9885. variable "region" {
  9886. type = string
  9887. default = "us-east-1"
  9888. }
  9889. `;
  9890. const result = extractFromSource('variables.tf', code);
  9891. const v = result.nodes.find((n) => n.qualifiedName === 'var.region');
  9892. expect(v).toBeDefined();
  9893. expect(v?.kind).toBe('variable');
  9894. expect(v?.name).toBe('region');
  9895. });
  9896. it('should extract an output block as variable with qualified name output.X', () => {
  9897. const code = `
  9898. output "bucket_arn" {
  9899. value = aws_s3_bucket.my_bucket.arn
  9900. }
  9901. `;
  9902. const result = extractFromSource('outputs.tf', code);
  9903. const out = result.nodes.find((n) => n.qualifiedName === 'output.bucket_arn');
  9904. expect(out).toBeDefined();
  9905. expect(out?.kind).toBe('variable');
  9906. });
  9907. it('should extract a module block as module with qualified name module.X', () => {
  9908. const code = `
  9909. module "vpc" {
  9910. source = "./modules/vpc"
  9911. cidr = var.vpc_cidr
  9912. }
  9913. `;
  9914. const result = extractFromSource('main.tf', code);
  9915. const m = result.nodes.find((n) => n.qualifiedName === 'module.vpc');
  9916. expect(m).toBeDefined();
  9917. expect(m?.kind).toBe('module');
  9918. });
  9919. it('should extract a provider block as namespace', () => {
  9920. const code = `
  9921. provider "aws" {
  9922. region = "us-east-1"
  9923. }
  9924. `;
  9925. const result = extractFromSource('main.tf', code);
  9926. const p = result.nodes.find((n) => n.qualifiedName === 'provider.aws');
  9927. expect(p).toBeDefined();
  9928. expect(p?.kind).toBe('namespace');
  9929. });
  9930. it('should extract every locals attribute as its own constant with local.K qualified name', () => {
  9931. const code = `
  9932. locals {
  9933. prefix = "prod"
  9934. full_name = "\${local.prefix}-app"
  9935. max_retries = 3
  9936. }
  9937. `;
  9938. const result = extractFromSource('locals.tf', code);
  9939. const names = result.nodes
  9940. .filter((n) => n.kind === 'constant')
  9941. .map((n) => n.qualifiedName)
  9942. .sort();
  9943. expect(names).toEqual(['local.full_name', 'local.max_retries', 'local.prefix']);
  9944. });
  9945. it('should ignore a terraform settings block', () => {
  9946. const code = `
  9947. terraform {
  9948. required_version = ">= 1.5"
  9949. }
  9950. `;
  9951. const result = extractFromSource('versions.tf', code);
  9952. const symbols = result.nodes.filter((n) => n.kind !== 'file');
  9953. expect(symbols).toHaveLength(0);
  9954. });
  9955. it('should index .tfvars top-level attributes via the same parser path', () => {
  9956. // .tfvars files have no blocks — just bare attributes, each of which
  9957. // SETS the root module variable of that name. No symbols are declared,
  9958. // but every top-level assignment references its variable so "what sets
  9959. // var.region" is answerable.
  9960. const code = `
  9961. region = "us-east-1"
  9962. environment = "prod"
  9963. `;
  9964. const result = extractFromSource('terraform.tfvars', code);
  9965. expect(result.errors.filter((e) => e.severity === 'error')).toHaveLength(0);
  9966. const symbols = result.nodes.filter((n) => n.kind !== 'file');
  9967. expect(symbols).toHaveLength(0);
  9968. const refs = result.unresolvedReferences.map((r) => r.referenceName);
  9969. expect(refs).toContain('var.region');
  9970. expect(refs).toContain('var.environment');
  9971. });
  9972. });
  9973. describe('Reference extraction', () => {
  9974. it('should emit a reference for var.X used inside a resource', () => {
  9975. const code = `
  9976. variable "region" {}
  9977. resource "aws_s3_bucket" "b" {
  9978. bucket = var.region
  9979. }
  9980. `;
  9981. const result = extractFromSource('main.tf', code);
  9982. const refs = result.unresolvedReferences.map((r) => r.referenceName);
  9983. expect(refs).toContain('var.region');
  9984. });
  9985. it('should emit a reference for module.M.<output> as module.M', () => {
  9986. const code = `
  9987. output "vpc_id" {
  9988. value = module.vpc.vpc_id
  9989. }
  9990. `;
  9991. const result = extractFromSource('outputs.tf', code);
  9992. const refs = result.unresolvedReferences.map((r) => r.referenceName);
  9993. expect(refs).toContain('module.vpc');
  9994. });
  9995. it('should emit a scoped module.M:output.X ref alongside module.M for output chains', () => {
  9996. const code = `
  9997. output "vpc_id" {
  9998. value = module.vpc.vpc_id
  9999. }
  10000. `;
  10001. const result = extractFromSource('outputs.tf', code);
  10002. const refs = result.unresolvedReferences.map((r) => r.referenceName);
  10003. expect(refs).toContain('module.vpc:output.vpc_id');
  10004. // A bare module.M use (no output segment) stays a single ref.
  10005. const bare = extractFromSource('main.tf', 'output "m" {\n value = module.vpc\n}\n');
  10006. const bareRefs = bare.unresolvedReferences.map((r) => r.referenceName);
  10007. expect(bareRefs).toContain('module.vpc');
  10008. expect(bareRefs.some((r) => r.includes(':output.'))).toBe(false);
  10009. });
  10010. it('should wire module blocks: scoped input refs, meta-args skipped, local source imported', () => {
  10011. const code = `
  10012. module "vpc" {
  10013. source = "./modules/vpc"
  10014. version = "1.0.0"
  10015. count = 2
  10016. depends_on = [aws_iam_role.net]
  10017. cidr = var.vpc_cidr
  10018. name = "prod"
  10019. }
  10020. `;
  10021. const result = extractFromSource('main.tf', code);
  10022. const refs = result.unresolvedReferences.map((r) => r.referenceName);
  10023. // Input attributes wire to the child module's variables (scoped spelling).
  10024. expect(refs).toContain('module.vpc:var.cidr');
  10025. expect(refs).toContain('module.vpc:var.name');
  10026. // Meta-arguments configure the call, not child variables.
  10027. expect(refs).not.toContain('module.vpc:var.source');
  10028. expect(refs).not.toContain('module.vpc:var.version');
  10029. expect(refs).not.toContain('module.vpc:var.count');
  10030. expect(refs).not.toContain('module.vpc:var.depends_on');
  10031. // A local ./ source emits the module→file imports ref.
  10032. const fileRef = result.unresolvedReferences.find((r) => r.referenceName === 'module.vpc:file');
  10033. expect(fileRef).toBeDefined();
  10034. expect(fileRef?.referenceKind).toBe('imports');
  10035. // Attribute VALUES still reference the parent scope as before.
  10036. expect(refs).toContain('var.vpc_cidr');
  10037. expect(refs).toContain('aws_iam_role.net');
  10038. });
  10039. it('should not emit a module.M:file ref for registry or git sources', () => {
  10040. const code = `
  10041. module "s3" {
  10042. source = "terraform-aws-modules/s3-bucket/aws"
  10043. version = "4.0.0"
  10044. bucket = "x"
  10045. }
  10046. module "net" {
  10047. source = "git::https://example.com/net.git"
  10048. cidr = "10.0.0.0/16"
  10049. }
  10050. `;
  10051. const result = extractFromSource('main.tf', code);
  10052. const refs = result.unresolvedReferences.map((r) => r.referenceName);
  10053. expect(refs.some((r) => r.endsWith(':file'))).toBe(false);
  10054. // Input wiring is still emitted — the resolver drops it when the
  10055. // source turns out to be out-of-repo.
  10056. expect(refs).toContain('module.s3:var.bucket');
  10057. });
  10058. it('should emit a remote-output candidate for module.M.outputs.X chains', () => {
  10059. const code = `
  10060. resource "aws_eks_cluster" "this" {
  10061. vpc_id = module.vpc.outputs.vpc_id
  10062. }
  10063. `;
  10064. const result = extractFromSource('main.tf', code);
  10065. const refs = result.unresolvedReferences.map((r) => r.referenceName);
  10066. expect(refs).toContain('module.vpc');
  10067. expect(refs).toContain('module.vpc:remote-output.vpc_id');
  10068. // A plain two-segment chain must NOT produce a remote-output candidate.
  10069. const plain = extractFromSource('o.tf', 'output "x" {\n value = module.vpc.vpc_id\n}\n');
  10070. const plainRefs = plain.unresolvedReferences.map((r) => r.referenceName);
  10071. expect(plainRefs.some((r) => r.includes(':remote-output.'))).toBe(false);
  10072. });
  10073. it('should reference resource addresses from moved/import/removed blocks, anchored to the file', () => {
  10074. const code = `
  10075. resource "aws_instance" "new" {}
  10076. moved {
  10077. from = aws_instance.old
  10078. to = aws_instance.new
  10079. }
  10080. import {
  10081. to = aws_s3_bucket.b
  10082. id = "bucket-name"
  10083. }
  10084. removed {
  10085. from = module.legacy.aws_iam_role.r
  10086. }
  10087. `;
  10088. const result = extractFromSource('main.tf', code);
  10089. const refs = result.unresolvedReferences;
  10090. const names = refs.map((r) => r.referenceName);
  10091. expect(names).toContain('aws_instance.old');
  10092. expect(names).toContain('aws_instance.new');
  10093. expect(names).toContain('aws_s3_bucket.b');
  10094. expect(names).toContain('module.legacy');
  10095. // Scoped module refs are suppressed here: module.legacy.aws_iam_role.r
  10096. // names a resource inside a module instance, never a module output.
  10097. expect(names.some((n) => n.includes(':'))).toBe(false);
  10098. // Anchored to the file node, and no phantom symbols were declared.
  10099. const fileNode = result.nodes.find((n) => n.kind === 'file');
  10100. for (const r of refs.filter((x) => x.referenceName === 'aws_instance.old')) {
  10101. expect(r.fromNodeId).toBe(fileNode?.id);
  10102. }
  10103. expect(result.nodes.filter((n) => n.kind !== 'file')).toHaveLength(1); // just aws_instance.new
  10104. });
  10105. it('should collect check-assert condition references and still index check-scoped data blocks', () => {
  10106. const code = `
  10107. check "health" {
  10108. data "http" "ping" {
  10109. url = var.endpoint
  10110. }
  10111. assert {
  10112. condition = data.http.ping.status_code == 200 && var.strict
  10113. error_message = "unhealthy"
  10114. }
  10115. }
  10116. `;
  10117. const result = extractFromSource('checks.tf', code);
  10118. const names = result.unresolvedReferences.map((r) => r.referenceName);
  10119. expect(names).toContain('data.http.ping');
  10120. expect(names).toContain('var.strict');
  10121. expect(names).toContain('var.endpoint');
  10122. // The scoped data source inside the check is a real symbol.
  10123. expect(result.nodes.find((n) => n.qualifiedName === 'data.http.ping')).toBeDefined();
  10124. });
  10125. it('should qualify aliased provider blocks and reference provider selections', () => {
  10126. const code = `
  10127. provider "aws" {
  10128. region = "us-east-1"
  10129. }
  10130. provider "aws" {
  10131. alias = "east"
  10132. region = "us-east-2"
  10133. }
  10134. resource "aws_s3_bucket" "b" {
  10135. provider = aws.east
  10136. bucket = "x"
  10137. }
  10138. resource "google_service_account" "sa" {
  10139. provider = google-beta
  10140. }
  10141. `;
  10142. const result = extractFromSource('main.tf', code);
  10143. const providers = result.nodes.filter((n) => n.kind === 'namespace').map((n) => n.qualifiedName).sort();
  10144. expect(providers).toEqual(['provider.aws', 'provider.aws.east']);
  10145. const names = result.unresolvedReferences.map((r) => r.referenceName);
  10146. expect(names).toContain('provider.aws.east');
  10147. expect(names).toContain('provider.google-beta');
  10148. // The selection must not be misread as a resource reference.
  10149. expect(names).not.toContain('aws.east');
  10150. });
  10151. it('should reference the values (not keys) of a module providers map', () => {
  10152. const code = `
  10153. module "vpc" {
  10154. source = "./modules/vpc"
  10155. providers = {
  10156. aws = aws.east
  10157. }
  10158. }
  10159. `;
  10160. const result = extractFromSource('main.tf', code);
  10161. const names = result.unresolvedReferences.map((r) => r.referenceName);
  10162. expect(names).toContain('provider.aws.east');
  10163. expect(names).not.toContain('provider.aws');
  10164. expect(names).not.toContain('aws.east');
  10165. // providers is a meta-argument — no input wiring for it.
  10166. expect(names).not.toContain('module.vpc:var.providers');
  10167. });
  10168. it('should emit data.T.N references stripped of the trailing attribute', () => {
  10169. const code = `
  10170. output "account" {
  10171. value = data.aws_caller_identity.current.account_id
  10172. }
  10173. `;
  10174. const result = extractFromSource('outputs.tf', code);
  10175. const refs = result.unresolvedReferences.map((r) => r.referenceName);
  10176. expect(refs).toContain('data.aws_caller_identity.current');
  10177. });
  10178. it('should emit T.N references for managed-resource attribute access', () => {
  10179. const code = `
  10180. resource "aws_iam_policy" "p" {
  10181. policy = aws_s3_bucket.my.arn
  10182. }
  10183. `;
  10184. const result = extractFromSource('main.tf', code);
  10185. const refs = result.unresolvedReferences.map((r) => r.referenceName);
  10186. expect(refs).toContain('aws_s3_bucket.my');
  10187. });
  10188. it('should emit local.K references from locals attribute expressions', () => {
  10189. const code = `
  10190. locals {
  10191. prefix = "prod"
  10192. name = "\${local.prefix}-app"
  10193. }
  10194. `;
  10195. const result = extractFromSource('locals.tf', code);
  10196. const refs = result.unresolvedReferences.map((r) => r.referenceName);
  10197. expect(refs).toContain('local.prefix');
  10198. });
  10199. it('should skip built-in heads (each, count, self, path, terraform.workspace)', () => {
  10200. const code = `
  10201. resource "aws_instance" "x" {
  10202. count = each.value
  10203. name = path.module
  10204. workspace = terraform.workspace
  10205. self_ref = self.id
  10206. index_value = count.index
  10207. }
  10208. `;
  10209. const result = extractFromSource('main.tf', code);
  10210. const refs = result.unresolvedReferences.map((r) => r.referenceName);
  10211. // None of the built-ins should produce project references.
  10212. expect(refs.some((r) => r.startsWith('each.'))).toBe(false);
  10213. expect(refs.some((r) => r.startsWith('count.'))).toBe(false);
  10214. expect(refs.some((r) => r.startsWith('self.'))).toBe(false);
  10215. expect(refs.some((r) => r.startsWith('path.'))).toBe(false);
  10216. expect(refs.some((r) => r.startsWith('terraform.'))).toBe(false);
  10217. });
  10218. });
  10219. });
  10220. // =============================================================================
  10221. // ArkTS (HarmonyOS / OpenHarmony declarative UI — `.ets`)
  10222. // =============================================================================
  10223. describe('ArkTS Extraction', () => {
  10224. it('reports ArkTS as supported', () => {
  10225. expect(isLanguageSupported('arkts')).toBe(true);
  10226. expect(getSupportedLanguages()).toContain('arkts');
  10227. });
  10228. describe('@Component struct extraction', () => {
  10229. const code = `
  10230. import { TodoItem } from '../model/TodoItem';
  10231. @Entry
  10232. @Component
  10233. struct Index {
  10234. @State message: string = 'Hello';
  10235. @Prop count: number = 0;
  10236. @StorageLink('theme') theme: string = 'light';
  10237. private service: TodoService = new TodoService();
  10238. aboutToAppear(): void {
  10239. this.load();
  10240. }
  10241. load(): void {
  10242. this.message = 'loaded';
  10243. }
  10244. build() {
  10245. Column() {
  10246. Text(this.message).fontSize(50)
  10247. }
  10248. .height('100%')
  10249. }
  10250. }
  10251. `;
  10252. it('extracts the struct with its ArkUI decorators', () => {
  10253. const result = extractFromSource('pages/Index.ets', code);
  10254. const comp = result.nodes.find((n) => n.kind === 'struct' && n.name === 'Index');
  10255. expect(comp).toBeDefined();
  10256. expect(comp?.language).toBe('arkts');
  10257. expect(comp?.decorators).toEqual(expect.arrayContaining(['Entry', 'Component']));
  10258. });
  10259. it('extracts an EXPORTED struct whose decorators sit on the export statement', () => {
  10260. const result = extractFromSource(
  10261. 'components/Card.ets',
  10262. `@Component\nexport struct Card {\n build() {\n Row() {}\n }\n}\n`
  10263. );
  10264. const card = result.nodes.find((n) => n.kind === 'struct' && n.name === 'Card');
  10265. expect(card).toBeDefined();
  10266. expect(card?.isExported).toBe(true);
  10267. expect(card?.decorators).toContain('Component');
  10268. });
  10269. it('extracts struct members: build(), lifecycle + regular methods with qualified names', () => {
  10270. const result = extractFromSource('pages/Index.ets', code);
  10271. const methods = result.nodes.filter((n) => n.kind === 'method');
  10272. expect(methods.find((m) => m.qualifiedName === 'Index::build')).toBeDefined();
  10273. expect(methods.find((m) => m.qualifiedName === 'Index::aboutToAppear')).toBeDefined();
  10274. expect(methods.find((m) => m.qualifiedName === 'Index::load')).toBeDefined();
  10275. });
  10276. it('extracts @State/@Prop/@StorageLink members as properties with their decorators', () => {
  10277. const result = extractFromSource('pages/Index.ets', code);
  10278. const message = result.nodes.find((n) => n.kind === 'property' && n.qualifiedName === 'Index::message');
  10279. expect(message).toBeDefined();
  10280. expect(message?.decorators).toContain('State');
  10281. const count = result.nodes.find((n) => n.kind === 'property' && n.qualifiedName === 'Index::count');
  10282. expect(count?.decorators).toContain('Prop');
  10283. // Decorator-with-args: the decorator NAME is captured, not its argument.
  10284. const theme = result.nodes.find((n) => n.kind === 'property' && n.qualifiedName === 'Index::theme');
  10285. expect(theme?.decorators).toContain('StorageLink');
  10286. });
  10287. it('emits intra-struct method call refs (this.load())', () => {
  10288. const result = extractFromSource('pages/Index.ets', code);
  10289. const call = result.unresolvedReferences.find(
  10290. (r) => r.referenceKind === 'calls' && r.referenceName === 'load'
  10291. );
  10292. expect(call).toBeDefined();
  10293. });
  10294. });
  10295. describe('build() DSL call surface', () => {
  10296. const code = `
  10297. @Extend(Text) function titleStyle(size: number) {
  10298. .fontSize(size)
  10299. }
  10300. @Component
  10301. struct Page {
  10302. count: number = 0;
  10303. handleTap(): void {
  10304. this.count += 1;
  10305. }
  10306. @Builder
  10307. headerBar(title: string) {
  10308. Row() {
  10309. Text(title).titleStyle(24)
  10310. Button('Go').onClick(this.handleTap)
  10311. }
  10312. }
  10313. build() {
  10314. Column({ space: 8 }) {
  10315. this.headerBar('Home')
  10316. ChildCard({ label: 'hi' })
  10317. }
  10318. .height('100%')
  10319. }
  10320. }
  10321. `;
  10322. function callRefsFrom(result: ReturnType<typeof extractFromSource>, methodName: string): string[] {
  10323. const from = result.nodes.find((n) => n.kind === 'method' && n.name === methodName);
  10324. return result.unresolvedReferences
  10325. .filter((r) => r.referenceKind === 'calls' && r.fromNodeId === from?.id)
  10326. .map((r) => r.referenceName);
  10327. }
  10328. it('emits a call ref for a custom component instantiation inside build()', () => {
  10329. const result = extractFromSource('pages/Page.ets', code);
  10330. expect(callRefsFrom(result, 'build')).toContain('ChildCard');
  10331. });
  10332. it('emits dot-prefixed call refs for chained attributes (@Extend/@Styles-only resolution)', () => {
  10333. const result = extractFromSource('pages/Page.ets', code);
  10334. // `.titleStyle(24)` chains on the Text component — one node, repeated
  10335. // property/arguments field pairs, NOT nested call_expressions. The
  10336. // leading dot routes the ref to the decorator-gated matcher strategy so
  10337. // framework attributes (`.height` below) can never hit an arbitrary
  10338. // same-named symbol.
  10339. expect(callRefsFrom(result, 'headerBar')).toContain('.titleStyle');
  10340. expect(callRefsFrom(result, 'build')).toContain('.height');
  10341. expect(callRefsFrom(result, 'build')).not.toContain('height');
  10342. });
  10343. it('recovers the detached-chain shape (chain on the line after a nested component)', () => {
  10344. // Inside arkui_children, a chain starting after the closing `}` is
  10345. // detached by the grammar into sibling leading_dot_expression +
  10346. // parenthesized_expression statements — the close-button idiom.
  10347. const detached = `
  10348. @Component
  10349. struct Panel {
  10350. close(): void {}
  10351. build() {
  10352. Column() {
  10353. Row() {
  10354. Text('x')
  10355. }
  10356. .width(10)
  10357. .onClick(this.close)
  10358. .id('close_button')
  10359. }
  10360. }
  10361. }
  10362. `;
  10363. const result = extractFromSource('components/Panel.ets', detached);
  10364. const refs = callRefsFrom(result, 'build');
  10365. expect(refs).toContain('close');
  10366. expect(refs).toContain('.width');
  10367. expect(refs).not.toContain('width');
  10368. });
  10369. it('dot-prefixes the innermost call of a proper-form detached chain', () => {
  10370. // `.alignItems(x).layoutWeight(1)` under a leading_dot_expression: the
  10371. // wrapper consumes the dot, so the innermost call has a bare identifier
  10372. // function and would otherwise emit as a plain `alignItems(...)` call.
  10373. const chained = `
  10374. @Component
  10375. struct Card {
  10376. build() {
  10377. Column() {
  10378. List() {
  10379. Text('x')
  10380. }
  10381. .alignItems(HorizontalAlign.Start)
  10382. .layoutWeight(1)
  10383. .height('100%')
  10384. }
  10385. }
  10386. }
  10387. `;
  10388. const result = extractFromSource('components/Card.ets', chained);
  10389. const refs = callRefsFrom(result, 'build');
  10390. expect(refs).toContain('.alignItems');
  10391. expect(refs).not.toContain('alignItems');
  10392. expect(refs).toContain('.layoutWeight');
  10393. expect(refs).not.toContain('layoutWeight');
  10394. });
  10395. it('emits a call ref for an .onClick(this.handler) method-reference binding', () => {
  10396. const result = extractFromSource('pages/Page.ets', code);
  10397. expect(callRefsFrom(result, 'headerBar')).toContain('handleTap');
  10398. });
  10399. it('emits a call ref for a @Builder method invoked as this.headerBar()', () => {
  10400. const result = extractFromSource('pages/Page.ets', code);
  10401. expect(callRefsFrom(result, 'build')).toContain('headerBar');
  10402. });
  10403. it('extracts a global @Extend function with its decorator', () => {
  10404. const result = extractFromSource('pages/Page.ets', code);
  10405. const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'titleStyle');
  10406. expect(fn).toBeDefined();
  10407. expect(fn?.decorators).toContain('Extend');
  10408. });
  10409. });
  10410. describe('Global @Builder functions', () => {
  10411. it('extracts a decorated global @Builder function with signature and decorator', () => {
  10412. const result = extractFromSource(
  10413. 'common/builders.ets',
  10414. `@Builder\nfunction EmptyHint(message: string) {\n Column() {\n Text(message).fontSize(16)\n }\n}\n`
  10415. );
  10416. const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'EmptyHint');
  10417. expect(fn).toBeDefined();
  10418. expect(fn?.signature).toBe('(message: string)');
  10419. expect(fn?.decorators).toContain('Builder');
  10420. });
  10421. });
  10422. describe('Standard TypeScript constructs in .ets', () => {
  10423. it('extracts classes, interfaces, enums, type aliases and their members', () => {
  10424. const code = `
  10425. export enum Priority { Low, Medium = 2, High }
  10426. export interface Shape {
  10427. area(): number;
  10428. }
  10429. export type Handler = (e: string) => void;
  10430. export class Service {
  10431. private count: number = 0;
  10432. doWork(x: number): number {
  10433. return this.helper(x);
  10434. }
  10435. helper(n: number): number { return n * 2; }
  10436. }
  10437. `;
  10438. const result = extractFromSource('common/service.ets', code);
  10439. expect(result.nodes.find((n) => n.kind === 'class' && n.name === 'Service')).toBeDefined();
  10440. expect(result.nodes.find((n) => n.kind === 'enum' && n.name === 'Priority')).toBeDefined();
  10441. const members = result.nodes.filter((n) => n.kind === 'enum_member').map((n) => n.qualifiedName);
  10442. expect(members).toEqual(expect.arrayContaining(['Priority::Low', 'Priority::Medium', 'Priority::High']));
  10443. expect(result.nodes.find((n) => n.kind === 'interface' && n.name === 'Shape')).toBeDefined();
  10444. expect(result.nodes.find((n) => n.kind === 'type_alias' && n.name === 'Handler')).toBeDefined();
  10445. const doWork = result.nodes.find((n) => n.qualifiedName === 'Service::doWork');
  10446. expect(doWork?.kind).toBe('method');
  10447. expect(doWork?.signature).toBe('(x: number): number');
  10448. expect(
  10449. result.unresolvedReferences.find((r) => r.referenceKind === 'calls' && r.referenceName === 'helper')
  10450. ).toBeDefined();
  10451. });
  10452. });
  10453. describe('Import extraction', () => {
  10454. it('extracts relative, SDK (@ohos/@kit) and default imports', () => {
  10455. const code = `
  10456. import router from '@ohos.router';
  10457. import { promptAction } from '@kit.ArkUI';
  10458. import { TodoItem } from '../model/TodoItem';
  10459. import DataStore from '../data/DataStore';
  10460. `;
  10461. const result = extractFromSource('pages/imports.ets', code);
  10462. const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name);
  10463. expect(imports).toContain('@ohos.router');
  10464. expect(imports).toContain('@kit.ArkUI');
  10465. expect(imports).toContain('../model/TodoItem');
  10466. expect(imports).toContain('../data/DataStore');
  10467. });
  10468. });
  10469. });
  10470. // R7a preParse additions — the blanking passes added so macro-heavy C/C++
  10471. // parses clean enough for the kernel route (each also improves the wasm
  10472. // path's own graphs). Offset preservation is load-bearing everywhere.
  10473. describe('C/C++ kernel-port preParse blanks (R7a)', () => {
  10474. it('blankCCplusplusGuardBodies blanks extern-C guard bodies, keeps directives', async () => {
  10475. const { blankCCplusplusGuardBodies } = await import('../src/extraction/languages/c-cpp');
  10476. const src = [
  10477. '#ifdef __cplusplus',
  10478. 'extern "C" {',
  10479. '#endif',
  10480. 'int real_decl(void);',
  10481. '#ifdef __cplusplus',
  10482. '}',
  10483. '#endif',
  10484. '',
  10485. ].join('\n');
  10486. const out = blankCCplusplusGuardBodies(src);
  10487. expect(out.length).toBe(src.length);
  10488. expect(out).not.toContain('extern "C"');
  10489. expect(out).toContain('#ifdef __cplusplus'); // directives stay
  10490. expect(out).toContain('int real_decl(void);');
  10491. // A guard with a nested directive bails (needs real preprocessing).
  10492. const nested = [
  10493. '#ifdef __cplusplus',
  10494. '#define EXTERNC extern "C"',
  10495. '#endif',
  10496. '',
  10497. ].join('\n');
  10498. expect(blankCCplusplusGuardBodies(nested)).toBe(nested);
  10499. // The `#ifndef` inverse guard is C-visible and must be untouched.
  10500. const inverse = ['#ifndef __cplusplus', 'int c_only(void);', '#endif', ''].join('\n');
  10501. expect(blankCCplusplusGuardBodies(inverse)).toBe(inverse);
  10502. });
  10503. it('blankLoneMacroLines blanks namespace-management macros, spares expression operands', async () => {
  10504. const { blankLoneMacroLines } = await import('../src/extraction/languages/c-cpp');
  10505. const src = ['FMT_BEGIN_NAMESPACE', 'struct S { int x; };', 'FMT_END_NAMESPACE', ''].join('\n');
  10506. const out = blankLoneMacroLines(src);
  10507. expect(out.length).toBe(src.length);
  10508. expect(out).not.toContain('FMT_BEGIN_NAMESPACE');
  10509. expect(out).toContain('struct S { int x; };');
  10510. // An ALL-CAPS operand alone on a line inside a multi-line expression is
  10511. // NOT a lone macro — the next line starts with an operator.
  10512. const expr = ['int x = 0', ' | FLAG_ONE', ' | FLAG_TWO;', ''].join('\n');
  10513. expect(blankLoneMacroLines(expr)).toBe(expr);
  10514. const cont = ['int y =', 'SOME_FLAG', '| OTHER;', ''].join('\n');
  10515. expect(blankLoneMacroLines(cont)).toBe(cont);
  10516. // Underscore-free solid words are too risky and stay.
  10517. const bare = ['NDEBUG', 'int z;', ''].join('\n');
  10518. expect(blankLoneMacroLines(bare)).toBe(bare);
  10519. });
  10520. it('blankCDesignatedMacroArgs empties a designated-initializer macro call, offsets kept (#1729)', async () => {
  10521. const { blankCDesignatedMacroArgs } = await import('../src/extraction/languages/c-cpp');
  10522. const src = [
  10523. 'void resetProfile(profile_t *p)',
  10524. '{',
  10525. ' RESET_CONFIG(profile_t, p,',
  10526. ' .pid = { [PID_ROLL] = PID_ROLL_DEFAULT, [PID_YAW] = { 50, 75 } },',
  10527. ' .limit = 500, // trailing comma follows',
  10528. ' );',
  10529. ' log(.5);',
  10530. ' OTHER_MACRO(a == b, c);',
  10531. '}',
  10532. ].join('\n');
  10533. const out = blankCDesignatedMacroArgs(src);
  10534. expect(out.length).toBe(src.length);
  10535. expect(out.split('\n').length).toBe(src.split('\n').length);
  10536. expect(out).toContain('RESET_CONFIG(');
  10537. expect(out).not.toContain('.pid');
  10538. expect(out).not.toContain('PID_ROLL');
  10539. // The closing `);` keeps its column; the argument lines are spaces.
  10540. expect(out.split('\n')[5]).toBe(' );');
  10541. expect(out.split('\n')[3]).toBe(' '.repeat(src.split('\n')[3].length));
  10542. // A numeric literal and a comparison are not designators.
  10543. expect(out).toContain('log(.5);');
  10544. expect(out).toContain('OTHER_MACRO(a == b, c);');
  10545. });
  10546. it('a designated-initializer macro call no longer swallows the functions after it (#1729)', async () => {
  10547. const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1729-'));
  10548. try {
  10549. // Issue fixture: designated-initializer args + trailing comma. Without
  10550. // blankCDesignatedMacroArgs, tree-sitter-c error recovery extends
  10551. // `function_definition` to EOF — `g` vanishes and `h` nests as `f::h`.
  10552. fs.writeFileSync(
  10553. path.join(dir, 'pid.c'),
  10554. [
  10555. 'void f(void)',
  10556. '{',
  10557. ' M(a, b,',
  10558. ' .x = 1,',
  10559. ' .y = { 1, 2 },',
  10560. ' );',
  10561. '}',
  10562. '',
  10563. 'void g(void)',
  10564. '{',
  10565. '}',
  10566. '',
  10567. 'int h(void)',
  10568. '{',
  10569. ' return 1;',
  10570. '}',
  10571. '',
  10572. ].join('\n')
  10573. );
  10574. const cg = await CodeGraph.init(dir, { index: true });
  10575. try {
  10576. const fns = cg.getNodesByKind('function').filter((n) => n.filePath === 'pid.c');
  10577. const byName = Object.fromEntries(fns.map((n) => [n.name, n]));
  10578. expect(Object.keys(byName).sort()).toEqual(['f', 'g', 'h']);
  10579. expect(byName.f!.endLine).toBe(7);
  10580. expect(byName.g!.qualifiedName).toBe('g');
  10581. expect(byName.h!.qualifiedName).toBe('h');
  10582. } finally {
  10583. cg.close();
  10584. }
  10585. } finally {
  10586. fs.rmSync(dir, { recursive: true, force: true });
  10587. }
  10588. });
  10589. it('a large designated-initializer macro call keeps later functions top-level (#1729)', async () => {
  10590. const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1729-large-'));
  10591. try {
  10592. // Scale guard for betaflight-sized RESET_CONFIG argument lists.
  10593. const fields = Array.from({ length: 120 }, (_, i) => ` .field${i} = ${i},`).join('\n');
  10594. fs.writeFileSync(
  10595. path.join(dir, 'pid.c'),
  10596. `void resetProfile(profile_t *p)\n{\n RESET_CONFIG(profile_t, p,\n${fields}\n );\n}\n\nvoid g(void)\n{\n}\n\nint h(void)\n{\n return 1;\n}\n`
  10597. );
  10598. const cg = await CodeGraph.init(dir, { index: true });
  10599. try {
  10600. const fns = cg.getNodesByKind('function').filter((n) => n.filePath === 'pid.c');
  10601. const byName = Object.fromEntries(fns.map((n) => [n.name, n]));
  10602. expect(Object.keys(byName).sort()).toEqual(['g', 'h', 'resetProfile']);
  10603. expect(byName.resetProfile!.endLine).toBe(125);
  10604. expect(byName.g!.qualifiedName).toBe('g');
  10605. expect(byName.h!.qualifiedName).toBe('h');
  10606. } finally {
  10607. cg.close();
  10608. }
  10609. } finally {
  10610. fs.rmSync(dir, { recursive: true, force: true });
  10611. }
  10612. });
  10613. it('blankCStatementMacroCalls blanks indented iterator macros, keeps the block', async () => {
  10614. const { blankCStatementMacroCalls } = await import('../src/extraction/languages/c-cpp');
  10615. const src = [
  10616. 'static void walk(struct list *head) {',
  10617. '\tlist_for_each_entry(pos, head, member) {',
  10618. '\t\tuse(pos);',
  10619. '\t}',
  10620. '}',
  10621. '',
  10622. ].join('\n');
  10623. const out = blankCStatementMacroCalls(src);
  10624. expect(out.length).toBe(src.length);
  10625. expect(out).not.toContain('list_for_each_entry');
  10626. expect(out).toContain('use(pos);');
  10627. // A real call statement ends with `;` — untouched.
  10628. expect(out).toContain('use(pos);');
  10629. const call = ['void f(void) {', '\tdo_thing(a, b);', '}', ''].join('\n');
  10630. expect(blankCStatementMacroCalls(call)).toBe(call);
  10631. // Column-0 `name(args) {` is an implicit-int function definition — untouched.
  10632. const kandr = ['main(argc, argv)', '{', '\treturn 0;', '}', ''].join('\n');
  10633. expect(blankCStatementMacroCalls(kandr)).toBe(kandr);
  10634. // Control-flow keywords are never macros.
  10635. const ctrl = ['void g(int x) {', '\twhile (x) {', '\t\tx--;', '\t}', '}', ''].join('\n');
  10636. expect(blankCStatementMacroCalls(ctrl)).toBe(ctrl);
  10637. });
  10638. it('blankCTrailingParamAttrMacros blanks `name UNUSED` params, spares call args', async () => {
  10639. const { blankCTrailingParamAttrMacros } = await import('../src/extraction/languages/c-cpp');
  10640. const src = 'static int run(int argc UNUSED, const char **argv UNUSED)\n{\n\treturn 0;\n}\n';
  10641. const out = blankCTrailingParamAttrMacros(src);
  10642. expect(out.length).toBe(src.length);
  10643. expect(out).not.toContain('UNUSED');
  10644. expect(out).toContain('int argc ');
  10645. // A macro CONSTANT as a call argument is preceded by `,`/`(`, never by a
  10646. // bare identifier — untouched.
  10647. const call = 'void f(void) {\n\tconnect(sock, DEFAULT_TIMEOUT);\n}\n';
  10648. expect(blankCTrailingParamAttrMacros(call)).toBe(call);
  10649. });
  10650. it('blankCKernelAnnotations blanks sparse/section dunders, spares parameterized ones and real types', async () => {
  10651. const { blankCKernelAnnotations } = await import('../src/extraction/languages/c-cpp');
  10652. const src = [
  10653. 'static int __init audit_init(void) { return 0; }',
  10654. 'void copy(void __user *dst, const char *src);',
  10655. '__bpf_kfunc void bpf_iter_destroy(struct bpf_iter_num *it);',
  10656. '__printf(1, 2) void log_fmt(const char *fmt, ...);',
  10657. 'struct e *entry = container_of(r, struct audit_entry, rule);',
  10658. '__u32 count = 0;',
  10659. '',
  10660. ].join('\n');
  10661. const out = blankCKernelAnnotations(src);
  10662. expect(out.length).toBe(src.length);
  10663. expect(out).not.toContain('__init');
  10664. expect(out).not.toContain('__user');
  10665. expect(out).not.toContain('__bpf_kfunc');
  10666. // Parameterized annotations keep their name — blanking it would strand
  10667. // the argument list as a floating parenthesis.
  10668. expect(out).toContain('__printf(1, 2)');
  10669. // container_of's type-keyword argument blanks; other `struct` keywords stay.
  10670. expect(out).toContain('container_of(r, audit_entry, rule)');
  10671. expect(out).toContain('struct e *entry');
  10672. // Real dunder TYPES are not annotations.
  10673. expect(out).toContain('__u32 count');
  10674. });
  10675. it('blankCParameterizedAnnotationMacros blanks name+args whole, eats a stranded field semicolon', async () => {
  10676. const { blankCParameterizedAnnotationMacros } = await import('../src/extraction/languages/c-cpp');
  10677. const src = [
  10678. 'struct file *f __free(fput) = NULL;',
  10679. 'static void __printf(4, 0) log_it(int a, const char *fmt, ...);',
  10680. 'struct ctx {',
  10681. '\t__bpf_md_ptr(struct bpf_iter_meta *, meta);',
  10682. '};',
  10683. 'int keep = __hash(key);',
  10684. '',
  10685. ].join('\n');
  10686. const out = blankCParameterizedAnnotationMacros(src);
  10687. expect(out.length).toBe(src.length);
  10688. expect(out).not.toContain('__free');
  10689. expect(out).not.toContain('__printf');
  10690. // Mid-line match keeps its statement tail…
  10691. expect(out).toContain('= NULL;');
  10692. // …but a whole-line FIELD match eats the `;` too — a lone `;` field is
  10693. // itself a parse error while an empty struct body is not.
  10694. expect(out).not.toContain('__bpf_md_ptr');
  10695. expect(out.split('\n')[3]?.trim()).toBe('');
  10696. // Non-curated dunder calls are real code.
  10697. expect(out).toContain('__hash(key)');
  10698. });
  10699. it('blankCTypeKeywordArgs blanks bare type-keyword call args, spares valid look-alikes', async () => {
  10700. const { blankCTypeKeywordArgs } = await import('../src/extraction/languages/c-cpp');
  10701. const src = [
  10702. 'void j(void *head, void *map) {',
  10703. '\tvoid *p = kzalloc_obj(struct bpf_mount_opts);',
  10704. '\tvoid *e = list_first_entry(head,',
  10705. '\t\t\tstruct async_entry, domain_list);',
  10706. '\tvoid *n = hlist_entry_safe(rcu_dereference_raw(hlist_next_rcu(head)),',
  10707. '\t\t\tstruct bpf_dtab_netdev, index_hlist);',
  10708. '\treturn container_of(map, struct bpf_map, inner);',
  10709. '}',
  10710. 'DEFINE_PER_CPU(struct task_struct *, ksoftirqd);',
  10711. '',
  10712. ].join('\n');
  10713. const out = blankCTypeKeywordArgs(src);
  10714. expect(out.length).toBe(src.length);
  10715. expect(out).not.toContain('struct bpf_mount_opts');
  10716. expect(out).toContain(' bpf_mount_opts'); // keyword → spaces, ident stays
  10717. expect(out).toContain(' async_entry, domain_list');
  10718. expect(out).toContain(' bpf_dtab_netdev'); // nested-paren predecessor arg
  10719. expect(out).toContain(' bpf_map, inner'); // `return` precedes real calls
  10720. // Pointer form blanks the stars too — two plain identifier args remain.
  10721. expect(out).toContain(' task_struct , ksoftirqd');
  10722. // Valid look-alikes stay byte-identical.
  10723. for (const valid of [
  10724. 'int a = sizeof(struct point);',
  10725. 'int b = offsetof(struct point, y);',
  10726. 'int c = _Generic(x, struct foo *: 1, default: 0);',
  10727. 'int wf(struct a,\n\tstruct b);',
  10728. 'void cast(void *p) { use((struct foo *)p); }',
  10729. 'struct ops { int (*probe)(struct device *dev); };',
  10730. ]) {
  10731. expect(blankCTypeKeywordArgs(valid)).toBe(valid);
  10732. }
  10733. });
  10734. it('blankCFileScopePrefixedDeclMacros blanks static/extern CAPS-macro lines at any scope', async () => {
  10735. const { blankCFileScopePrefixedDeclMacros } = await import('../src/extraction/languages/c-cpp');
  10736. const src = [
  10737. 'static DEFINE_PER_CPU(struct llist_head, rstat_backlog_list);',
  10738. 'void f(void) {',
  10739. '\tstatic DEFINE_RATELIMIT_STATE(ratelimit, 5 * HZ, 5);',
  10740. '}',
  10741. 'EXPORT_SYMBOL(vmalloc);',
  10742. 'static DEFINE_PER_CPU(struct cpuhp_cpu_state, cpuhp_state) = {',
  10743. '',
  10744. ].join('\n');
  10745. const out = blankCFileScopePrefixedDeclMacros(src);
  10746. expect(out.length).toBe(src.length);
  10747. expect(out).not.toContain('DEFINE_PER_CPU(struct llist_head');
  10748. expect(out).not.toContain('DEFINE_RATELIMIT_STATE'); // block scope blanks too
  10749. // Bare CAPS lines parse natively as K&R declarations — untouched.
  10750. expect(out).toContain('EXPORT_SYMBOL(vmalloc);');
  10751. // Initializer forms belong to the rewrite, not the blank.
  10752. expect(out).toContain('cpuhp_state) = {');
  10753. });
  10754. it('rewriteCPrefixedDeclMacroInitializers rewrites `static CAPS(type, name) = {` into the declaration', async () => {
  10755. const { rewriteCPrefixedDeclMacroInitializers } = await import('../src/extraction/languages/c-cpp');
  10756. const line = 'static DEFINE_PER_CPU(struct cpuhp_cpu_state, cpuhp_state) = {';
  10757. const src = [line, '\t.fail = CPUHP_INVALID,', '};', ''].join('\n');
  10758. const out = rewriteCPrefixedDeclMacroInitializers(src);
  10759. expect(out.length).toBe(src.length);
  10760. const rewritten = out.split('\n')[0] as string;
  10761. expect(rewritten).toContain('static struct cpuhp_cpu_state');
  10762. expect(rewritten).not.toContain('DEFINE_PER_CPU');
  10763. // The NAME keeps its exact original column, and the tail its offsets.
  10764. expect(rewritten.indexOf('cpuhp_state')).not.toBe(-1);
  10765. expect(rewritten.indexOf('cpuhp_state', 30)).toBe(line.indexOf('cpuhp_state', 30));
  10766. expect(rewritten.indexOf('= {')).toBe(line.indexOf('= {'));
  10767. // Three-argument macros never match.
  10768. const threeArg = 'static DEFINE_TIMER(t, fn, 0) = {\n};\n';
  10769. expect(rewriteCPrefixedDeclMacroInitializers(threeArg)).toBe(threeArg);
  10770. });
  10771. it('blankCVaArgQualifiedTypeArgs blanks multi-token va_arg types, spares single tokens', async () => {
  10772. const { blankCVaArgQualifiedTypeArgs } = await import('../src/extraction/languages/c-cpp');
  10773. const src = 'void f(va_list ap) {\n\tconst char *s = va_arg(ap, const char *);\n\tint n = va_arg(ap, int);\n}\n';
  10774. const out = blankCVaArgQualifiedTypeArgs(src);
  10775. expect(out.length).toBe(src.length);
  10776. expect(out).toContain('va_arg(ap );');
  10777. expect(out).toContain('va_arg(ap, int);'); // parses natively — untouched
  10778. });
  10779. it('blankCNamedVariadicDefineDots blanks only the dots of GNU named-variadic params', async () => {
  10780. const { blankCNamedVariadicDefineDots } = await import('../src/extraction/languages/c-cpp');
  10781. const named = '#define verbose(env, fmt, args...) log_write(env, fmt, ##args)\nint x;\n';
  10782. const out = blankCNamedVariadicDefineDots(named);
  10783. expect(out.length).toBe(named.length);
  10784. expect(out).toContain('args )'); // dots → spaces
  10785. expect(out).toContain('##args'); // body untouched
  10786. const std = '#define pr(fmt, ...) printk(fmt, __VA_ARGS__)\nint y;\n';
  10787. expect(blankCNamedVariadicDefineDots(std)).toBe(std);
  10788. });
  10789. it('blankCSandwichedAnnotations and blankCAutoInference: sandwich and C23-auto guards', async () => {
  10790. const { blankCSandwichedAnnotations, blankCAutoInference } = await import(
  10791. '../src/extraction/languages/c-cpp'
  10792. );
  10793. const src = 'static notrace void tick_do(void) { }\nstatic nokprobe_inline void arm(void) { }\n';
  10794. const out = blankCSandwichedAnnotations(src);
  10795. expect(out.length).toBe(src.length);
  10796. expect(out).not.toContain('notrace');
  10797. expect(out).not.toContain('nokprobe_inline');
  10798. // As a variable name (no following word) it survives.
  10799. const varUse = 'void f(void) { int notrace = 1; use(notrace); }\n';
  10800. expect(blankCSandwichedAnnotations(varUse)).toBe(varUse);
  10801. const c23 = 'void q(void) { auto hb = get_hb(); }\n';
  10802. const autoOut = blankCAutoInference(c23);
  10803. expect(autoOut.length).toBe(c23.length);
  10804. expect(autoOut).toContain(' hb = get_hb();');
  10805. // The storage-class reading has a TYPE after `auto` — untouched.
  10806. const storage = 'void s(void) { auto int x = 1; }\n';
  10807. expect(blankCAutoInference(storage)).toBe(storage);
  10808. });
  10809. it('blankCStatementMacroCalls spans wrapped iterator macros, spares wrapped real calls', async () => {
  10810. const { blankCStatementMacroCalls } = await import('../src/extraction/languages/c-cpp');
  10811. const src = [
  10812. 'static void walk(void *head) {',
  10813. '\thlist_for_each_entry_rcu(p, head, hlist,',
  10814. '\t\t\t\t lockdep_is_held(&kprobe_mutex)) {',
  10815. '\t\tuse(p);',
  10816. '\t}',
  10817. '}',
  10818. '',
  10819. ].join('\n');
  10820. const out = blankCStatementMacroCalls(src);
  10821. expect(out.length).toBe(src.length);
  10822. expect(out).not.toContain('hlist_for_each_entry_rcu');
  10823. expect(out).not.toContain('lockdep_is_held');
  10824. expect(out).toContain('use(p);');
  10825. // A wrapped REAL call ends in `;` — untouched.
  10826. const call = 'void f(void) {\n\tdo_thing(a,\n\t\t b);\n}\n';
  10827. expect(blankCStatementMacroCalls(call)).toBe(call);
  10828. // A wrapped condition is keyword-led — untouched.
  10829. const cond = 'void g(int a) {\n\tif (check(a,\n\t\t a)) {\n\t\tuse(a);\n\t}\n}\n';
  10830. expect(blankCStatementMacroCalls(cond)).toBe(cond);
  10831. });
  10832. it('restoreDirectiveLines keeps #define lines out of the blanking blast radius', async () => {
  10833. const { extractFromSource } = await import('../src/extraction');
  10834. // FMT_API matches the _API-suffix member blank; without the directive
  10835. // restore the #define loses its NAME and the file gains a parse error.
  10836. const src = [
  10837. '#define FMT_API FMT_VISIBILITY("default")',
  10838. 'class Widget {',
  10839. ' public:',
  10840. ' int size() const { return 1; }',
  10841. '};',
  10842. '',
  10843. ].join('\n');
  10844. const result = extractFromSource('lib.hpp', src, 'cpp');
  10845. expect(result.errors).toEqual([]);
  10846. expect(result.nodes.some((n) => n.kind === 'class' && n.name === 'Widget')).toBe(true);
  10847. expect(result.nodes.some((n) => n.kind === 'method' && n.name === 'size')).toBe(true);
  10848. });
  10849. });