Как я могу надежно определить тип переменной, объявленной с помощью var во время разработки?

Я работаю над установкой завершения (intellisense) для С# в emacs.

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

Выполнение этого требует, чтобы тип выполняемой вещи был известен. Если это строка, существует известный набор возможных методов и свойств; если это Int32, он имеет отдельный набор и т.д.

Используя семантический, пакет lexer/parser кода, доступный в emacs, я могу найти объявления переменных и их типы. Учитывая это, просто использовать рефлексию для получения методов и свойств в типе, а затем представить список вариантов для пользователя. (Хорошо, не так просто сделать в emacs, но используя возможность запускать процесс powershell внутри emacs, это становится намного проще. пользовательская сборка .NET, чтобы сделать отражение, загрузить его в powershell, а затем elisp, работающий в emacs, может отправлять команды на powershell и читать ответы через comint. В результате emacs может быстро получить результаты отражения.)

Проблема возникает, когда код использует var в объявлении завершенной вещи. Это означает, что тип явно не указан, и завершение не будет работать.

Как я могу достоверно определить используемый фактический тип, когда переменная объявлена ​​с ключевым словом var? Чтобы быть ясным, мне не нужно определять его во время выполнения. Я хочу определить его в "Время разработки".

Пока у меня есть эти идеи:

  • скомпилировать и вызвать:
    • извлечь выражение о декларации, например `var foo = "строковое значение";`
    • конкатенировать оператор `foo.GetType();`
    • динамически компилировать полученный С# фрагмент в новую сборку
    • загрузите сборку в новый AppDomain, запустите фрейм-сегмент и получите возвращаемый тип.
    • выгрузить и удалить сборку

    Я знаю, как все это делать. Но это звучит ужасно тяжело, для каждого запроса на завершение в редакторе.

    Я полагаю, что мне не нужен новый новый AppDomain каждый раз. Я мог бы повторно использовать один AppDomain для нескольких временных сборок и амортизировать затраты на его установку и разрывать его по нескольким запросам на завершение. Это более тонкая идея базовой идеи.

  • скомпилировать и проверить IL

    Просто скомпилируйте декларацию в модуль и затем проверите IL, чтобы определить фактический тип, который был выведен компилятором. Как это возможно? Что я буду использовать для изучения ИЛ?

Есть ли лучшие идеи? Комментарии? предложения?


РЕДАКТИРОВАТЬ - подумайте об этом дальше, компиляция и вызов не приемлемы, потому что вызов может иметь побочные эффекты. Поэтому первый вариант должен быть исключен.

Кроме того, я думаю, что не могу предположить наличие .NET 4.0.


ОБНОВЛЕНИЕ. Правильный ответ, не упомянутый выше, но осторожно отмеченный Эриком Липпертом, заключается в том, чтобы внедрить систему вывода полного типа верности. Это единственный способ надежно определить тип var во время разработки. Но это также непросто сделать. Поскольку у меня нет иллюзий, что я хочу попытаться построить такую ​​вещь, я взял ярлык варианта 2 - извлеките соответствующий код декларации и скомпилируйте его, а затем проверите полученный IL.

Это действительно работает для справедливого подмножества сценариев завершения.

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

var x = "hello there"; 
x.?

По завершении понимает, что x является строкой и предоставляет соответствующие параметры. Он делает это, генерируя и затем компилируя следующий исходный код:

namespace N1 {
  static class dmriiann5he { // randomly-generated class name
    static void M1 () {
       var x = "hello there"; 
    }
  }
}

... и затем проверяя IL с простым отражением.

Это также работает:

var x = new XmlDocument();
x.? 

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

Это тоже работает:

var x = "hello"; 
var y = x.ToCharArray();    
var z = y.?

Это просто означает, что инспекция IL должна найти тип третьей локальной переменной вместо первой.

И это:

var foo = "Tra la la";
var fred = new System.Collections.Generic.List<String>
    {
        foo,
        foo.Length.ToString()
    };
var z = fred.Count;
var x = z.?

..., что на один уровень глубже, чем в предыдущем примере.

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

var foo = this.InstanceMethod();
foo.?

Синтаксис Nor LINQ.

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

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


Еще одно обновление - завершение работы над vars, которое зависит от членов экземпляра, теперь работает.

То, что я сделал, было опросом типа (через семантику), а затем генерировать синтетические элементы для всех существующих членов. Для буфера С#:

public class CsharpCompletion
{
    private static int PrivateStaticField1 = 17;

    string InstanceMethod1(int index)
    {
        ...lots of code here...
        return result;
    }

    public void Run(int count)
    {
        var foo = "this is a string";
        var fred = new System.Collections.Generic.List<String>
        {
            foo,
            foo.Length.ToString()
        };
        var z = fred.Count;
        var mmm = count + z + CsharpCompletion.PrivateStaticField1;
        var nnn = this.InstanceMethod1(mmm);
        var fff = nnn.?

        ...more code here...

... сгенерированный код, который компилируется, так что я могу узнать из выходного IL тип локального var nnn, выглядит так:

namespace Nsbwhi0rdami {
  class CsharpCompletion {
    private static int PrivateStaticField1 = default(int);
    string InstanceMethod1(int index) { return default(string); }

    void M0zpstti30f4 (int count) {
       var foo = "this is a string";
       var fred = new System.Collections.Generic.List<String> { foo, foo.Length.ToString() };
       var z = fred.Count;
       var mmm = count + z + CsharpCompletion.PrivateStaticField1;
       var nnn = this.InstanceMethod1(mmm);
      }
  }
}

Все члены экземпляра и статического типа доступны в коде скелета. Он успешно компилируется. В этот момент определение типа локального var выполняется с помощью Reflection.

Что делает это возможным:

  • возможность запуска powershell в emacs
  • Компилятор С# очень быстрый. На моей машине для компиляции сборки в памяти требуется около 0,5 с. Не достаточно быстро для анализа между нажатиями клавиш, но достаточно быстро, чтобы поддерживать составление списков завершения по требованию.

Я еще не изучал LINQ.
Это будет гораздо более серьезной проблемой, потому что семантический lexer/parser emacs имеет для С#, не "делает" LINQ.

Ответ 1

Я могу описать для вас, как мы делаем это эффективно в "реальной" С# IDE.

Первое, что мы делаем, это запустить проход, в котором анализируется только материал "верхнего уровня" в исходном коде. Мы пропускаем все тела метода. Это позволяет нам быстро создавать базу данных о том, какое пространство имен, типы и методы (и конструкторы и т.д.) Находятся в исходном коде программы. Анализ каждой отдельной строки кода в каждом теле метода займет слишком много времени, если вы пытаетесь сделать это между нажатиями клавиш.

Когда среда IDE должна выработать тип конкретного выражения внутри тела метода - скажем, вы набрали "foo" . и нам нужно выяснить, каковы члены foo - мы делаем то же самое; мы пропустим столько работы, насколько это возможно.

Начнем с прохода, который анализирует только локальные объявления переменных внутри этого метода. Когда мы запускаем этот проход, мы делаем сопоставление от пары "scope" и "name" к "определителю типа". "Определитель типа" - это объект, который представляет собой понятие "Я могу определить тип этого локального, если мне нужно". Разработка типа локального может быть дорогостоящим, поэтому мы хотим отложить эту работу, если нам нужно.

Теперь у нас есть лениво-построенная база данных, которая может рассказать нам тип каждого локального. Итак, вернемся к этому "foo" . - мы выясняем, в каком выражении находится соответствующее выражение, а затем запускаем семантический анализатор против этого утверждения. Например, предположим, что у вас есть тело метода:

String x = "hello";
var y = x.ToCharArray();
var z = from foo in y where foo.

и теперь нам нужно решить, что foo имеет тип char. Мы создаем базу данных, в которой есть все метаданные, методы расширения, типы исходного кода и т.д. Мы создаем базу данных, которая имеет определители типа для x, y и z. Мы анализируем утверждение, содержащее интересное выражение. Начнем с синтаксического преобразования его в

var z = y.Where(foo=>foo.

Чтобы выработать тип foo, мы должны сначала знать тип y. Итак, в этот момент мы задаем тип определителя "какой тип y"? Затем он запускает анализатор выражений, который анализирует x.ToCharArray() и спрашивает "какой тип x"? У нас есть определитель типа, который говорит: "Мне нужно искать" String "в текущем контексте". В текущем типе нет типа String, поэтому мы смотрим в пространстве имен. Это также не так, поэтому мы смотрим в директивах по использованию и обнаруживаем, что существует "использование системы", и эта система имеет тип String. ОК, так что тип x.

Затем мы запрашиваем метаданные System.String для типа ToCharArray, и он говорит, что это System.Char []. Супер. Итак, у нас есть тип для y.

Теперь мы спрашиваем: "У System.Char [] есть метод Where?" Нет. Поэтому мы смотрим в директивах по использованию; мы уже предварительно вычислили базу данных, содержащую все метаданные для методов расширения, которые можно было бы использовать.

Теперь мы говорим "ОК, существует восемнадцать десятков методов расширения, называемых Where in scope, любой из них имеет первый формальный параметр, тип которого совместим с System.Char []?" Поэтому мы начинаем раунд тестирования конвертируемости. Тем не менее, методы расширения Where являются общими, что означает, что мы должны делать вывод типа.

Я написал специальный механизм ввода infererencing, который может обрабатывать неполные выводы из первого аргумента методу расширения. Мы запускаем тип inferrer и обнаруживаем, что существует метод Where, который принимает IEnumerable<T>, и что мы можем сделать вывод от System.Char [] до IEnumerable<System.Char>, поэтому T является System.Char.

Подпись этого метода Where<T>(this IEnumerable<T> items, Func<T, bool> predicate), и мы знаем, что T является System.Char. Также мы знаем, что первый аргумент в круглых скобках для метода расширения - лямбда. Таким образом, мы запускаем указатель типа выражения лямбда, который говорит, что "формальный параметр foo считается System.Char", используйте этот факт при анализе остальной части лямбда.

Теперь у нас есть вся информация, необходимая для анализа тела лямбды, которая является "foo" . Мы смотрим на тип foo, мы обнаруживаем, что в соответствии с lambda binder это System.Char, и мы закончили; мы отображаем информацию о типе для System.Char.

И мы делаем все, кроме анализа "верхнего уровня" между нажатиями клавиш. Это настоящий хитроумный бит. На самом деле написать весь анализ не сложно; это делает его достаточно быстрым, чтобы вы могли сделать это при скорости набора текста, что является настоящим сложным битом.

Удачи!

Ответ 2

Я могу рассказать вам, как Delphi IDE работает с компилятором Delphi для выполнения intellisense (понимание кода - это то, что Delphi называет). Это не 100% применимо к С#, но это интересный подход, который заслуживает внимания.

Большинство семантического анализа в Delphi выполняется в самом парсере. Выражения набираются так, как они анализируются, за исключением ситуаций, когда это непросто - в этом случае синтаксический анализ с открытым исходным кодом используется для определения того, что предназначено, а затем это решение используется в анализе.

Синтаксический анализ - это в основном LL (2) рекурсивный спуск, за исключением выражений, которые анализируются с использованием приоритета оператора. Одна из отличительных особенностей Delphi заключается в том, что это однопроходный язык, поэтому конструкции должны быть объявлены перед использованием, поэтому для вывода этой информации не требуется прохождение верхнего уровня.

Эта комбинация функций означает, что синтаксический анализатор имеет примерно всю информацию, необходимую для понимания кода для любой точки, где он нужен. Способ работы: IDE информирует лексер компилятора о позиции курсора (точка, в которой требуется проницательность кода), а лексер превращает это в специальный токен (он называется токеном kibitz). Всякий раз, когда синтаксический анализатор встречает этот токен (который может быть где угодно), он знает, что это сигнал для отправки всей информации, которую он возвращает редактору. Он делает это, используя longjmp, потому что он написан на C; что он делает, он уведомляет конечного вызывающего лица о синтаксической конструкции (т.е. о грамматическом контексте), в которой находилась точка кибица, а также все символические таблицы, необходимые для этой точки. Так, например, если контекст находится в выражении, которое является аргументом метода, мы можем проверить перегрузку метода, посмотреть типы аргументов и фильтровать действительные символы только для тех, которые могут разрешить этот тип аргумента (это сокращается во множестве нерелевантных трещин в выпадающем списке). Если он находится в контексте вложенной области (например, после "." ), Синтаксический анализатор вернет ссылку на область действия, а среда IDE может перечислить все символы, найденные в этой области.

Другие вещи также выполняются; например, тела методов пропускаются, если токен кибица не лежит в их диапазоне - это делается оптимистично и откатывается, если он пропускает токен. Эквивалентные методы расширения - помощники классов в Delphi - имеют своего рода кеш версии, поэтому их поиск достаточно быстр. Но вывод общего типа Delphi намного слабее, чем С#.

Теперь, к конкретному вопросу: вывод типов переменных, объявленных с помощью var, эквивалентен тому, как Pascal указывает тип констант. Он исходит из типа выражения инициализации. Эти типы построены снизу вверх. Если x имеет тип Integer, а y имеет тип Double, то x + y будет иметь тип Double, потому что это правила языка; и т.д. Вы следуете этим правилам, пока у вас не будет типа для полного выражения с правой стороны, и того типа, который вы используете для символа слева.

Ответ 3

Если вы не хотите писать собственный синтаксический анализатор для создания абстрактного синтаксического дерева, вы можете посмотреть на использование парсеров из SharpDevelop или MonoDevelop, оба из которых являются открытыми.

Ответ 4

Системы Intellisense обычно представляют собой код с использованием абстрактного дерева синтаксиса, который позволяет им разрешать возвращаемый тип функции, назначаемой переменной "var", более или менее так же, как и компилятор. Если вы используете VS Intellisense, вы можете заметить, что он не даст вам тип var до тех пор, пока вы не закончите вводить допустимое (разрешимое) присваивание. Если выражение все еще неоднозначно (например, он не может полностью вывести общие аргументы для выражения), тип var не будет разрешен. Это может быть довольно сложный процесс, так как вам может понадобиться достаточно глубоко пройтись по дереву, чтобы разрешить этот тип. Например:

var items = myList.OfType<Foo>().Select(foo => foo.Bar);

Тип возврата IEnumerable<Bar>, но для этого требуется знание:

  • myList имеет тип, который реализует IEnumerable.
  • Существует метод расширения OfType<T>, который применяется к IEnumerable.
  • Результирующее значение IEnumerable<Foo>, и к нему применяется метод расширения Select.
  • В выражении лямбда foo => foo.Bar имеется параметр foo типа Foo. Это вызывается использованием Select, который принимает Func<TIn,TOut>, и поскольку TIn известен (Foo), можно определить тип foo.
  • Тип Foo имеет свойство Bar, которое имеет тип Bar. Мы знаем, что Select возвращает IEnumerable<TOut>, и TOUT может быть выведен из результата выражения лямбда, поэтому результирующий тип элементов должен быть IEnumerable<Bar>.

Ответ 5

Поскольку вы нацеливаете Emacs, лучше всего начать с набора CEDET. Все детали, которые Эрик Липперт уже включен в анализатор кода в CEDET/Semantic tool для С++. Существует также анализатор С# (который, вероятно, нуждается в небольшой TLC), поэтому единственные недостающие части связаны с настройкой необходимых частей для С#.

Основные типы поведения определяются в основных алгоритмах, которые зависят от перегружаемых функций, определенных для каждого языка. Успех механизма завершения зависит от того, сколько настроек было выполнено. С С++ в качестве руководства получение поддержки, аналогичной С++, не должно быть слишком плохим.

Ответ Дэниела предполагает использование MonoDevelop для синтаксического анализа и анализа. Это может быть альтернативный механизм вместо существующего синтаксического анализатора С# или его можно использовать для увеличения существующего синтаксического анализатора.

Ответ 6

Это тяжелая проблема. В основном вам нужно смоделировать спецификацию языка/компилятор с помощью большей части лексинга/разбора/проверки типов и построить внутреннюю модель исходного кода, которую вы затем можете запросить. Эрик подробно описывает его для С#. Вы всегда можете скачать исходный код компилятора F # (часть F # CTP) и взглянуть на service.fsi, чтобы увидеть интерфейс, открытый из компилятора F #, который сервисы языка F # потребляют для предоставления intellisense, всплывающих подсказок для выводимых типов и т.д.. Это дает ощущение возможного "интерфейса", если у вас уже есть компилятор, доступный как API для вызова.

Другой маршрут состоит в том, чтобы повторно использовать компиляторы как есть, как вы описываете, а затем использовать отражение или посмотреть на сгенерированный код. Это проблематично с точки зрения того, что вам нужны "полные программы" для получения компиляции из компилятора, тогда как при редактировании исходного кода в редакторе часто есть только "частичные программы", которые еще не анализируются, не все еще реализованы методы и т.д.

Короче говоря, я думаю, что версия с низким бюджетом очень тяжелая, и "настоящая" версия очень и очень преуспевает. (Там, где "трудно" измеряет "усилия" и "технические трудности".)

Ответ 7

NRefactory сделает это за вас.

Ответ 8

Для решения "1" у вас есть новое средство в .NET 4, чтобы сделать это быстро и легко. Поэтому, если вы можете преобразовать свою программу в .NET 4, это будет ваш лучший выбор.