Я работаю над средством завершения (intellisense) для C # в emacs.
Идея заключается в том, что если пользователь набирает фрагмент, а затем запрашивает завершение с помощью определенной комбинации клавиш, средство завершения будет использовать отражение .NET для определения возможных завершений.
Для этого необходимо знать тип завершаемой вещи. Если это строка, существует известный набор возможных методов и свойств; если это Int32, у него отдельный набор и так далее.
Используя семантику, пакет лексера / анализатора кода, доступный в emacs, я могу найти объявления переменных и их типы. Учитывая это, легко использовать отражение, чтобы получить методы и свойства типа, а затем представить список параметров пользователю. (Хорошо, это не совсем просто сделать в emacs, но, используя возможность запускать процесс powershell внутри emacs , это становится намного проще. Я пишу настраиваемую сборку .NET для отражения, загружаю ее в powershell, а затем elisp работает в emacs может отправлять команды в powershell и читать ответы через comint. В результате emacs может быстро получить результаты отражения.)
Проблема возникает, когда код использует var
в объявлении то, что выполняется. Это означает, что тип явно не указан, и завершение не будет работать.
Как я могу надежно определить фактический используемый тип, если переменная объявлена с var
ключевым словом? Чтобы быть ясным, мне не нужно определять его во время выполнения. Я хочу определить это во «Время разработки».
Пока у меня есть эти идеи:
- скомпилировать и вызвать:
- извлеките оператор объявления, например, `var foo =" строковое значение ";`
- объединить оператор `foo.GetType ();`
- динамически скомпилировать полученный фрагмент C # в новую сборку
- загрузите сборку в новый AppDomain, запустите фрагмент и получите возвращаемый тип.
- выгрузить и выбросить сборку
Я умею все это делать. Но это звучит ужасно тяжеловесно для каждого запроса на завершение в редакторе.
Полагаю, мне не нужен каждый раз новый домен приложения. Я мог бы повторно использовать один AppDomain для нескольких временных сборок и амортизировать затраты на его настройку и разрушение по нескольким запросам на завершение. Это больше изменение основной идеи.
- компилировать и проверять IL
Просто скомпилируйте объявление в модуль, а затем проверьте IL, чтобы определить фактический тип, который был выведен компилятором. Как это было возможно? Что бы я использовал для проверки IL?
Есть идеи получше? Комментарии? предложения?
РЕДАКТИРОВАТЬ - размышляя об этом дальше, компиляция и вызов недопустимы, потому что вызов может иметь побочные эффекты. Так что первый вариант нужно исключить.
Кроме того, я думаю, что не могу предположить наличие .NET 4.0.
ОБНОВЛЕНИЕ . Правильный ответ, не упомянутый выше, но мягко указанный Эриком Липпертом, - реализовать систему вывода типов с полной точностью. Это единственный способ надежно определить тип переменной во время разработки. Но сделать это тоже непросто. Поскольку у меня нет иллюзий, что я хочу попытаться создать такую вещь, я воспользовался сокращением варианта 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.?
Движок добавляет соответствующие предложения using к сгенерированному исходному коду, чтобы он правильно компилировался, а затем проверка 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.?
Ни синтаксиса LINQ.
Мне нужно будет подумать о том, насколько ценны эти вещи, прежде чем я решу решить их с помощью того, что определенно является «ограниченным дизайном» (вежливое слово для взлома) для завершения.
Подход к решению проблемы, связанной с зависимостями от аргументов метода или методов экземпляра, заключался бы в замене во фрагменте кода, который генерируется, компилируется и затем анализируется IL, ссылки на эти вещи с «синтетическими» локальными переменными того же типа.
Еще одно обновление - завершение для варов, которые зависят от членов экземпляра, теперь работает.
Что я сделал, так это опросил тип (через семантику), а затем сгенерировал синтетические замещающие члены для всех существующих членов. Для такого буфера C #:
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 тип локальной переменной 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);
}
}
}
Все члены экземпляра и статического типа доступны в скелетном коде. Он успешно компилируется. В этот момент определить тип локальной переменной просто через Reflection.
Что делает это возможным:
- возможность запускать powershell в emacs
- компилятор C # действительно быстр. На моей машине компиляция сборки в памяти занимает около 0,5 с. Не достаточно быстро для анализа между нажатиями клавиш, но достаточно быстро, чтобы поддерживать создание списков завершения по запросу.
Я еще не изучал LINQ.
Это будет гораздо более серьезной проблемой, потому что семантический лексер / синтаксический анализатор, который emacs имеет для C #, не "выполняет" LINQ.
источник
Ответы:
Я могу описать вам, как мы делаем это эффективно в «настоящей» C # IDE.
Первое, что мы делаем, это запускаем проход, который анализирует только «верхний уровень» в исходном коде. Мы пропускаем все тела методов. Это позволяет нам быстро создать базу данных с информацией о том, какое пространство имен, типы и методы (а также конструкторы и т. Д.) Находятся в исходном коде программы. Анализ каждой строчки кода в каждом теле метода займет слишком много времени, если вы попытаетесь сделать это между нажатиями клавиш.
Когда IDE необходимо определить тип конкретного выражения внутри тела метода - допустим, вы набрали «foo». и нам нужно выяснить, что входит в состав foo - мы делаем то же самое; мы пропускаем столько работы, сколько можем.
Мы начинаем с прохода, который анализирует только объявления локальных переменных в этом методе. Когда мы запускаем этот проход, мы производим отображение пары «область видимости» и «имя» на «определитель типа». «Определитель типа» - это объект, который представляет понятие «Я могу определить тип этого локального объекта, если мне нужно». Определение типа местного жителя может быть дорогостоящим, поэтому мы хотим отложить эту работу, если нам нужно.
Теперь у нас есть база данных, созданная лениво, которая может сказать нам тип каждого локального. Итак, возвращаясь к этому "foo." - мы выясняем, в каком операторе находится соответствующее выражение, а затем запускаем семантический анализатор только для этого оператора. Например, предположим, что у вас есть тело метода:
и теперь нам нужно определить, что foo имеет тип char. Мы создаем базу данных, в которой есть все метаданные, методы расширения, типы исходного кода и так далее. Мы создаем базу данных, в которой есть определители типов для x, y и z. Разбираем высказывание, содержащее интересное выражение. Начнем с его синтаксического преобразования в
Чтобы определить тип foo, мы должны сначала узнать тип y. Итак, на этом этапе мы спрашиваем определитель типа «что это за тип y»? Затем он запускает оценщик выражений, который анализирует x.ToCharArray () и спрашивает «какой тип x»? У нас есть определитель типа, который говорит: «Мне нужно найти строку« в текущем контексте »». В текущем типе нет типа String, поэтому смотрим в пространство имен. Этого тоже нет, поэтому мы смотрим в директивы using и обнаруживаем, что есть «using System» и что System имеет тип String. Хорошо, это тип x.
Затем мы запрашиваем у метаданных System.String тип ToCharArray, и он говорит, что это System.Char []. Супер. Итак, у нас есть тип для y.
Теперь мы спрашиваем: "Есть ли у System.Char [] метод Where?" Нет. Итак, мы смотрим в директивы using; мы уже предварительно вычислили базу данных, содержащую все метаданные для методов расширения, которые могут быть использованы.
Теперь мы говорим: «Хорошо, есть восемнадцать дюжин методов расширения с именами Где в области видимости, есть ли у любого из них первый формальный параметр, тип которого совместим с System.Char []?» Итак, мы начинаем раунд тестирования конвертируемости. Однако методы расширения Where являются общими , что означает, что мы должны делать вывод типа.
Я написал специальный механизм вывода типов, который может обрабатывать неполные выводы из первого аргумента метода расширения. Мы запускаем вывод типа и обнаруживаем, что существует метод 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, обнаруживаем, что согласно связке лямбда это System.Char, и все готово; мы отображаем информацию о типе для System.Char.
И делаем все, кроме анализа «верхнего уровня» между нажатиями клавиш . Это действительно сложный момент. На самом деле написать весь анализ несложно; это сделать его достаточно быстрым, чтобы вы могли делать это со скоростью набора текста, что является очень сложной задачей.
Удачи!
источник
Я могу примерно рассказать вам, как Delphi IDE работает с компилятором Delphi для выполнения intellisense (понимание кода - это то, что Delphi называет этим). Он не на 100% применим к C #, но это интересный подход, заслуживающий рассмотрения.
Большая часть семантического анализа в Delphi выполняется в самом парсере. Выражения набираются по мере их анализа, за исключением ситуаций, когда это непросто - в этом случае упреждающий синтаксический анализ используется для определения того, что задумано, а затем это решение используется при синтаксическом анализе.
Анализ в основном представляет собой рекурсивный спуск LL (2), за исключением выражений, которые анализируются с использованием приоритета операторов. Одна из отличительных особенностей Delphi заключается в том, что это однопроходный язык, поэтому конструкции необходимо объявлять перед использованием, поэтому для вывода этой информации не требуется проход верхнего уровня.
Эта комбинация функций означает, что синтаксический анализатор имеет примерно всю информацию, необходимую для понимания кода в любой точке, где это необходимо. Это работает следующим образом: IDE сообщает лексеру компилятора о позиции курсора (точке, где требуется понимание кода), и лексер превращает это в специальный токен (он называется токеном кибица). Всякий раз, когда анализатор встречает этот токен (который может быть где угодно), он знает, что это сигнал для отправки всей имеющейся информации обратно в редактор. Он делает это с помощью longjmp, потому что он написан на C; что он делает, так это уведомляет конечного вызывающего о типе синтаксической конструкции (то есть грамматическом контексте), в которой была найдена точка кибица, а также обо всех символических таблицах, необходимых для этой точки. Так, например, если контекст находится в выражении, которое является аргументом метода, мы можем проверить перегрузки метода, посмотреть на типы аргументов и отфильтровать допустимые символы только до тех, которые могут разрешаться в этот тип аргумента (это сокращает много неактуального хлама в выпадающем списке). Если он находится во вложенном контексте области (например, после "."), Синтаксический анализатор вернет ссылку на область, и IDE может перечислить все символы, найденные в этой области.
Также делаются другие вещи; например, тела методов пропускаются, если токен кибица не находится в их диапазоне - это делается оптимистично, и откатывается, если он пропускает токен. Эквивалент методов расширения - помощники классов в Delphi - имеют своего рода версионный кеш, поэтому их поиск выполняется достаточно быстро. Но вывод обобщенного типа в Delphi намного слабее, чем в C #.
Теперь к конкретному вопросу: определение типов переменных, объявленных с помощью
var
, эквивалентно тому, как Паскаль определяет тип констант. Это происходит от типа выражения инициализации. Эти типы строятся снизу вверх. Еслиx
имеет типInteger
иy
имеет типDouble
, тогдаx + y
будет типDouble
, потому что таковы правила языка; и т. д. Вы следуете этим правилам, пока не получите тип для полного выражения с правой стороны, и этот тип вы используете для символа слева.источник
Если вы не хотите писать собственный синтаксический анализатор для построения абстрактного синтаксического дерева, вы можете использовать синтаксические анализаторы из SharpDevelop или MonoDevelop , оба из которых имеют открытый исходный код.
источник
Системы Intellisense обычно представляют код с помощью абстрактного синтаксического дерева, которое позволяет им определять тип возвращаемого значения функции, присвоенной переменной 'var', более или менее так же, как это сделает компилятор. Если вы используете VS Intellisense, вы можете заметить, что он не предоставит вам тип переменной, пока вы не закончите ввод действительного (разрешимого) выражения присваивания. Если выражение по-прежнему неоднозначно (например, оно не может полностью вывести общие аргументы для выражения), тип var не будет разрешен. Это может быть довольно сложный процесс, так как вам может потребоваться довольно глубоко проникнуть в дерево, чтобы определить тип. Например:
Тип возврата есть
IEnumerable<Bar>
, но для его решения необходимо знать:IEnumerable
.OfType<T>
, применимый к IEnumerable.IEnumerable<Foo>
и к нему применяется метод расширенияSelect
.foo => foo.Bar
имеет параметр foo типа Foo. Об этом свидетельствует использование Select, которое принимает a,Func<TIn,TOut>
а поскольку TIn известен (Foo), можно вывести тип foo.IEnumerable<TOut>
и TOut может быть выведено из результата лямбда-выражения, поэтому результирующий тип элементов должен бытьIEnumerable<Bar>
.источник
Поскольку вы ориентируетесь на Emacs, возможно, лучше начать с пакета CEDET. Все детали, которые Эрик Липперт уже покрыл в анализаторе кода в CEDET / Semantic tool для C ++. Существует также синтаксический анализатор C # (который, вероятно, нуждается в небольшом TLC), поэтому единственные отсутствующие части связаны с настройкой необходимых частей для C #.
Базовое поведение определяется в основных алгоритмах, которые зависят от перегружаемых функций, определенных для каждого языка. Успех двигателя завершения зависит от того, сколько настроек было выполнено. С С ++ в качестве руководства получить поддержку, подобную С ++, не должно быть так уж плохо.
Ответ Дэниела предлагает использовать MonoDevelop для синтаксического анализа и анализа. Это может быть альтернативный механизм вместо существующего синтаксического анализатора C # или его можно использовать для расширения существующего синтаксического анализатора.
источник
var
. Semantic правильно определяет его как var, но не обеспечивает вывод типа. Мой вопрос был конкретно вокруг того, как решить , что . Я также искал возможность подключения к существующему завершению CEDET, но не мог понять, как это сделать. Документация для CEDET ... а ... не полная.Преуспеть - это сложная проблема. По сути, вам необходимо смоделировать спецификацию / компилятор языка посредством большей части лексирования / синтаксического анализа / проверки типов и построить внутреннюю модель исходного кода, которую вы затем можете запросить. Эрик подробно описывает это для C #. Вы всегда можете скачать исходный код компилятора F # (часть F # CTP) и взглянуть на
service.fsi
интерфейс, предоставляемый компилятором F #, который языковая служба F # использует для обеспечения intellisense, всплывающих подсказок для предполагаемых типов и т. Д. ощущение возможного «интерфейса», если у вас уже есть компилятор, доступный в качестве API для вызова.Другой путь - повторно использовать компиляторы как есть, как вы описываете, а затем использовать отражение или посмотреть на сгенерированный код. Это проблематично с точки зрения того, что вам нужны «полные программы», чтобы получить вывод компиляции из компилятора, тогда как при редактировании исходного кода в редакторе у вас часто есть только «частичные программы», которые еще не анализируются, не реализованы все методы и т. д.
Короче говоря, я думаю, что «малобюджетную» версию очень сложно сделать хорошо, а «настоящую» версию очень и очень сложно сделать хорошо. (Где «сложно» здесь измеряется как «усилие», так и «техническая сложность».)
источник
NRefactory сделает это за вас.
источник
Для решения «1» у вас есть новое средство в .NET 4, позволяющее делать это быстро и легко. Поэтому, если вы можете преобразовать свою программу в .NET 4, это будет ваш лучший выбор.
источник