AST (Abstract Syntax Tree)

AST (Abstract Syntax Tree) er en grafisk repræsentation af kildekode, der primært bruges af compilere til at læse kode og generere binære målprogrammer.

For eksempel vil AST’en for dette kodeeksempel:

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

se således ud:

Transformationen fra kildekode til en AST, er et meget almindeligt mønster ved behandling af alle typer strukturerede data. Den typiske arbejdsgang er baseret på “Creating a parser that converts raw data into an graph-based format, that can then be consumed by an rendering engine”.

Dette er dybest set processen med at konvertere rå data til i stærkt typede in-memory objekter, der kan manipuleres programmatisk.

Her er et andet eksempel fra det virkelig fede online værktøj astexplorer.net:

Bemærk, hvordan i billedet ovenfor den rå tekst i DOT-sproget blev konverteret til et objekttræ (som også kan forbruges/gemmeres som en json-fil).

Som udvikler, hvis du er i stand til at “se kode eller rådata som en graf”, har du foretaget et fantastisk paradigmeskift, som vil hjælpe dig enormt meget på tværs af din omsorgsgiver.

For eksempel har jeg brugt AST’er og brugerdefinerede parsere til at:

  • skrive tests, der tjekker, hvad der rent faktisk sker i koden (nemt, når du har adgang til en objektmodel af koden)
  • bruge logdata, som normale ‘regex’-baserede parsere havde svært ved at forstå
  • udføre statisk analyse af kode skrevet i brugerdefinerede sprog
  • transformere datadumps til in-hukommelsesobjekter (som derefter nemt kan transformeres og filtreres)
  • skabe transformerede filer med skiver af flere kildekodefiler (som jeg kaldte MethodStreams og CodeStreams) – Se eksempel nedenfor for hvordan dette ser ud i praksis
  • udføre brugerdefineret kode-refactoring (f.eks. for automatisk at rette sikkerhedsproblemer i kode) – Se eksempel nedenfor for hvordan dette ser ud i praksis

Når man opbygger en parser til en bestemt datakilde, er en vigtig milepæl at kunne gå fra AST’en tilbage til den oprindelige tekst (uden tab af data).

Det er præcis sådan, hvordan refaktorisering af kode i IDE’er fungerer (f.eks. når du omdøber en variabel, og alle forekomster af denne variabel omdøbes).

Sådan fungerer denne rundrejse:

  1. start med fil A (dvs. kildekoden)
  2. skabe AST’en for fil A
  3. skabe fil B som transformation fra AST’en
  4. fil A er lig med fil B (byte byte)

Når dette er muligt, bliver det nemt at ændre kode programmatisk, da vi vil manipulere stærkt typede objekter uden at bekymre os om at skabe syntaktisk korrekt kildekode (som nu er en transformation fra AST’en).

Skrivning af tests ved hjælp af ASTs objekter

Når du begynder at se din kildekode (eller data, som du forbruger) som “kun en AST-parser væk fra at være objekter, du kan manipulere”, åbner der sig et helt ord af muligheder og muligheder for dig.

Et godt eksempel er, hvordan du kan opdage et bestemt mønster i kildekoden, som du vil sikre dig, at det forekommer i et stort antal filer, lad os sige f.eks: “sikre, at en bestemt Authorization (eller data validering) metode kaldes på hver eksponeret web services metode”?

Dette er ikke et trivielt problem, da medmindre du er i stand til programmatisk at skrive en test, der kontrollerer for dette kald, er dine eneste muligheder:

  1. skrive et “standarddokument/wiki-side”, der definerer dette krav, og sørge for, at alle udviklere læser det, forstår det og endnu vigtigere følger det
  2. manuelt kontrollere, om standarden/kravet blev implementeret korrekt (på Pull Requests kodegennemgange)
  3. forsøge at bruge automatisering med “regex”-baserede værktøjer (kommercielle eller open source), og indse, at det er meget svært at få gode resultater ud af det
  4. tilbagefald til manuelle QA-tests (og sikkerhedsgennemgange) for at opsamle eventuelle blinde pletter

Men når du har mulighed for at skrive tests, der kontrollerer dette krav, sker der følgende:

  1. skrive tests, der forbruger kodens AST for at kunne kontrollere meget eksplicit, om standarden/kravet er korrekt implementeret/kodet
  2. via kommentarer i testfilen, kan dokumentationen genereres fra testkoden (i.dvs. der kræves ikke noget ekstra trin for at skabe dokumentation for denne standard/krav)
  3. kør disse tests som en del af det lokale build og som en del af den primære CI-pipeline
  4. ved at have en fejlslagen test, vil udviklerne vide ASAP, når et problem er blevet opdaget, og kan rette det meget hurtigt

Dette er et perfekt eksempel på, hvordan man kan skalere arkitektur- og sikkerhedskrav på en måde, der er indlejret i Software Development Lifecycle.

Vi har brug for AST’er til legacy- og cloud-miljøer

Desto mere man sætter sig ind i AST’er, jo mere går det op for en, at de er abstraktionslag mellem forskellige lag eller dimensioner. Endnu vigtigere er det, at de gør det muligt at manipulere et bestemt lags data på en programmatisk måde.

Men når man ser på de nuværende legacy- og cloud-miljøer (den del, som vi kalder “infrastruktur som kode”), vil man se, at store dele af disse økosystemer i dag ikke har AST-parsere til at konvertere deres virkelighed til programmerbare objekter.

Det er et fantastisk forskningsområde, hvor man kan fokusere på at skabe DSL’er (domænespecifikke sprog) til enten legacy-systemer eller til cloud-applikationer (vælg et af dem, da de hver især vil have helt forskellige sæt af kildematerialer). Et eksempel på den slags DSL, vi har brug for, er et sprog til at beskrive og kodificere Lambda-funktioners adfærd (nemlig de ressourcer, de har brug for at udføre, og hvad der er den forventede adfærd for Lambda-funktionen)

MethodStreams

Et af de mest kraftfulde eksempler på AST-manipulation, jeg har set, er MethodStreams-funktionen, som jeg tilføjede til O2-platformen.

Med denne funktion var jeg i stand til programmatisk at oprette en fil baseret på en bestemt metodes kaldstræ. Denne fil indeholdt al den kildekode, der var relevant for den oprindelige metode (genereret fra flere filer), og gjorde en enorm forskel, når jeg foretog kodegennemgange.

For at forstå, hvorfor jeg gjorde dette, skal vi starte med det problem, jeg havde.

Tilbage i 2010 foretog jeg en kodegennemgang af en .Net-applikation, der havde en million kodelinjer. Men jeg kiggede kun på WebServices-metoderne, som kun dækkede en lille del af denne kodebase (hvilket gav mening, da det var de metoder, der blev eksponeret til internettet). Jeg vidste, hvordan jeg kunne finde disse interneteksponerede metoder, men for at forstå, hvordan de fungerede, måtte jeg se på hundredvis af filer, som var de filer, der indeholdt kode i udførelsesstien for disse metoder.

Da jeg i O2-platformen allerede havde en meget stærk C#-parser og støtte til refaktorisering af kode (implementeret til REPL-funktionen), kunne jeg hurtigt skrive et nyt modul, der:

  1. starter på webtjenestemetode X
  2. beregnede alle metoder, der blev kaldt fra denne metode X
  3. beregnede alle metoder, der blev kaldt af 2. (rekursivt)
  4. fanger AST-objekterne fra alle de metoder, der er identificeret ved de foregående trin
  5. oprettede en ny fil med alle objekterne fra 4.

Denne nye fil var fantastisk, da den KUN indeholdt den kode, som jeg skal læse under min sikkerhedsgennemgang.

Men det blev endnu bedre, da jeg i denne situation var i stand til at tilføje validerings-RegEx’erne (som anvendes på alle WebServices-metoder) øverst i filen og tilføje kildekoden til de relevante Stored Procedures nederst i filen.

Sammenlagt havde nogle af de genererede filer 3k+ kodelinjer, hvilket var en massiv forenkling af de 20+ filer, som indeholdt dem (som sandsynligvis havde 50k+ kodelinjer).

Her er et godt eksempel på, at jeg kan gøre et bedre stykke arbejde ved at have adgang til et bredt sæt af muligheder og teknikker (i dette tilfælde evnen til at manipulere kildekoden programmatisk)

Denne type AST-manipulation er et forskningsområde, som jeg stærkt anbefaler, at du fokuserer på (hvilket også vil give dig en massiv værktøjskasse til dine daglige kodningsaktiviteter). Btw, Hvis du går denne vej, skal du også tjekke O2 Platforms CodeStreams, som er en videreudvikling af MethodStreams-teknologien. CodeStreams giver dig en strøm af alle alle variabler, der berøres af en bestemt kildevariabel (det, der i statisk analyse kaldes Taint flow analysis og Taint Checking)

Fixing code in real time (or at compilation time)

Et andet virkelig fedt eksempel på kraften i AST manipulation er den PoC, jeg skrev i 2011 om Fixing/Encoding .NET code in real time (i dette tilfælde Response.Write), hvor jeg viser, hvordan man programmatisk tilføjer et sikkerhedsfix til en sårbar by design-metode.

Her er hvordan UI’et så ud, hvor koden til venstre, blev transformeret programmatisk til koden til højre (ved at tilføje den ekstra AntiXSS.HtmlEncode wrapper-metode)

Her er kildekoden, der foretager transformationen og kodefixet (bemærk rundgangen af kode):

I 2018 er måden at implementere denne arbejdsgang på en udviklervenlig måde, at der automatisk oprettes en Pull Request med disse ekstra ændringer.

Skriv et svar

Din e-mailadresse vil ikke blive publiceret.