После прочтения
вот этой статьи на хабре возникло желание попробовать сделать что-нибудь подобное на Nemerle. Там делается две вещи: детектирование проблем в коде и исправления в нем. Можно ли сделать второе на Nemerle — не вкурсе. Расскажу как я сделал первую часть.
Для поиска и показа issues были сделаны два макроса. Первый — это макрос уровня выражения, который, будучи примененным к блоку кода, ищет issues и показывает их в VS. Второй — это макроатрибут уровня сборки, который оборачивает тела всех методов проекта в вызов первого макроса.
Для удобства были реализованы следующие вспомогательные методы:
public ForAllMethods(this typer : Typer, changeBoby : PExpr -> PExpr): void
{
def types = typer.Manager.NameTree.NamespaceTree.GetTypeBuilders(onlyTopDeclarations=false);
def bindingFlags = BindingFlags.DeclaredOnly | BindingFlags.Instance |
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
foreach (method is MethodBuilder in types.SelectMany(_.GetMembers(bindingFlags)))
method.Body = changeBoby(<[ $(method.Body) : $(method.ReturnType : typed) ]>) <- { Location = method.Body.Location };
}
public HintIssues(this typer : Typer, body : PExpr, getIssues: PExpr -> IEnumerable[string]) : PExpr
{
typer.TransformWhenAllTypesWouldBeInfered(
(pexpr, _) =>
{
ExprWalker().Walk(pexpr,info =>
{
match(info.Node)
{
| node is PExpr =>
foreach(issue in getIssues(node))
Message.Hint(node.Location, issue);
| _ => ()
}
});
pexpr
},
typer.TypeExpr(body), body);
}
Метод ForAllMethods занимается поиском всех методов во всех классах проекта и применяет к ним метод changeBoby. Он нужен для реализации нашего макроатрибута.
Метод HintIssues нужен, чтобы пройтись по всем подвыражениям body, вызвать для них функцию getIssues, которая собственно детектирует issues, и показать их.
Нужно, чтобы все подвыражения были типизированы к моменту работы этого метода, поэтому он откладывает свое выполнение на этот момент, используя typer.TransformWhenAllTypesWouldBeInfered.
Используя HintIssues, реализация макроса уровня выражения выглядит так:
macro CheckCountAny(body : PExpr)
syntax ("checkCountAny", body)
{
CheckCountAnyImpl.DoTransform(Macros.ImplicitCTX(), body)
}
module CheckCountAnyImpl
{
public DoTransform(typer : Typer, body : PExpr) : PExpr
{
Macros.DefineCTX(typer);
def enumType = <[ ttype: System.Collections.Generic.IEnumerable[_] ]>;
typer.HintIssues(body, node =>
comp enumerable
{
match(node)
{
| <[ $o.Count() > 0 ]>
| <[ 0 < $o.Count() ]>
| <[ $o.Count() >= 1 ]>
| <[ 1 <= $o.Count() ]> =>
when(typer.TypeExpr(o).FixedType().TryRequire(enumType))
yield $"Change '$node' to '$o.Any()' to avoid possible enumeration of entire sequence.";
| _ => ()
}
});
}
}
Макрос в действии:
Макрос, оборачивающий тела методов:
[MacroUsage(MacroPhase.WithTypedMembers, MacroTargets.Assembly)]
macro CheckCountAny()
{
CheckCountAnyImpl.DoTransform(Macros.ImplicitCTX())
}
module CheckCountAnyImpl
{
public DoTransform(typer : Typer) : void
{
Macros.DefineCTX(typer);
typer.ForAllMethods(body => <[ CheckCountAny($body) ]>);
}
}
Для его применения в проекте, где нужны соответствующие проверки, необходимо добавить ссылку на макросборку, открыть пространство имен с макросами и добавить [assembly: CheckCountAny]:
using CheckCountAnyMacro;
[assembly: CheckCountAny]
class A
{
public Count():int {42}
}
class B : IEnumerable[int]
{
public Count():int {42}
public GetEnumerator() : IEnumerator[int] {null}
}
Проверка:
Исходники
тут.
Просьба попинать.