Сообщений 6    Оценка 637        Оценить  
Система Orphus

Генераторы Кода в VS.NET

Автор: Дмитрий Комаров
Источник: RSDN Magazine #1-2004
Опубликовано: 19.09.2004
Исправлено: 13.03.2005
Версия текста: 1.0
Генераторы кода в VS.NET
Рецепт простого генератора
Шаг 1
Шаг 2
Шаг 3
Отладка
Приручение базовых классов
Применение генераторов
Заключение
Ссылки

Настоящий программист ленив – если работа занимает неделю, 
то он потратит месяц,
но найдет способ выполнить ее за один день.

Исходные коды BaseCodeGeneratorWithSite.zip
Исходные коды vsnetgen.zip

В статье рассмотрено использование “Custom Tool” в VS.NET и приводится пример создания простого генератора кода. Текст рассчитан на знание C#, опыт работы с VS.NET и интерес к CASE-средствам.

Генераторы кода в VS.NET

Одним из интересных механизмов, добавленных в VS.NET, и объявленным вторым по крутизне в статье “Top 10 Cool Features of VS.NET” в октябрьском выпуске MSDN Magazine за 2002 год, была возможность обрабатывать исходные файлы генератором и создавать новые файлы, которые будут включены в тот же проект. Microsoft использовала этот механизм для создания исходного кода типизированых датасетов по XSD и в Web-сервисах. Crystal Reports, генератор отчетов, поставляемый с VS.NET, использует тот же механизм для обработки своих файлов.

Назначить генератор для файла проекта можно через панель свойств файла: выберите файл в проекте, откройте панель свойств (F4) и введите имя генератора в свойстве “Custom Tool”. Если VS.NET найдет генератор с заданным именем, то исходный файл и свойство “Custom Tool Namespace” будут переданы ему, результат обработки записан в новый файл, и файл добавлен в проект.


Генератор может быть определен для любого файла в проекте, и запускается каждый раз, когда происходит сохранение исходного файла, изменение свойства “Custom Tool” или “Custom Tool Namespace”. Visual Studio не перезапускает генерацию, если генератор был изменен. Если вы разрабатываете свой генератор или переходите на другую версию генератора, вам может потребоваться пропустить исходные файлы через генератор еще раз. Это можно сделать через пункт “Run Custom Tool” контекстного меню исходного файла, которое появится после введение значения в свойство “Custom Tool”.

Теоретически можно определять генераторы не только для исходных, но и для сгенерированых файлов – и далее по цепочке, но после каждой генерации свойства сгенерированого файла будут установлены VS в значения по умолчанию: “Build Action” определяется расширением файла, “Custom Tool” и “Custom Tool Namespace” пусты, что делает поддержку более чем одного “уровня вложенности генераторов” в проекте довольно утомительной.

ПРИМЕЧАНИЕ

VSS:

При использовании VSS, CVS или другого средства контроля версий VS автоматически добавит сгенерированый файл в то же место репозитория, что и исходный.

Список доступных генераторов VS.NET 2003 берет при запуске из реестра по ключу - HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\7.1\Generators. Для VS 7 используется ключ HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\7.0\Generators.

Visual Studio разделяет генераторы по языкам, с которыми они работают. Для каждого языка определен свой уникальный идентификатор и свой набор генераторов:

Языкидентификатор
C#{FAE04EC1-301F-11d3-BF4B-00C04F79EFBC}
VB.NET{164B10B9-B200-11D0-8C61-00A0C91E29D5}
J#{E6FDF8B0-F3D1-11D4-8576-0002A516ECE8}

Ниже дан пример записи в реестре генератора для проектов на C#:

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\7.1\Generators\{FAE04EC1-301F-11d3-BF4B-00C04F79EFBC}\MyCustomGen]
@="My Custom Generator"
"CLSID"="{78309800-200C-493A-BE9D-0CD7444DC484}"
"GeneratesDesignTimeSource"=dword:00000001

Здесь имя ключа “MyCustomGen” – это имя генератора, как оно будет введено в свойство “Custom Tool”; значение ключа не используется; CLSID – уникальный идентификатор, используемый VS.NET для поиска сборки генератора в списке зарегистрированных классов в реестре; “GeneratesDesignTimeoutSource”, установленный в “1”, показывает, что результат генерации будет помещен в новый файл проекта.

Традиционно класс генератора наследуется от абстрактных классов Microsoft.VSDesigner.CodeGenerator.BaseCodeGenerator или BaseCodeGeneratorWithSite, которые находятся в microsoft.vsdesigner.dll. Эти классы имели модификатор доступа public в Visual Studio 7, что позволяло сторонним разработчикам генераторов просто подключать microsoft.vsdesigner.dll к своим проектам, чтобы использовать базовые классы. Однако в версии 7.1 эти классы сделаны internal, .поэтому для поддержки существующих генераторов с VS.NET 2003 и создания новых, потребуется написать базовые классы самим или скачать и использовать исходный код базовых классов из публично доступных проектов [1], [2], [3] и т.д.

Рецепт простого генератора

Для начала пойдем традиционным путем и напишем генератор, используя базовые классы из GotDotNet User Sample BaseCodeGeneratorWithSite [2].

Для этого нужно:

  1. Создать новый solution и проект, добавить исходники BaseCodeGenerator, добавить ссылки.
  2. Написать основной класс генератора.
  3. Подписать сборку и зарегистрировать новый генератор.

Шаг 1

Начнем с создания пустого solution “CodeGenerators” и нового проекта C# class library “CustomGen”. Возьмем исходный код базовых классов генератора [2] и добавим в проект следующие файлы из папки BaseCodeGeneratorWithSite\BaseCodeGeneratorWithSite.2003:

Добавим ссылки (путь к файлам может отличаться в зависимости от того, куда установлен Windows, Program Files и от версии VS.NET):

ПРИМЕЧАНИЕ

Библиотека microsoft.visualstudio.designer.interfaces.dll включена в пример на www.GotDotNet.com, но ее также можно найти в \Program Files\Microsoft Visual Studio .NET 2003\Common7\IDE – возможно, автор хотел подстраховаться на случай, если MS изменит часть интерфейсов.

Скомпилируем проект, чтобы убедиться, что исходный код BaseCodeGenerator подключен без ошибок.

Шаг 2

Удалим файл Class1.cs из проекта и создадим новый класс GeneratorMain:

GeneratorMain.cs
using System;
using System.Runtime.InteropServices;
using CustomToolGenerator;

namespace CustomGen
{
  [Guid("78309800-200C-493A-BE9D-0CD7444DC484")]
  public class GeneratorMain : BaseCodeGenerator
  {
    public override string GetDefaultExtension()
    {
      return ".cs";
    }

    protected override byte[] GenerateCode(
      string inputFileName,
      string inputFileContent)
    {
      string code = @"using System;
namespace Boo
{
  public class HelloWorld
  {
    public static void Hi()
    {
      Console.Out.WriteLine(""Hi, my namespace is "
        + base.FileNameSpace + @""");
    }
  }
}
";
      return System.Text.Encoding.ASCII.GetBytes(code);
    }
  }
}

Пройдемся по коду: сначала сердце генератора – метод GenerateCode. В него передается полный путь и содержимое исходного файла. Текст, сконвертированный в массив байтов и возвращенный из этого метода, будет помещен в сгенерированный файл. Значение свойства “Custom Tool Namespace” доступно через свойство FileNameSpace базового класса – эту строку можно использовать для передачи в генератор произвольных параметров, необязательно именно namespace. Если пользователь оставит свойство “Custom Tool Namespace” файла незаполненым, то значение FileNameSpace будет равно значению namespace по умолчанию.

Конечно, в таком виде наш генератор практически бесполезен, поскольку результат его работы пока никак не зависит от содержимого входного файла, но давайте сначала просто подключим этот примитивный генератор к VS.

Атрибут класса GUID используется при регистрации генератора. Если сборка содержит более одного генератора, то этот идентификатор позволит VS различать их при загрузке.

ПРИМЕЧАНИЕ

Имя сгенерированого файла не указывается явно: VS возьмет имя исходного файла, подставит расширение, указанное в методе GetDefaultExtension, и положит файл в ту же директорию. Если файл с таким именем уже существует, то к имени будет добавлен суффикс “1”. Например, в нашем случае из файла Class1.cs будет сгенерирован файл Class11.cs.

Шаг 3

Создадим SNK-файл при помощи sn.exe, укажем путь к нему в AssemblyInfo.cs и скомпилируем проект.

Фрагмент AssemblyInfo.cs
[assembly: AssemblyKeyFile(@"..\..\CustomGen.snk")]

Теперь нам надо зарегистрировать сборку и сообщить VS.NET, что у нее появился новый генератор. Чтобы не повторять эти операции вручную, можно создать файл CustomGen.reg и добавить в проект пакетный файл, который будет выполняться в случае удачной компиляции:

CustomGen.reg
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\7.1\Generators\{FAE04EC1-301F-11d3-BF4B-00C04F79EFBC}\MyCustomGen]
@="My Custom Generator"
"CLSID"="{78309800-200C-493A-BE9D-0CD7444DC484}"
"GeneratesDesignTimeSource"=dword:00000001
postbuild.cmd
rem Post-build Event Command Line:
rem "$(ProjectDir)postbuild.cmd" "$(DevEnvDir)" $(TargetFileName) $(ProjectName)

%1..\..\Sdk\v1.1\Bin\tlbexp %2

%SystemRoot%\Microsoft.NET\Framework\v1.1.4322\regasm /codebase %2

regedit /S ..\..\%3.reg

Каждая новая версия сборки генератора будет добавлять новую запись в реестр. Чтобы не плодить лишних записей, можно зафиксировать номер версии в AssemblyInfo.cs или же удалять предыдущие версии сборки перед добавлением новой, как это сделано в приведенном ниже командном файле. Создание файла ключа, кстати, тоже можно занести в команду компиляции:

prebuild.cmd
rem Pre-build Event Command Line:
rem "$(ProjectDir)prebuild.cmd" "$(DevEnvDir)" $(ProjectName)

IF NOT EXIST ..\..\%3.snk %1..\..\Sdk\v1.1\Bin\sn -q -k ..\..\%3.snk

%SystemRoot%\Microsoft.NET\Framework\v1.1.4322\regasm /unregister %2

Версии .Net Framework и VS в приведенном коде можно изменить, если у вас установлены другие.

После успешной компиляции и запуска VS.NET генератор MyCustomGen можно использовать с файлами C#-проекта. Давайте посмотрим, как он работает, на примере отладки.

Отладка

Отлаживать генератор можно как и любой другой проект: в свойствах проекта установим режим отладки “Program”, выберем стартовый файл VS.NET IDE devenv.exe в “Start Application” и запустим отладку.

В открывшейся Visual Studio создадим новый проект C# - например, Class Library. В свойствах файла Class1.cs установим “Custom Tool” в “MyCustomGen” и “Custom Tool Namespace” в “Something”.


Чтобы найти сгенерированый файл, нужно переключить Solution Explorer в режим “Show All Files” – новый файл будет прикреплен к исходному, носить то же имя, что и исходный файл (возможно с добавленым символом “1”), расширение “.cs” и код:

using System;
namespace Boo
{
  public class HelloWorld
  {
    public static void Hi()
    {
      Console.Out.WriteLine("Hi, my namespace is Something");
    }
  }
}

На приведенном экране можно заметить небольшой трюк: метод GetDefaultExtension был изменен, и вместо “.cs” возвращает “.generated.cs” – таким образом, в Windows или VSS Explorer сгенерированые файлы явно отличаются от исходных.

Вот собственно и все, мы можем себя поздравить: наш простенький генератор, наконец, заработал.

Теперь, для полноты ощущений, попытаемся понять – что именно делает взятый нами довольно объемный код базовых классов генератора и для чего нужны подключенные библиотеки.

Приручение базовых классов

Как выясняется, из подключенного кода реально была использована только очень небольшая часть – пара интерфейсов и один базовый класс. Остальной код вступает в игру только при работе с BaseCodeGeneratorWithSite для доступа к контексту – CodeProvider текущего проекта и другим объктам VS, которая запустила генератор. Для простых случаев, когда контекст исполнения генератора неважен, можно оставить только необходимые интерфейсы и класс, показаные в следующем коде:

CustomToolBase.cs
using System;
using System.Runtime.InteropServices;

namespace CustomToolGenerator
{
  public abstract class CustomToolBase : IVsSingleFileGenerator
  {
    /// <summary>
    /// TODO: Укажите расширение сгенерированного файла
    /// </summary>
    /// <returns>Расширение, например ".cs"</returns>
    abstract public string GetDefaultExtension();

    /// <summary>
    /// TODO: Напишите основную функцию генератора
    /// <param name="fileContent">Содержимое исходного файла</param>
    /// <returns>Содержимое сгенерированого файла</returns>
    abstract protected string GenerateCode(string fileContent);

    /// <summary>
    /// Свойство "CustomToolNamespace" исходного файла
    /// </summary>
    protected string CustomToolNamespace
    {
      get{ return customToolNamespace; }
    }

    /// <summary>
    /// Полный путь к исходному файлу
    /// </summary>
    protected string InputFilePath
    {
      get{return inputFilePath;}
    }

    /// <summary>
    /// IDE API: Сообщить статус выполнения в IDE 
    /// </summary>
    /// <param name="complete">Количество законченых шагов или %</param>
    /// <param name="total">Общее количество шагов или 100 для процентов</param>
    protected void SetProgress(int complete, int total)
    {
      if (codeGeneratorProgress == null) 
        return;

      codeGeneratorProgress.Progress(complete, total);
    }

    /// <summary>
    /// IDE API: Послать сообщение об ошибке в IDE
    /// </summary>
    /// <param name="isWarning">Предупреждение или ошибка</param>
    /// <param name="level">Уровень предупреждения</param>
    /// <param name="error">Текст сообщения</param>
    /// <param name="line">Строка</param>
    /// <param name="column">Позиция в строке</param>
    protected void ReportError(bool isWarning, int level,
      string error, int line, int column)
    {
      if (codeGeneratorProgress == null) return;

      codeGeneratorProgress.GeneratorError(
        isWarning, level, error, line, column);
    }
    
    private IVsGeneratorProgress codeGeneratorProgress;
    private string customToolNamespace;
    private string inputFilePath;

    /// <summary>
    /// Точка входа в генератор из IDE
    /// </summary>
    public void Generate(
      string               wszInputFilePath,
      string               bstrInputFileContents,
      string               wszDefaultNamespace,
      out System.IntPtr    rgbOutputFileContents,
      out int              pcbOutput,
      IVsGeneratorProgress pGenerateProgress)
    {
      inputFilePath         = wszInputFilePath;
      customToolNamespace   = wszDefaultNamespace;
      codeGeneratorProgress = pGenerateProgress;

      string code = GenerateCode(bstrInputFileContents);
      
      if (code == null) 
        throw new NullReferenceException(
          "Generated code is empty.");

      byte[] bytes = System.Text.Encoding.ASCII.GetBytes(code);
      pcbOutput = bytes.Length;
      rgbOutputFileContents = Marshal.AllocCoTaskMem(pcbOutput);
      Marshal.Copy(bytes, 0, rgbOutputFileContents, pcbOutput);
    }
  }


  [ComImport, 
  Guid("3634494C-492F-4F91-8009-4541234E4E99"), 
  InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
  public interface IVsSingleFileGenerator 
  {
    /// <summary>
    /// Расширение сгенерированого файла
    /// </summary>
    /// <returns>расширение, например “.cs”</returns>
    [return: MarshalAs(UnmanagedType.BStr)]
    string GetDefaultExtension();

    /// <summary>
    /// Точка входа в генератор из IDE
    /// </summary>
    /// <param name="wszInputFilePath">
    ///        Имя и полный путь к исходному файлу</param>
    /// <param name="bstrInputFileContents">
    ///        Текст исходного файла</param>
    /// <param name="wszDefaultNamespace">
    ///        Параметр CustomToolNamespace </param>
    /// <param name="rgbOutputFileContents">
    ///        Сгенерированый код в массиве байт</param>
    /// <param name="pcbOutput">
    ///        Размер сгенерированого кода</param>
    /// <param name="pGenerateProgress">
    ///        callback-методы генератора</param>
    void Generate(
      [MarshalAs(UnmanagedType.LPWStr)] string wszInputFilePath,
      [MarshalAs(UnmanagedType.BStr)]   string bstrInputFileContents,
      [MarshalAs(UnmanagedType.LPWStr)] string wszDefaultNamespace, 
                                    out IntPtr rgbOutputFileContents,
      [MarshalAs(UnmanagedType.U4)] out int    pcbOutput,
      IVsGeneratorProgress                     pGenerateProgress);
  }


  [ComImport, 
  Guid("BED89B98-6EC9-43CB-B0A8-41D6E2D6669D"), 
  InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
  public interface IVsGeneratorProgress 
  {
    /// <summary>
    /// IDE API: Послать сообщение об ошибке в IDE
    /// </summary>
    /// <param name="isWarning">Предупреждение или ошибка</param>
    /// <param name="dwLevel">Уровень предупреждения</param>
    /// <param name="bstrError">Текст сообщения</param>
    /// <param name="dwLine">Строка</param>
    /// <param name="dwColumn">Позиция в строке</param>
    void GeneratorError(                  bool   isWarning,
      [MarshalAs(UnmanagedType.U4)]   int    dwLevel,
      [MarshalAs(UnmanagedType.BStr)] string bstrError,
      [MarshalAs(UnmanagedType.U4)]   int    dwLine,
      [MarshalAs(UnmanagedType.U4)]   int    dwColumn);

    /// <summary>
    /// IDE API: Сообщить статус выполнения в IDE 
    /// </summary>
    /// <param name="nComplete">Количество законченых шагов или %</param>
    /// <param name="nTotal">Общее количество шагов или 100 для процентов</param>
    void Progress(
      [MarshalAs(UnmanagedType.U4)] int nComplete, 
      [MarshalAs(UnmanagedType.U4)] int nTotal);
  }
}

В коде базового класса есть два еще незнакомых нам метода: ReportError и SetProgress.

Метод ReportError может использоваться генератором для сообщения об обнаруженных ошибках и выдачи предупреждений. При вызове этого метода VS добавит сообщение в список задач (Task List) и обработает его таким же образом, как любую другую ошибку или предупреждение компилятора.

SetProgress имеет смысл использовать, если генерация может занять существенное время – чтобы продемонстрировать пользователю, что Visual Studio не зависла.

Теперь заменим код базовых классов нашим кодом, уберем ссылки на ненужные библиотеки из проекта и слегка упростим код генератора:

GeneratorMain.cs
using System;
using System.Runtime.InteropServices;

namespace CustomToolGenerator
{
  [Guid("78309800-200C-493A-BE9D-0CD7444DC484")]
  public class GeneratorMain : CustomToolBase
  {
    public override string GetDefaultExtension()
    {
      return ".generated.cs";
    }

    protected override string GenerateCode(string fileContent)
    {
      return @"using System;
namespace Boo
{
  public class HelloWorld
  {
    public static void Hi()
    {
      Console.Out.WriteLine(""Hi, my namespace is "
        + FileNameSpace + @""");
    }
  }
}
";
    }
  }
}

На этом месте можно остановиться и заархивировать проект – у нас получился очень небольшой по объему шаблон, который удобно использовать для создания простых генераторов [6].

Применение генераторов

Практическое использование генератора ограничено только фантазией разработчика: создание исходного кода по XML или XSD, прекомпилятор, генератор справочных файлов, стандартных тестов или отчетов о соответствии принятым в компании стандартам по исходному коду класса и т.д.

Лучше сразу наложить одно довольно очевидное ограничение на пользователей генератора: стоит запретить редактирование сгенерированных файлов, поскольку те могут быть обновлены генератором в любой момент. Если у пользователя генератора может возникнуть желание расширять созданый генератором код, то наиболее безболезненный вариант – наследование от сгенерированых классов. У меня сложилось ощущение, что основная причина, по которой был очень быстро переписан родной генератор DataSet-ов Microsoft, была в создании им sealed-классов в VS.NET 7.0. В 2003-ей VS.NET модификатор sealed был убран, но почувствовавшие удобство генераторов разработчики уже вовсю переписывали микрософтовский генератор, заодно расширяя его возможности.

Генерация исходного кода из XML, например, при помощи XSLT - задача скорее творческая, поскольку техническая реализация ее в .NET сравнительно легка и подробно рассмотрена. Но раз речь зашла о генераторе DataSet-ов, то стоит упомянуть примененное в нем интересное решение: исходный XSD-файл сначала используется как схема для временного DataSet-а, затем этот DataSet разбирается генератором, который формирует код типизированого DataSet-а при помощи CodeDOM. Доработанные версии генератора вносят дополнительную функциональность в результируюший код, обрабатывая расширенные свойства элементов DataSet-а.

Генератор DataSet-ов обращается к объектам CodeDOM через второй “традиционный” базовый класс генератора BaseCodeGeneratorWithSite, который расширяет функциональность BaseCodeGenerator свойствами и методами доступа к объектам Visual Studio. Среди доступных объектов есть парсер исходного кода на языке текущего проекта в System.CodeDom.CodeCompileUnit и генератор, создающий исходный код по CodeCompileUnit.

Приведенный ниже пример кода пропускает исходный файл через парсер и преобразует обратно через генератор CodeDOM’а:

Фрагмент класса GeneratorMain : BaseCodeGeneratorWithSite
protected override byte[] GenerateCode(
  string inputFileName,
  string inputFileContent)
{
  StringReader         reader    = new StringReader(inputFileContent);
  StringWriter         writer    = new StringWriter();
  CodeGeneratorOptions options   = new CodeGeneratorOptions();
  ICodeGenerator       generator = CodeProvider.CreateGenerator();
  ICodeParser          parser    = CodeProvider.CreateParser();
  CodeCompileUnit      codeUnit  = parser.Parse(reader);
      
  generator.GenerateCodeFromCompileUnit(codeUnit, writer, options);

  string code = writer.ToString();

  return System.Text.Encoding.ASCII.GetBytes(code);
}

К сожалению, если мы запустим генератор в таком виде на следующем тестовом файле:

AClass.cs
01 | using System;
02 | namespace Namespace
03 | {
04 |  public abstract class AClass
05 |  {
06 |    public AClass() {}
07 |
08 |    int n;
09 |
10 |    public int Add(int a, int b)
11 |    {
12 |      // return a + b;
13 |    }
14 |  }
15 | }

Полученный результат будет существенно отличаться от исходного текста.

AClass.generated.cs
01 | namespace Namespace {
02 |   
03 |   class AClass : object {
04 |     
05 |     #line 8 "C:\...\AClass.cs"
06 |     private int n;
07 |     
08 |     #line default
09 |     #line hidden
10 |     
11 |     static AClass() {
12 |     }
13 |     
14 |     public virtual int Add(int a, int b) {
15 |       //  return a + b;
16 | //
17 |     }
18 |   }
19 | }

Приручение парсера CodeDOM выходит за рамки этой статьи, поэтому отметим только один прием, использованный CodeDOM-ом: применение директивы #line позволяет отлаживать код в исходном файле вместо сгенерированного. В приведенном примере точка останова в исходном файле в восьмой строке AClass.cs будет корректно обработана отладчиком, и при исполнении файла AClass.generated.cs отладочная информация будет указывать на оригинальный код в AClass.cs. Если вы работали с ASP.NET, то наверняка уже знакомы с удобствами такого использования директивы #line в автоматически сгенерированном коде.

Давайте попробуем другой подход и обработаем исходник без привлечения тяжелой артилерии, например, при помощи регулярных выражений. Предположим, что от вас требуется, чтобы класс предоставлял доступ только через свойства, скрывая поля, но реальной логики при доступе к свойствам класса немного, и большая часть кода будет выглядеть как public-свойство, предоставляющее доступ к private-полю. Поставим перед генератором задачу найти в коде недореализованные свойства – строки в формате, выделенном ниже:

SomeClass.cs
namespace sandbox
{
  public class SomeClass
  {
    public SomeClass (){}

    public int A {get; set;}
  }
}

И преобразовать их к виду

SomeClass.generated.cs
namespace sandbox
{
  public class SomeClass
  {
    public SomeClass (){}

    private int m_A;

    public int A { get{return m_A;} set{m_A=value;} }
  }
}

Небольшое изменение нашего простого генератора позволит достичь требуемого результата:

GeneratorMain.cs
using System;
using System.Text.RegularExpressions;
using System.Runtime.InteropServices;

namespace CustomToolGenerator
{
  [Guid("78309800-200C-493A-BE9D-0CD7444DC484")]
  public class GeneratorMain : CustomToolBase
  {
    public override string GetDefaultExtension()
    {
      return ".generated.cs";
    }

    protected override string GenerateCode(string inputFileName)
    {
      string[,] rules =
      {
        {
          // правило замены для read/write свойств
          @"(\s*)(public|protected)(\s+)(\S+)(\s+)(\S+)(\s*)" +
          @"{(\s*)(get)(\s*);(\s*)(set)(\s*);(\s*)}",
          
          @"$1private$3$4$5m_$6;$1$2$3$4$5$6$7" +
          @"{ get{return m_$6;} set{m_$6=value;} }"
        },
        {
        // правило замены для read-only свойств
        @"(\s*)(public|protected)(\s+)(\S+)(\s+)(\S+)(\s*)" +
        @"{(\s*)(get)(\s*); (\s*)}",
          
        @"$1private$3$4$5m_$6;$1$2$3$4$5$6$7" +
        @"{ get{return m_$6;} }"
        }
      };

      string output = inputFileContent;
      for(int n = 0; n < rules.GetLength(0); n++)
      {
        output = Regex.Replace(
          output, rules[n, 0], rules[n, 1],
          RegexOptions.Multiline);
      }

      return output;
    }
  }
}

При желании количество правил поиска и замены в приведенном коде может быть увеличено, правила можно вынести во внешний конфигурационный файл, добавить более сложные проверки, превращающие этот генератор в достаточно сложный препроцессор, ориентированный на ваши задачи.

Поскольку исходный C#-файл синтаксически некорректен и не может быть скомпилирован, пользователь должен будет поставить его свойство “Build Action” в “None”. Чтобы избежать использования в проекте исходного кода, не участвующего в компиляции, можно поставить задачу более серьезно – “догенерировать дочерний класс по заданному абстрактному”, хотя полноценная реализация такой задачи опять же потребует более интеллектуального (например [7]) обработчика исходного кода.

Заключение

Мы рассмотрели назначение, некоторые варианты использования и написали пример генератора кода для VS.NET. Цель статьи достигнута. Комментарии, вопросы, замечания и дополнения с удовольствием принимаются на форуме RSDN.

Ссылки

  1. Writing a custom tool to generate code for Visual Studio .NET
  2. GotDotNet User Sample: BaseCodeGeneratorWithSite
  3. SourceForge: dotnetopensrc/XGoF/CustomTool
  4. PowerToys for Visual Studio .NET 2003
  5. MSDN Magazine 10/2002: Top Ten Cool Features of Visual Studio .NET
  6. Шаблон для создания простых генераторов: vsnetgen.zip
  7. RSDN R# - препроцессор/компилятор


Эта статья опубликована в журнале RSDN Magazine #1-2004. Информацию о журнале можно найти здесь
    Сообщений 6    Оценка 637        Оценить