2012-06-27 16 views
6

Mam program C# regex-parsera z trzech plików w nim, z których każda zawiera statyczną klasę:Generowanie pliku T4, gdy pliki kodu samej klasy zostały zmodyfikowane

1) jedna klasa statyczna wypełnione słowników smyczkowych

static class MyStringDicts 
{ 
    internal static readonly Dictionary<string, string> USstates = 
     new Dictionary<string, string>() 
     { 
      { "ALABAMA", "AL" }, 
      { "ALASKA", "AK" }, 
      { "AMERICAN SAMOA", "AS" }, 
      { "ARIZONA", "AZ" }, 
      { "ARKANSAS", "AR" } 
      // and so on 
     } 
    // and some other dictionaries 
} 

2) Klasa, która zestawia te wartości do Regex

public static class Patterns 
{  
    Public static readonly string StateUS = 
     @"\b(?<STATE>" + CharTree.GenerateRegex(Enumerable.Union(
      AddrVals.USstates.Keys, 
      AddrVals.USstates.Values)) 
     + @")\b"; 

    //and some more like these 
} 

3) Niektóre kodu, który działa w oparciu o wyrażenia regularne tych ciągów:

public static class Parser 
{ 
    // heavily simplified example 
    public static GroupCollection SearchStringForStates(string str) 
    { 
     return Regex.Match(str, 
      "^" + Patterns.StateUS, 
      RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase).Groups; 
    } 
} 

Chciałbym być w stanie wygenerować 2) za pomocą szablonu T4, jak cały ten konkatenacji jest identyczna na każdym wykonania:

@"\b(?<STATE><#=CharTree.GenerateRegex(Enumerable.Union(
    AddrVals.USstates.Keys, 
    AddrVals.USstates.Values)#>)\b"; 

To działa, ale jeśli tworzę nowy członek MyStringDicts lub dodać/usunąć niektóre wartości ze swoich słowników, szablon T4 nie rozpozna ich, dopóki nie wykluczy Patterns.cs z kompilacji i rekompilacji. Ponieważ Parser zależy od Patterns, to naprawdę nie jest opcja - potrzebuję transformacji T4, aby uwzględnić zmiany w innych plikach w tej samej kompilacji.

Czego nie chcą rób:

  • Podział MyStringDicts do własnego projektu. Chciałbym zachować pliki w jednym projekcie, ponieważ są one logiczną jednostką.
  • Po prostu przenieś MyStringDicts w górę Patterns.cs. Muszę członków MyStringDicts do innych celów, zbyt (dla wyszukiwań słownika, lub w innych szablonów T4, na przykład.)

I przyjął porady here temat korzystania T4Toolbox na VolatileAssembly i takie, ale to wydaje się tylko pracować odwrotny kierunek, kiedy pliki klas muszą zostać rekompilowane po edycji szablonu T4.

Czy to jest możliwe? I?

edytowane dla jasności

+1

Czy możesz wyjaśnić więcej o tym, dlaczego to robisz? Widzę kilka sposobów radzenia sobie z tym, ale trudno jest wiedzieć, który z nich jest odpowiedni w twoim scenariuszu bez jakiegoś tła. – AVee

+0

Przy okazji, to są pomysły, które mam. Może podpowiedź jest wystarczająca, abyś mógł iść. Rozwiązanie dostarczone przez FuleSnabel prawdopodobnie zadziała, możesz też zrobić coś podobnego, używając rzeczy w przestrzeni nazw EnvDte. Ale może zrobi to coś prostszego. Możesz rozważyć umieszczenie klas, których potrzebujesz w T4 w oddzielnym projekcie i odniesienie do nich z szablonów. Można również dynamicznie kompilować i wykonywać kod, którego potrzebujesz dynamicznie w swoich szablonach T4. – AVee

+0

@AVee dziękuję za zmuszenia mnie do robienia właściwych rzeczy i uwzględnienia mojej faktycznej sprawy. – Arithmomaniac

Odpowiedz

4

Właśnie stworzył małą szablon testowy, który wykorzystuje EnvDte (Visual Studio Automation) oraz T4Toolbox biec przez pierwszego pliku. Pobiera plik przez projekt, więc nie ma potrzeby kompilacji przed uruchomieniem szablonu. W rzeczywistości, nawet podnosi niezapisane zmiany ...

Jest to zasadniczo to samo podejście, jak używa FullSnabel, ale bez potrzeby Roslyn.

<#@ template debug="false" hostspecific="True" language="C#" #> 
<#@ output extension=".cs" #> 
<#@ Assembly Name="System.Core.dll" #> 
<#@ dte processor="T4Toolbox.DteProcessor" #> 
<#@ TransformationContext processor="T4Toolbox.TransformationContextProcessor" #> 
<#@ assembly name="System.Xml" #> 
<#@ assembly name="EnvDTE" #> 
<#@ assembly name="EnvDTE80" #> 
<#@ import namespace="T4Toolbox" #> 
<#@ import namespace="EnvDTE" #> 
<#@ import namespace="EnvDTE80" #> 
<# 
    ProjectItem projectItem = TransformationContext.FindProjectItem("Dictionaries.cs"); 
    FileCodeModel codeModel = projectItem.FileCodeModel; 

    foreach (CodeElement element in codeModel.CodeElements) 
    { 
     CodeNamespace ns = element as CodeNamespace; 
     if(ns != null) 
     { 
      foreach(CodeElement ele in ns.Children) 
      { 
       CodeClass cl = ele as CodeClass; 

       if(cl != null && cl.Name == "Dictionaries") 
       { 
        foreach(CodeElement member in cl.Members) 
        { 
         // Generate stuff... 
         this.WriteLine(member.Name); 
        } 
       } 
      } 
     } 
    } 
#> 

Powinno to zadziałać, jeśli chcesz zachować oryginalne podejście.

To, co najwyraźniej robisz, to przechowywanie danych w pliku klasy. Można rozważyć przechowywanie list poza kodem (w pliku xml lub ini) i generowanie obu plików na podstawie tych danych. W ten sposób unikniesz problemu razem, może to również ułatwić zarządzanie listami. Jeśli nie przejmujesz się zbytnio zmianami na liście, możesz umieścić słowniki wewnątrz samego szablonu T4.

Inna alternatywa może całkowicie sobie z tym poradzić w kodzie. Można utworzyć podklasę Dictionary, która ma właściwość "Pattern" (lub funkcję GetPattern()). Analizator składni używałby następnie metody AddrVals.USstates.Pattern, a klasa wzorów nie będzie już potrzebna. W ten sposób nie potrzebujesz generowania kodu.

Być może opakowanie rzeczywistego słownika byłoby lepsze, ponieważ pozwala ukryć rzeczywistą kolekcję, aby upewnić się, że nie zostanie zmieniona w czasie wykonywania. Przykładem tego jest Is there a read-only generic dictionary available in .NET?.

+0

Po drugie uważam, że dane powinny być przechowywane w pliku "treści", a nie w kodzie. Sprawia, że ​​cały shebang jest dużo łatwiejszy do czytania i utrzymywania. –

3

Spójrz na roslyn. Pozwala na kompilację plików źródłowych na drzewach składni, z których można następnie przeglądać i generować kod. To CTP, ale sprawdziło się całkiem dobrze.

(Dodano próbkę Roslyn).

Stworzyłem plik o nazwie class2.cs w moim roztworu:

namespace StackOverflow 
{ 
    class Class2 
    { 
     public static int One() { return 8; } 
     public static int Eight(int x, double z) { return 8; } 
    } 
} 

Korzystanie z Roslyn CTP (potrzebują Państwo Visual studio SDK również) Stworzyłem ten prosty szablon T4, który wykorzystuje do analizowania Class2.cs Roslyn i produkcji na podstawie tego:

<#@ template hostspecific= "true"       #> 
<#@ assembly name  = "System.Core"      #> 
<#@ assembly name  = "Roslyn.Compilers"    #> 
<#@ assembly name  = "Roslyn.Compilers.CSharp"   #> 
<#@ import  namespace = "System.IO"      #> 
<#@ import  namespace = "System.Linq"      #> 
<#@ import  namespace = "Roslyn.Compilers.CSharp"   #> 

<# 

    var host = Path.GetFullPath(Host.ResolvePath(@".\Class2.cs")); 
    var content = File.ReadAllText(host); 

    var tree = SyntaxTree.ParseCompilationUnit(content); 

    var methods = tree 
     .GetRoot() 
     .ChildNodes() 
     .OfType<NamespaceDeclarationSyntax>() 
     .SelectMany(x => x.ChildNodes()) 
     .OfType<ClassDeclarationSyntax>() 
     .SelectMany(x => x.ChildNodes()) 
     .OfType<MethodDeclarationSyntax>() 
     .ToArray() 
     ; 
#>    

namespace StackOverflow 
{ 
    using System; 

    static partial class Program 
    { 
     public static void Main() 
     { 
<# 
    foreach (var method in methods) 
    { 
     var parent = (ClassDeclarationSyntax)method.Parent; 
     var types = method 
      .ParameterList 
      .ChildNodes() 
      .OfType<ParameterSyntax>() 
      .Select(t => t.Type.PlainName) 
      .ToArray() 
      ; 

     var plist = string.Join(", ", types); 
#> 
      Console.WriteLine("<#=parent.Identifier.ValueText#>.<#=method.Identifier.ValueText#>(<#=plist#>).ToString()"); 
<# 
    } 
#> 
     } 
    } 
} 

Ten szablon produkuje następujące dane wyjściowe na podstawie Class2.CS:

namespace StackOverflow 
{ 
    using System; 

    static partial class Program 
    { 
     public static void Main() 
     { 
       Console.WriteLine("Class2.One().ToString()"); 
       Console.WriteLine("Class2.Eight(int, double).ToString()"); 
      } 
    } 
} 

Nadzieja to pomaga

+0

Naprawdę wolałbym T4 rozwiązanie, ale weź co mogę uzyskać. Czy masz link do działającego przykładu tego? Bez jednego, to będzie koniec mojej ligi. – Arithmomaniac

+0

Uwaga; użyjesz Roslyn do kompilacji kodu C# do drzewa składni. Następnie użyjesz T4 do wygenerowania kodu. Niestety nie mam teraz działającej próbki. – FuleSnabel

+3

Dodałem próbkę, aby pokazać, jak używać Roslyn z T4. – FuleSnabel

Powiązane problemy