Sellest, kuidas parsida C# koodifaile..

Laisk inimene ei tee käsitööd vaid kulutab (halvemal juhul sama suurusjärku) aja loomaks vahendeid, mis nüri töö tema eest ära teeks. Mul oli vaja iga koodifaili kohta teada, mis nimeruume ta deklareerib ja millised klasse ta sisaldab. Failide arv on paari tuhande ringis, seega käsitsi üle käimine ei olnud kindlasti plaanis.

Regexp ?

Esimene mõte oli vanad head regulaaravaldised. Nende lugemine ja debugimine on küll tükk vaeva, aga päris palju saab ära teha. Lihtsustatud stsenaariumi nimeruumide leidmine on mõõdukal lihtne : need on faili alguses, enne deklaratsiooni on üldjuhul vaid using-käsud ja erinevad kommentaarid, võib-olla ka hunnik whitespace’i. Aga sisemised nimeruumid ? mitu nimeruumi failis?  atribuudid ? Klasside leidmine oleks sel meetodil veel kordades keerukam, sundides arvestama ka näiteks string-konstantides sisalduvaga jms..

Või parser ?

Ja siis taipasin – nii tore kui jalgratta leiutamine ja regulaaravaldiste kirjutamine ka ei ole, antud probleem on ju juba ammu lahendatud. Iga C# koodiparser teeb oma põhitöö seas just seda, mida mul vaja. Näiteks proovisin SharpDevelop’i (vabavarane IDE c# jm jaoks) koosseisu kuuluvat NRefactory-nimelist parserit.

Samm1: muretse NRefactory

NRefactory näib olevat kättesaadava ainult SharpDevelop koosseisus, mille installeri  saab siit: http://www.icsharpcode.net/OpenSource/SD/Download/, misjärel on teek leitav sõltuvalt installeerimise kohast, näiteks siit:

C:\Program Files (x86)\SharpDevelop\4.0\bin\ICSharpCode.NRefactory.dll

Kui sa ei taha SharpDevelopiga tutvuda, siis dll saab loomulikult ka SharpDevelopi lähtekoodist.

Samm2: Kood

using ICSharpCode.NRefactory;
using ICSharpCode.NRefactory.Ast;

Parseri loomisel ei ole just palju valikuid, kuid mõned siiski. kuna meid huvitavad ainult klassid-nimeruumid, siis sobib näiteks selline parseri loomine.

IParser parser = ParserFactory.CreateParser(@"C:\Somepath\somecodefile.cs");
parser.ParseMethodBodies = false;
parser.Parse();

if (parser.Errors.Count > 0)
{
throw new Exception(String.Format("Parsing of '{0}' failed, Errors: {1}.",
codeFile.FullName, parser.Errors.ErrorOutput));
}

// this is my method to query the information.
WalkFile(parser.CompilationUnit.Children);


Meetod WalkFile() käib rekursiivselt läbi AST (Abstract Syntax Tree) ja viskab mind huvitava informatsiooni konsooli. Loomulikult oleks kena rekursiooni vältida, aga see ei ole praegu oluline:

private static void WalkFile(List<INode> nodes)
{
foreach (var node in nodes)
{
var asNamespace = node as NamespaceDeclaration;
if (asNamespace != null)
{
Console.WriteLine("Namespace found: '{0}' from {1}",
asNamespace.Name, asNamespace.StartLocation.ToString());
}

var asType = node as TypeDeclaration;
if (asType != null) {
Console.WriteLine("Type found: '{0}' from {1}",
asType.Name, asType.StartLocation.ToString());
}
WalkFile(node.Children);
}
}

Ja tulemuseks näiteks midagi sarnast:

Namespace found: 'SomeRoot.Parent' from (Line 6, Col 1)
Type found: 'ClassInParent' from (Line 8, Col 2)
Type found: 'SubClass' from (Line 10, Col 3)
Namespace found: 'ChildNamespace' from (Line 23, Col 2)
Type found: 'ClassInChild' from (Line 25, Col 3)

Mõistmaks, mis klass-nimeruum, mille sees asub, peaks muidugi vanemate ahelat vms infot meeles pidama, aga see ei ole enam midagi keerulist.

mis edasi ?

Koodifailide parsimine võimaldab teha koodi pealt päris huvitavaid päringuid. Näiteks kontrollida:

  • kas klassid jms ikka kasutavad kokkulepitud nimetamisreegleid ?
  • millised klassid ei järgi kokkulepitud public/internal/private-reegleid?
  • millised koodifailid ei järgi “1 file = 1 class” põhimõtet ?
  • millised koodifailid ei asu õiges kaustas (näiteks nimeruumi järgi) ?
  • mitu rida on pikim meetod ?

Mitte kõike neist ei saa teha reflectioni abil ja erinevalt reflectionist ei ole sul vaja kompileerimist, vaid piisab märgatavalt vähematest eeldustest: ainult vaadatavad koodifailid peab olema süntaktiliselt korrektsed. Puuduvad teegid, või kompileerimist takistavad vead ei takista teisi faile analüüsimast. Või suvalist faili.

Võimalik on teha näiteks koodifailide konverteerijat, mis näiteks eemaldab või lisab atribuute, muudab klassivälju, näiteks field –> property jne. Üks väike video sel teemal on näiteks siin. AST abil koodifailide muutmisel on küll miinuseks see, et kommentaarid lähevad kaotsi, mistõttu on ehk kaval kasutada parsimist informatsiooni pärimisks ja kasutada parseri antud asukohainfot automaatsete muudatuste tegemiseks muid vahendeid kasutades.

Kommentaare ei ole: