Code Issue - Nemerle way
От: artelk  
Дата: 17.02.12 10:49
Оценка: 165 (10) +1
После прочтения вот этой статьи на хабре возникло желание попробовать сделать что-нибудь подобное на 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}
}


Проверка:


Исходники тут.
Просьба попинать.
code issue roslyn nemerle
Re: Code Issue - Nemerle way
От: hardcase Пират http://nemerle.org
Дата: 17.02.12 11:00
Оценка: +1
Здравствуйте, artelk, Вы писали:

A>После прочтения вот этой статьи на хабре возникло желание попробовать сделать что-нибудь подобное на Nemerle.


На хабре показана скорее точка входа для анализаторов. Проблема этих штук в масштабировании. Представь, что мы забили 100 паттернов (Nemerle, C# не важно — все выглядит хреново) — и эти 100 посетителей будут 100 раз утюжить один и тот же AST. Очевидно что необходим DSL для записи таких поисковиков, который не просто позволит удобно записывать эти issues, но еще и позволит скомпилировать их вместе в один автомат дабы обход дерева выполнялся ровно один раз. DSL я бы сделал на базе сниппетов: показал пример "плохого кода" и система включила его в свою базу.
/* иЗвиНите зА неРовнЫй поЧерК */
Re[2]: Code Issue - Nemerle way
От: artelk  
Дата: 17.02.12 11:18
Оценка:
Здравствуйте, hardcase, Вы писали:

H>На хабре показана скорее точка входа для анализаторов. Проблема этих штук в масштабировании. Представь, что мы забили 100 паттернов (Nemerle, C# не важно — все выглядит хреново) — и эти 100 посетителей будут 100 раз утюжить один и тот же AST.


Теоретически, это можно сделать деталью реализации метода HintIssues — чтобы он при вызове как-то запоминал переданные body и getIssues и вызывал один единственный обход выражения с применением всех getIssues, подписанных на него.
Re[2]: Code Issue - Nemerle way
От: catbert  
Дата: 17.02.12 11:28
Оценка:
Здравствуйте, hardcase, Вы писали:

H>Очевидно что необходим DSL для записи таких поисковиков, который не просто позволит удобно записывать эти issues, но еще и позволит скомпилировать их вместе в один автомат дабы обход дерева выполнялся ровно один раз. DSL я бы сделал на базе сниппетов: показал пример "плохого кода" и система включила его в свою базу.


И прикрепить это к парсеру C# и зарабатывать миллионы
Re: Code Issue - Nemerle way
От: catbert  
Дата: 17.02.12 11:34
Оценка:
Здравствуйте, artelk, Вы писали:

A>Исходники тут.

A>Просьба попинать.

Маленькая стилистическая ремарка: в хинте желательно писать имя макроса, который этот хинт выдает, чтобы пользователь не мучился сомнениями

А так, даже попинать нечего.

Я сам как-то хотел написать такую штуку, но уперся в то, про что Хардкейс говорил
Автор: hardcase
Дата: 17.02.12
.
Re: Code Issue - Nemerle way
От: VladD2 Российская Империя www.nemerle.org
Дата: 17.02.12 15:55
Оценка: +1
Здравствуйте, artelk, Вы писали:

A>После прочтения вот этой статьи на хабре возникло желание попробовать сделать что-нибудь подобное на Nemerle. Там делается две вещи: детектирование проблем в коде и исправления в нем. Можно ли сделать второе на Nemerle — не вкурсе. Расскажу как я сделал первую часть.


A>Для поиска и показа issues были сделаны два макроса. Первый — это макрос уровня выражения, который, будучи примененным к блоку кода, ищет issues и показывает их в VS. Второй — это макроатрибут уровня сборки, который оборачивает тела всех методов проекта в вызов первого макроса.


Предлагаю чтобы избавиться от этих оборачивающих макросов ввести событие которое будет вызываться после окончания типизации метода.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re: Code Issue - Nemerle way
От: VladD2 Российская Империя www.nemerle.org
Дата: 17.02.12 16:03
Оценка:
Здравствуйте, artelk, Вы писали:

A>Используя HintIssues, реализация макроса уровня выражения выглядит так:

A>
A>                    match(node)
A>                    {
A>                        | <[ $o.Count() > 0 ]>
A>                        | <[ 0 < $o.Count() ]>
A>                        | <[ $o.Count() >= 1 ]>
A>                        | <[ 1 <= $o.Count() ]> =>
A>                            when(typer.TypeExpr(o).FixedType().TryRequire(enumType))
A>                                yield $"Change '$node' to '$o.Any()' to avoid possible enumeration of entire sequence.";
A>


Выделенное жирным лишнее. Тут лучше использовать свойство TypedObject. У подобных выражений в нем должен лежать TExpr. У него можно ивзять тип.

А вот повторная типизация — это опасный ход. Она может и не увенчаться успехом.

Так что данный код лучше переписать так:
 match(node)
 {
     | <[ $o.Count() > 0 ]>
     | <[ 0 < $o.Count() ]>
     | <[ $o.Count() >= 1 ]>
     | <[ 1 <= $o.Count() ]> =>
         when((node.TypedObject :> TExpr).Type.TryRequire(enumType))
             yield $"Change '$node' to '$o.Any()' to avoid possible enumeration of entire

Будет и проще, и надежнее.

A>Просьба попинать.


Предлагаю довести до ума и преобразовать это дело в статью. А мы ее разместим на РСДН (если будет перевод и на nemerle.org).
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[2]: Code Issue - Nemerle way
От: VladD2 Российская Империя www.nemerle.org
Дата: 17.02.12 16:07
Оценка: +1
Здравствуйте, VladD2, Вы писали:

VD>Так что данный код лучше переписать так:

VD>
VD> match(node)
VD> {
VD>     | <[ $o.Count() > 0 ]>
VD>     | <[ 0 < $o.Count() ]>
VD>     | <[ $o.Count() >= 1 ]>
VD>     | <[ 1 <= $o.Count() ]> =>
VD>         when((node.TypedObject :> TExpr).Type.TryRequire(enumType))
VD>             yield $"Change '$node' to '$o.Any()' to avoid possible enumeration of entire 
VD>

VD>Будет и проще, и надежнее.

Подумал еще... Правильно было бы анализировать TypedObject у подвыражения отвечающего за вызов функции (например, o.Count()). Там должен быть TExpr.Call функцией которого является статический метод Count. Это более точное решение (твое может дать не верный вывод, если Count не линковская функция).
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re: Code Issue - Nemerle way
От: VladD2 Российская Империя www.nemerle.org
Дата: 17.02.12 16:20
Оценка: 12 (1)
Здравствуйте, artelk, Вы писали:

A>Там делается две вещи: детектирование проблем в коде и исправления в нем. Можно ли сделать второе на Nemerle — не вкурсе.


Для 2010 студии можно. Причем практически так же как это описано в статье на которую ты дал ссылку.

Код исью в Рослинских шаблонах наверняка является простенькой оболочкой к АПИ студии. Можно сделать похожую оболочку для немерла.

В 2010 студии можно делать смарт-теги очень легко.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[2]: Code Issue - Nemerle way
От: VladD2 Российская Империя www.nemerle.org
Дата: 17.02.12 16:35
Оценка:
Здравствуйте, hardcase, Вы писали:

H>На хабре показана скорее точка входа для анализаторов. Проблема этих штук в масштабировании. Представь, что мы забили 100 паттернов (Nemerle, C# не важно — все выглядит хреново) — и эти 100 посетителей будут 100 раз утюжить один и тот же AST. Очевидно что необходим DSL для записи таких поисковиков, который не просто позволит удобно записывать эти issues, но еще и позволит скомпилировать их вместе в один автомат дабы обход дерева выполнялся ровно один раз. DSL я бы сделал на базе сниппетов: показал пример "плохого кода" и система включила его в свою базу.


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

Ну, и не так уж трудно, если что, сделать общий "обходчик" и в него уже пихать конкретные проверки как в плагины.

По сути — это некий аналог T2-шага типизации, только без преобразований.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[2]: Code Issue - Nemerle way
От: artelk  
Дата: 18.02.12 08:49
Оценка:
Здравствуйте, VladD2, Вы писали:

VD>Здравствуйте, artelk, Вы писали:


A>>Используя HintIssues, реализация макроса уровня выражения выглядит так:

A>>
A>>                    match(node)
A>>                    {
A>>                        | <[ $o.Count() > 0 ]>
A>>                        | <[ 0 < $o.Count() ]>
A>>                        | <[ $o.Count() >= 1 ]>
A>>                        | <[ 1 <= $o.Count() ]> =>
A>>                            when(typer.TypeExpr(o).FixedType().TryRequire(enumType))
A>>                                yield $"Change '$node' to '$o.Any()' to avoid possible enumeration of entire sequence.";
A>>


VD>Выделенное жирным лишнее. Тут лучше использовать свойство TypedObject. У подобных выражений в нем должен лежать TExpr. У него можно ивзять тип.

VD>А вот повторная типизация — это опасный ход. Она может и не увенчаться успехом.
Эх, Влад, появлялся бы ты как джин по кнопке F1. Может можно как-нибудь на макросах замутить твоего клона?
Я подозревал, что неспроста FixedType тут является методом, а не свойством...

Обратившись к свойству PExpr.TypedObject. Оно заполняется компилятором в процессе типизации. Это свойство имеет тип object, так как в него могут помещаться объекты разного типа (например, типизированное представление параметра или типизированное выражение), но в подавляющем большинстве случаев в него помещается ссылка на соответствующий TExpr (т.е. на соответствующее типизированное AST). Учтите, что значение свойства TypedObject доступно только после того, как PExpr (напрямую или в составе другого PExpr) будет типизирован методом Typer.TypeExpr().

Думаю, правильно было бы поменять тип TypedObject с object на какой-нибудь вариант, включающий все типы, которые может иметь это свойство.

VD>Предлагаю довести до ума и преобразовать это дело в статью. А мы ее разместим на РСДН (если будет перевод и на nemerle.org).

Интересно, попробую.
Re[3]: Code Issue - Nemerle way
От: hardcase Пират http://nemerle.org
Дата: 18.02.12 08:57
Оценка:
Здравствуйте, VladD2, Вы писали:

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


Один обход безусловно быстр. Вопрос в количестве паттернов некачественного кода, которые мы ищем. Чем больше их — тем более вероятны тормоза.
/* иЗвиНите зА неРовнЫй поЧерК */
Re[2]: Code Issue - Nemerle way
От: artelk  
Дата: 18.02.12 09:14
Оценка:
Здравствуйте, VladD2, Вы писали:

VD>Предлагаю чтобы избавиться от этих оборачивающих макросов ввести событие которое будет вызываться после окончания типизации метода.


Если делать все по-честному, нужно заводить отдельную фазу типизации — когда все, что возможно, типизированно. "Все, что возможно", а не "все" — поскольку в исходниках могут быть ошибки, а работу макроса хотелось бы увидеть и в этом случае.

Хотелось бы, чтобы сигнатура макросов этой фазы была похожа на сигнатуру getIssues:

[IssueMacro]//или что-нибудь такое
macro CheckCountAny(typer : Typer, expr : PExpr): IEnumerable[string * LazyValue[PExpr]]
{
  //...
}


Макросу подается на вход все типизированные выражения (т.е. обход внешний, осуществляется компилятором). Если какое-то выражение недотипизировано — его пропускать (CheckCountAny не вызывать), но для его подвыражений, если они полностью типизированы — вызывать. На выходе — список кортежей из предупреждений и выражений, которыми нужно заменить исходное, если пользователь захочет.

Еще хотелось бы, чтобы такие issue-macros, применялись бы по факту добавления macro reference — чтобы не нужно было открывать пространство имен и как-то явно применять эти макросы.

Еще хотелось бы иметь плагины к студии, которые при компиляции неявно добавляют макросборки (или сделать чтобы интеграция искала установленные плагины определенных типов и подмешивала их макросборки в компилируемые проекты).

PS Хотя компилятор под эти нужды переделывать, возможно, слишком. Нужно в интергации что-то прикрутить, чтобы этот функционал покрыть.
Re[3]: Code Issue - Nemerle way
От: VladD2 Российская Империя www.nemerle.org
Дата: 18.02.12 12:13
Оценка:
Здравствуйте, artelk, Вы писали:

A>

A>Обратившись к свойству PExpr.TypedObject. Оно заполняется компилятором в процессе типизации. Это свойство имеет тип object, так как в него могут помещаться объекты разного типа (например, типизированное представление параметра или типизированное выражение), но в подавляющем большинстве случаев в него помещается ссылка на соответствующий TExpr (т.е. на соответствующее типизированное AST). Учтите, что значение свойства TypedObject доступно только после того, как PExpr (напрямую или в составе другого PExpr) будет типизирован методом Typer.TypeExpr().

A>Думаю, правильно было бы поменять тип TypedObject с object на какой-нибудь вариант, включающий все типы, которые может иметь это свойство.

Там сейчас уже не обжект, а TypedBase. Это базовый класс для всего тпизированного АСТ.

А изучать конкретное АСТ удобнее всего под отладчиком. Влепил в макросе assert2(false). Запустил компиляцию. Вошел в режим отладки и смотри что находится в TypedObject у разных веток.

Только надо понимать, что в TypedObject информация получается в процессе типизации. Так что использовать это совйство можно только после того как выражение было типизировано. В твоем случае это делается с помощью метода typer.TransformWhenAllTypesWouldBeInfered().

Кстати, возможно что более надежнее и проще было бы сразу разбирать TExpr.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[3]: Code Issue - Nemerle way
От: VladD2 Российская Империя www.nemerle.org
Дата: 18.02.12 12:28
Оценка:
Здравствуйте, artelk, Вы писали:

A>Если делать все по-честному, нужно заводить отдельную фазу типизации — когда все, что возможно, типизированно.


Фаза такая есть. Нет точки входа между не и окончанием типизации. Вот я и предлагаю сделать событие.

A>"Все, что возможно", а не "все" — поскольку в исходниках могут быть ошибки, а работу макроса хотелось бы увидеть и в этом случае.


Думаю, что нет смысла делать дополнительные проверки в случае наличия ошибок.

A>Хотелось бы, чтобы сигнатура макросов этой фазы была похожа на сигнатуру getIssues:


A>
A>[IssueMacro]//или что-нибудь такое
A>macro CheckCountAny(typer : Typer, expr : PExpr): IEnumerable[string * LazyValue[PExpr]]
A>{
A>  //...
A>}
A>


Зачем тут тайпер? В прочем, можно и его передать. Тут намного нужнее полученный TExpr. По нему "бежать" надежнее.

A>Еще хотелось бы, чтобы такие issue-macros, применялись бы по факту добавления macro reference — чтобы не нужно было открывать пространство имен и как-то явно применять эти макросы.


В принципе решение с глобальным атрибутом было не плохое. Но можно придумать некий атрибут которым можно пометить сборку. Если он есть создавать его экземпляр и передавать ему Manager. Тогда в коде атрибута можно будет подписаться на события Manager-а. В том числе и на то которое даст возможность посещать тела методов после окончания типизации.

A>Еще хотелось бы иметь плагины к студии, которые при компиляции неявно добавляют макросборки (или сделать чтобы интеграция искала установленные плагины определенных типов и подмешивала их макросборки в компилируемые проекты).


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

A>PS Хотя компилятор под эти нужды переделывать, возможно, слишком. Нужно в интергации что-то прикрутить, чтобы этот функционал покрыть.


Возможно кроме Hint стоило бы добавить ее один тип сообщения компилятора — Issue. К нему сделать возможность добавления ссылки на функцию рефакторинга. Ну, а в интеграции сделать расширенную поддержку этого дела.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[3]: Code Issue - Nemerle way
От: artelk  
Дата: 21.02.12 12:40
Оценка:
Здравствуйте, VladD2, Вы писали:

VD>Подумал еще... Правильно было бы анализировать TypedObject у подвыражения отвечающего за вызов функции (например, o.Count()). Там должен быть TExpr.Call функцией которого является статический метод Count. Это более точное решение (твое может дать не верный вывод, если Count не линковская функция).


Возник вопрос:
match(node)
{
    | <[ $o > 0 ]>
    | <[ 0 < $o ]>
    | <[ $o >= 1 ]>
    | <[ 1 <= $o ]> =>
        match(o.TypedObject)
        {
            | countCall is TExpr.Call when /*ЧТО ТУТ ДОЛЖНО БЫТЬ?*/ =>
                yield $"Change '$node' to '$o.Any()' to avoid possible enumeration of entire..."
            | _ => ()
        }
    | _ => ()
};

Нужно как-то проверить, что это вызов Enumerable.Count[T](this IEnumerable[T] collection). Сам не нашел.
Re[4]: Code Issue - Nemerle way
От: VladD2 Российская Империя www.nemerle.org
Дата: 21.02.12 17:30
Оценка:
Здравствуйте, artelk, Вы писали:

A>Возник вопрос:

A>
A>match(node)
A>{
A>    | <[ $o > 0 ]>
A>    | <[ 0 < $o ]>
A>    | <[ $o >= 1 ]>
A>    | <[ 1 <= $o ]> =>
A>        match(o.TypedObject)
A>        {
A>            | countCall is TExpr.Call when /*ЧТО ТУТ ДОЛЖНО БЫТЬ?*/ =>
A>                yield $"Change '$node' to '$o.Any()' to avoid possible enumeration of entire..."
A>            | _ => ()
A>        }
A>    | _ => ()
A>};
A>

A>Нужно как-то проверить, что это вызов Enumerable.Count[T](this IEnumerable[T] collection). Сам не нашел.

Код должен быть примерно таким:
    | <[ 1 <= $o ]> =>
        match(o.TypedObject)
        {
            | TExpr.Call(StaticRef(from, meth is IMethod, typeArgs)) when анализируем meth =>
              ...


В качестве примера работы с TExpr погляди реализацию конвертации выражений немерла в деревья выражений линк-а.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.