Опыт написания рефакторинга

в 19:33, , рубрики: .net, plugin, resharper 8, Программирование, метки: , , ,

Недавно я столкнулся с проблемой коллизий имен из разных пространств имен. В C# есть возможность ввести синонимы для пространств имен, т.е. вместо использования полного имени класса ввести префикс, с помощью которого можно обращаться к данному пространству имен.

Я не нашел простого пути, как с помощью Visual Studio и ReSharperа ввести синоним в уже написанном классе. В связи с чем я решил реализовать свое дополнение к ReSharper которое позволило бы решить эту проблему. В этой статье я бы хотел рассказать о подводных камнях, с которыми пришлось столкнуться, реализуя, на первый взгляд, такой простой рефакторинг. (исходный код и реализация в конце статьи)

Итак. Я хотел получить код, который поможет сделать что-то такое:

using b.b2;
class Example
{
    MyTest test;
} 

using x = b.b2;
class Example
{
    x.MyTest test;
} 

К моей радости, объектная модель ReSharper-a позволила легко получить для каждого типа его FQN(fully qualified name), т.е. в примере мы точно можем сказать, что класс MyTest находится в пространстве имен b.b2 и его FQN b.b2.MyTest

Первая формулировка алгоритма может выглядеть следующим образом:
Если мы хотим ввести псевдоним x для пространства имен b.b2, то следует для всех используемых типов в части кода, на который распространяется данный using: если использование типа лежит в пространстве b.b2 и его запись в коде не использует FQN, то необходимо добавить префикс x.

Возникает проблема — мы забыли про Extension методы. Extension методы могут вызваны только в том случае, если в файл явно импортировано пространство имен. Когда мы заменяем прямой импорт на синоним, то компилятор не может узнать, какой метод ему нужно вызывать. Проблема легко решается — extension методы из классов, которые находятся в пространстве имен b.b2. необходимо вызывать как члены статического класса, в котором они лежат.

было будет надо
using b.b2;
 
class Example
{
    MyTest test;
 
    Example()
    {
        test.Ext();
    }
} 

using x = b.b2;
 
class Example
{
    x.MyTest test;
 
    Example()
    {
      test.Ext();// Ext не найден
    }
} 

using x = b.b2;
 
class Example
{
    x.MyTest test;
 
    Example()
    {
        x.Extension.Ext(test);
    }
} 

Далее идут грабли, на которые я наткнулся, пытаясь запустить рефакторинг на тестовых файлах. Например, нельзя просто сравнивать на то, используется FQN или нет

было будет надо
using r = b.b2;
using b.b2;
 
class Example
{
    r.MyTest test;

}

using r = b.b2;
using x = b.b2;
 
class Example
{
  x.r.MyTest test; // r лишнее

} 

using r = b.b2;
using b.b2;
 
class Example
{
    r.MyTest test;
} 

Такая ошибка возникнет, т.к. FQN для MyTest это b.b2.MyTest. Наш рефакторинг, зная эту информацию, добавляет суффикс. Эту ошибку можно исправить, если вместо операции добавления префикса использовать полное замещение использование типа на x.[ShortTypeName].

Конфликты имен

Отдельно стоит проблема, когда значения x будет конфликтовать с уже определенным чем-либо

using b.b2;
 
class x
{
  class MyTest{}
}
 
class Example
{
    MyTest test;
} 

using x = b.b2;
 
class x
{
  class MyTest{}
}
 
class Example
{
  x.MyTest test; // Error namespace contains a definition conflicting with alias 'x'
} 

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

query-expression

Остается еще одна проблема. Что если пользователь захочет ввести синоним для пространства имен System.Linq (и ему подобных). В случае, если используются Extension методы, то наш алгоритм справится замечательно. Но если используется query-expression то ничего хорошего не выйдет

было будет
using System.Linq;
 ...
var query =
from c in svcContext.ContactSet
join a in svcContext.AccountSet
on c.ContactId equals a.PrimaryContactId.Id
where a.Name.Contains("Contoso")
where c.LastName.Contains("Smith")
select new
{
    account_name = a.Name,
    contact_name = c.LastName
}; 

using aaa = System.Linq;

// Error	Could not find an implementation of the query pattern for
// source type 'string[]'.
// 'Select' not found.  Are you missing a reference to 'System.Core.dll'
// or a using directive for 'System.Linq'?
var query = // 
from c in svcContext.ContactSet
join a in svcContext.AccountSet
on c.ContactId equals a.PrimaryContactId.Id
where a.Name.Contains("Contoso")
where c.LastName.Contains("Smith")
select new
{
    account_name = a.Name,
    contact_name = c.LastName
}; 

Правильный рефакторинг должен раскрыть query expression в цепочку вызовов статических методов (что, собственно и делает компилятор) и получить примерно такой результат.

var query =
aaa.Enumerable.Select(
aaa.Enumerable.Where(
aaa.Enumerable.Where(
    aaa.Enumerable.Join(
        svcContext.ContactSet,
        svcContext.AccountSet,
        c => c.ContactId,
        a => a.PrimaryContactId.Id,
        (c, a) => new { c, a }),
    @t => @t.a.Name.Contains("Contoso")),
@t => @t.c.LastName.Contains("Smith")),

@t => new { account_name = @t.a.Name, contact_name = @t.c.LastName }); 

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

Несколько слов о реализации

В сети достаточно немного информации о том, как писать плагины для решарпера. Основной источник информации — декомпилятор. Немногим помогает SDK, в котором имеется несколько примеров написания расширений (но на рефакторинг — только один).

Исходные коды доступны на GitHub, но я крайне не рекомендую использовать их для изучения внутренней кухни ReSharper-а.

Результат меня устроил. Надеюсь кому-нибудь пригодиться.
resharper-plugins.jetbrains.com/packages/IntroduceNsAlias/
github.com/ulex/IntroduceNsAlias

Дополнительная информация для плагинописателей ReSharper-a:
• декомпилятор
confluence.jetbrains.com/display/NETCOM/ReSharper+Plugin+Development
tv.jetbrains.net/videocontent/getting-started-with-resharper-sdk

Автор: aulitin

Источник

Поделиться

* - обязательные к заполнению поля