AST (Abstract Syntax Tree)

AST (Abstract Syntax Tree) este o reprezentare grafică a codului sursă folosită în principal de compilatoare pentru a citi codul și a genera binariile țintă.

De exemplu, AST-ul acestui exemplu de cod:

while b ≠ 0
if a > b
a := a − b
else
b := b − a
return a

Va arăta astfel:

Transformarea din codul sursă într-un AST, este un model foarte comun atunci când se procesează orice tip de date structurate. Fluxul de lucru tipic se bazează pe „Crearea unui parser care convertește datele brute într-un format bazat pe grafuri, care poate fi apoi consumat de un motor de redare”.

Acesta este, în esență, procesul de conversie a datelor brute în obiecte în memorie puternic tipizate, care pot fi manipulate în mod programatic.

Iată un alt exemplu de la un instrument online foarte interesant, astexplorer.net:

Observați cum în imaginea de mai sus textul brut din limbajul DOT a fost convertit într-un arbore de obiecte (care poate fi, de asemenea, consumat/salvat ca un fișier json).

În calitate de dezvoltator, dacă sunteți capabil să „vedeți codul sau datele brute sub forma unui grafic”, veți fi făcut o schimbare de paradigmă uimitoare care vă va ajuta extraordinar de mult pe parcursul îngrijirii dumneavoastră.

De exemplu, am folosit AST-uri și analizoare personalizate pentru a:

  • scrieți teste care verifică ce se întâmplă de fapt în cod (ușor atunci când aveți acces la un model de obiect al codului)
  • consumați date de jurnal pe care analizoarele normale bazate pe ‘regex’ se chinuiau să le înțeleagă
  • efectuați analiză statică pe cod scris în limbaje personalizate
  • transformați descărcările de date în in-obiecte în memorie (care pot fi apoi transformate și filtrate cu ușurință)
  • creați fișiere transformate cu felii de mai multe fișiere de cod sursă (pe care le-am numit MethodStreams și CodeStreams) – A se vedea exemplul de mai jos pentru a vedea cum arată acest lucru în practică
  • efectuați refactorizarea personalizată a codului (de exemplu, pentru a remedia automat problemele de securitate din cod) – A se vedea exemplul de mai jos pentru a vedea cum arată acest lucru în practică

Când construiți un analizor pentru o anumită sursă de date, o piatră de hotar este capacitatea de a face dus-întors, de a putea trece de la AST, înapoi la textul original (fără pierderi de date).

Acesta este exact modul în care funcționează refactorizarea codului în IDE-uri (de exemplu, atunci când redenumiți o variabilă și toate instanțele acelei variabile sunt redenumite).

Iată cum funcționează acest round-trip:

  1. începeți cu fișierul A (i.e. codul sursă)
  2. creați AST al fișierului A
  3. creați fișierul B ca transformare din AST
  4. fila A este egală cu fișierul B (octet cu octet)

Când acest lucru este posibil, devine ușor să modificăm codul în mod programatic, deoarece vom manipula obiecte puternic tipizate fără să ne facem griji cu privire la crearea unui cod sursă sintactic corect (care este acum o transformare din AST).

Scrierea testelor folosind obiecte AST

După ce începeți să vedeți codul sursă (sau datele pe care le consumați) ca fiind „la doar un parser AST distanță de a fi obiecte pe care le puteți manipula”, vi se va deschide un cuvânt întreg de oportunități și capacități.

Un bun exemplu este cum să detectați un anumit tipar în codul sursă despre care vreți să vă asigurați că apare într-un număr mare de fișiere, să spunem, de exemplu, că vreți: „să vă asigurați că o anumită metodă de autorizare (sau de validare a datelor) este apelată la fiecare metodă de servicii web expusă”?

Aceasta nu este o problemă trivială, deoarece, dacă nu sunteți în măsură să scrieți programatic un test care să verifice acest apel, singurele dvs. opțiuni sunt:

  1. scrieți un „document standard/pagină wiki” care să definească această cerință și asigurați-vă că toți dezvoltatorii îl citesc, îl înțeleg și, mai important, îl respectă
  2. verificați manual dacă acel standard/cerere a fost implementat corect (la revizuirile de cod Pull Requests)
  3. încercați să utilizați automatizarea cu instrumente bazate pe „regex” (comerciale sau open source), și vă dați seama că este foarte greu să obțineți rezultate bune de la acesta
  4. reveniți la testele QA manuale (și la revizuirile de securitate) pentru a detecta orice puncte oarbe

Dar, atunci când aveți capacitatea de a scrie teste care verifică această cerință, iată ce se întâmplă:

  1. scrieți teste care consumă AST-ul codului pentru a putea verifica foarte explicit dacă standardul/cerința a fost implementat/codificat corect
  2. prin intermediul comentariilor din fișierul de test, documentația poate fi generată din codul de test (i.adică nu este nevoie de niciun pas suplimentar pentru a crea documentația pentru acest standard/cerință)
  3. executați aceste teste ca parte a compilării locale și ca parte a pipeline-ului principal CI
  4. având un test eșuat, dezvoltatorii vor ști cât mai curând posibil odată ce a fost descoperită o problemă și o pot remedia foarte repede

Acesta este un exemplu perfect al modului în care se poate scala arhitectura și cerințele de securitate, într-un mod care este încorporat în ciclul de viață al dezvoltării de software.

Avem nevoie de AST pentru mediile moștenite și pentru mediile cloud

Cu cât te familiarizezi mai mult cu AST, cu atât mai mult îți dai seama că acestea sunt straturi de abstractizare între diferite straturi sau dimensiuni. Mai important, ele permit manipularea datelor unui anumit strat într-un mod programatic.

Dar când vă uitați la mediile moștenite și la mediile cloud actuale (partea pe care o numim „Infrastructură ca și cod”), ceea ce veți vedea sunt părți mari ale acestor ecosisteme care, în prezent, nu au parser AST pentru a converti realitatea lor în obiecte programabile.

Aceasta este o zonă excelentă de cercetare, în care v-ați concentra pe crearea de DSL-uri (limbaje specifice domeniului) fie pentru sistemele moștenite, fie pentru aplicațiile cloud (alegeți una dintre ele, deoarece fiecare va avea seturi complet diferite de materiale sursă). Un exemplu al tipului de DSL de care avem nevoie este un limbaj care să descrie și să codifice comportamentul funcțiilor Lambda (și anume resursele de care au nevoie pentru a se executa și care este comportamentul așteptat al funcției Lambda)

MethodStreams

Unul dintre cele mai puternice exemple de manipulare AST pe care le-am văzut, este caracteristica MethodStreams pe care am adăugat-o la Platforma O2.

Cu această caracteristică am reușit să creez în mod programatic un fișier bazat pe arborele de apeluri al unei anumite metode. Acest fișier conținea tot codul sursă relevant pentru acea metodă originală (generat din mai multe fișiere) și a făcut o diferență masivă atunci când făceam revizuiri de cod.

Pentru a înțelege de ce am făcut acest lucru, să începem cu problema pe care o aveam.

Înapoi în 2010 făceam o revizuire de cod a unei aplicații .Net care avea un milion de linii de cod. Dar mă uitam doar la metodele WebServices, care acopereau doar o mică parte din acea bază de cod (ceea ce avea sens, deoarece acelea erau metodele expuse la internet). Știam cum să găsesc acele metode expuse pe internet, dar pentru a înțelege cum funcționează, a trebuit să mă uit în sute de fișiere, care erau fișierele care conțineau codul din calea de execuție a acelor metode.

Din moment ce în platforma O2 aveam deja un parser C# foarte puternic și un suport de refactorizare a codului (implementat pentru caracteristica REPL), am putut să scriu rapid un nou modul care:

  1. începând cu metoda serviciului web X
  2. a calculat toate metodele apelate din acea metodă X
  3. a calculat toate metodele apelate de 2. (recursiv)
  4. capturarea obiectelor AST din toate metodele identificate de pașii anteriori
  5. crearea unui nou fișier cu toate obiectele de la 4.

Acest nou fișier a fost uimitor, deoarece conținea NUMAI codul pe care trebuie să îl citesc în timpul revizuirii de securitate.

Dar evenimentul a devenit și mai bun, deoarece, în această situație, am putut să adaug RegEx-urile de validare (aplicate tuturor metodelor WebServices) în partea de sus a fișierului și să adaug codul sursă al procedurilor stocate relevante în partea de jos a fișierului.

Câteva dintre fișierele generate aveau 3k+ linii de cod, ceea ce reprezenta o simplificare masivă a celor peste 20 de fișiere care le conțineau (care aveau probabil 50k+ linii de cod).

Aici este un bun exemplu al faptului că am reușit să fac o treabă mai bună, având acces la un set larg de capacități și tehnici (în acest caz, capacitatea de a manipula programatic codul sursă)

Acest tip de manipulare AST este un domeniu de cercetare pe care vă recomand să vă concentrați (ceea ce vă va oferi, de asemenea, un set masiv de instrumente pentru activitățile de codare de zi cu zi). Btw, Dacă mergeți pe această cale, verificați și CodeStreams de la O2 Platform, care reprezintă o evoluție a tehnologiei MethodStreams. CodeStreams vă va oferi un flux al tuturor variabilelor care sunt atinse de o anumită variabilă sursă (ceea ce în analiza statică se numește Taint flow analysis și Taint Checking)

Fixarea codului în timp real (sau în momentul compilării)

Un alt exemplu foarte interesant al puterii manipulării AST este PoC-ul pe care l-am scris în 2011 despre Fixarea/Encodarea codului .NET în timp real (în acest caz Response.Write), în care arăt cum să adaug în mod programatic o corecție de securitate la o metodă vulnerabilă prin proiectare.

Iată cum arată interfața de utilizare, unde codul din stânga, a fost transformat programatic în codul din dreapta (prin adăugarea metodei suplimentare AntiXSS.HtmlEncode wrapper AntiXSS.HtmlEncode)

Iată codul sursă care face transformarea și corecția de cod (observați întoarcerea de cod):

În 2018, modalitatea de a implementa acest flux de lucru într-un mod prietenos pentru dezvoltatori, este de a crea automat o Pull Request cu acele modificări suplimentare.

Lasă un răspuns

Adresa ta de email nu va fi publicată.