В данной статье я хочу рассказать о том, с какими проблемами столкнулись разработчики PVS-Studio при поддержке новой версии Visual Studio. Кроме того, постараюсь ответить на вопрос: почему поддержка нашего C# анализатора, основанного на "готовом решении" (в данном случае, это Roslyn), оказывается в некоторых ситуациях более затратной, чем нашего "самописного" С++ анализатора.
С выходом новой версии Visual Studio - 2017, Microsoft представляет большое количество нововведений для своей "флагманской" IDE. К их числу относятся:
Версия PVS-Studio 6.14, поддерживающая Visual Studio 2017, была выпущена спустя всего 10 дней после выхода этой IDE. Работа по поддержке новой VS была начата нами значительно раньше - еще в конце прошлого года. Безусловно, далеко не все нововведения в Visual Studio задевают работу PVS-Studio, однако, последний релиз данной IDE оказался для нас особенно трудоёмким в плане его поддержки во всех компонентах нашего продукта. Наиболее сильно затронутым оказался не наш "традиционный" C++ анализатор (поддержать новую версию Visual C++ получилось достаточно быстро), а компоненты, отвечающие за взаимодействие со сборочной системой MSBuild и платформой Roslyn (на которой основан наш C# анализатор).
Также новая версия Visual Studio стала первой с того момента, как в PVS-Studio появился C# анализатор (который мы выпустили параллельно с первым релизом Roslyn в Visual Studio 2015), а C++ анализатор на Windows был более тесно интегрирован со сборочной системой MSBuild. Поэтому из-за трудностей, возникших при обновлении данных компонентов, поддержка новой VS стала для нас наиболее затратной за всю историю нашего продукта.
Скорее всего, вам известно, PVS-Studio - это статический анализатор для языков C/C++/C#, работающий на Windows и Linux. Из чего же состоит PVS-Studio? Прежде всего это, конечно, кроссплатформенный C++ анализатор и набор также (в основном) кроссплатформенных утилит для его интеграции в различные сборочные системы.
Однако, на платформе Windows большинство наших пользователей используют стек технологий для разработки ПО от Microsoft, т.е. Visual C++/C#, Visual Studio, MSBuild и т.д. Для них у нас есть средства для работы с анализатором из среды Visual Studio (IDE плагин), и command line утилита для проверки С++/C# MSBuild проектов. Эту же утилиту "под капотом" использует и наш VS плагин. Данная утилита для анализа структуры проектов напрямую использует API, предоставляемый самим MSBuild'ом. Наш C# анализатор основан на .NET Compiler Platform (Roslyn), и пока что доступен только для Windows пользователей.
Итак, мы видим, что на платформе Windows PVS-Studio использует "родные" средства от Microsoft для интеграции в Visual Studio, анализа сборочной системы и анализа C# кода. С выходом новой версии Visual Studio все эти компоненты также были обновлены.
Помимо обновления версий MSBuild и Roslyn, Visual Studio 2017 также содержит ряд нововведений, которые сильно задели наш продукт. Так совпало, что перестал работать ряд наших компонентов, которые без особых изменений использовались у нас на протяжении нескольких предыдущих релизов Visual Studio, причём некоторые из них работали ещё со времён Visual Studio 2005 (которую мы уже не поддерживаем). Рассмотрим далее более подробно эти изменения.
Новая модульная система установки, позволяющая выбирать пользователю только необходимые ему для работы компоненты, была полностью "отвязана" от использования системного реестра Windows. Теоретически, это сделало среду разработки более "переносимой", и позволило устанавливать несколько разных редакций Visual Studio на одной системе. Всё это, однако, существенно повлияло и на разработчиков расширений, т.к. весь код, позволявший ранее определять наличие самой установленной среды или отдельных её компонентов, перестал работать.
Рисунок 1 - Новый установщик Visual Studio
Для получения информации об установленных в системе экземплярах Visual Studio, разработчикам теперь предлагается использовать COM интерфейсы, в частности - ISetupConfiguration. Думаю, многие согласятся, что удобство использования COM интерфейсов уступает чтению из реестра. И если, например, для C# кода хотя бы уже существуют wrapper'ы этих интерфейсов, то над адаптацией нашего инсталлятора, основанного на InnoSetup, пришлось изрядно поработать. Фактически же, здесь Microsoft поменяла одну Windows-specific технологию на другую. На мой взгляд, выигрыш от такого перехода достаточно сомнителен, тем более полностью от использования реестра VS пока что не смогла отказаться. По крайней мере в этой версии.
Более существенным следствием такого перехода, помимо достаточно субъективного удобства использования, для нас стало то, что он косвенно повлиял на работу применяемых нами библиотек MSBuild 15, и их обратную совместимость с предыдущими версиями MSBuild. Причина этого в том, что новая версия MSBuild также перестала использовать реестр. Мы были вынуждены обновить все используемые нами MSBuild компоненты, т.к. Roslyn напрямую от них зависит. Подробнее о последствиях этих изменений я расскажу чуть позже.
Под инфраструктурой MSBuild для C++ я, прежде всего, подразумеваю прослойку, которая отвечает за непосредственный вызов компилятора при сборке Visual C++ проектов. Эта прослойка имеет в MSBuild название PlatformToolset и отвечает за подготовку окружения, в котором работает C++ компилятор. Система PlatformToolset'ов также обеспечивает обратную совместимость с предыдущими версиями компиляторов Visual C++. Она позволяет работать с последней версией MSBuild для сборки проектов, использующих предыдущие версии Visual C++ компилятора.
Например, можно собрать проект, использующий C++ компилятор из Visual Studio 2015, в MSBuild 15 / Visual Studio 2017, если данная версия компилятора установлена в системе. Это может быть полезно, т.к. позволяет начать использовать новую версию IDE на проекте сразу, без предварительного портирования проекта на новую версию компилятора (что не всегда просто).
PVS-Studio полностью поддерживает PlatformToolset'ы и использует "родной" API MSBuild для подготовки окружения C++ анализатора, тем самым позволяя анализатору проверять исходный код наиболее близко к тому, как он компилируется.
Такая близкая интеграция с MSBuild позволяла нам раньше достаточно легко поддерживать новые версии C++ компилятора от Microsoft. Точнее сказать, поддерживать его сборочное окружение, т.к. поддержка непосредственно новых возможностей компилятора (например, синтаксис и заголовочные файлы, использующие этот синтаксис) не входит в затрагиваемые в рамках данной статьи вопросы. Мы просто добавляли новый PlatformToolset в список поддерживаемых.
В новой версии Visual C++ порядок настройки окружения компилятора претерпел заметные изменения, опять "сломавшие" наш код, работавший раньше для всех версий, начиная с Visual Studio 2010. И хотя PlatformToolset'ы от предыдущих версий компилятора по-прежнему работают, для поддержки нового toolset'а пришлось писать отдельную ветку кода. По странному совпадению (а возможно и не совпадению) разработчики MSBuild поменяли также и паттерн именования С++ toolset'ов: v100, v110, v120, v140 у прошлых версий, и v141 у последней версии (при этом Visual Studio 2017 всё равно имеет версию 15.0).
В новой версии была полностью изменена структура скриптов vcvars, на которую завязано развёртывание окружения компилятора. Эти скрипты устанавливают требуемые для компилятора переменные окружения, дополняют PATH путями до binary директорий и системных C++ библиотек и т.п. Анализатору же для работы требуется идентичное окружение, в частности для препроцессирования исходных файлов перед началом непосредственно анализа.
Можно сказать, что новая версия скриптов развёртывания, в каком-то смысле, сделана "аккуратнее", и, скорее всего, её проще поддерживать и расширять (возможно, что обновление этих скриптов было вызвано включением в состав новой версии Visual C++ поддержки clang в качестве компилятора), но с точки зрения разработчиков C++ анализатора, это прибавило нам работы.
Вместе с Visual Studio 2017 были выпущены Roslyn 2.0 и MSBuild 15. Может показаться, что для поддержки этих новых версий в PVS-Studio C# будет достаточно просто обновить в своих проектах используемые NuGet пакеты. После чего нашему анализатору станут сразу доступны все "вкусности" новых версий, такие, как поддержка C# 7.0, новых типов .NET Core проектов и т.п.
Действительно, обновить используемые нами пакеты и пересобрать C# анализатор оказалось достаточно просто. Однако, первый же запуск такой новой версии на наших тестах показал, что "всё сломалось". Дальнейшие эксперименты показали, что C# анализатор корректно работает только в системе, в которой установлена Visual Studio 2017 \ MSBuild 15. Одного того, что наш дистрибутив содержит в себе нужные версии, используемых нами Roslyn\MSBuild библиотек, оказалось недостаточно. Выход новой версии C# анализатора "как есть", повлек бы за собой ухудшение результатов анализа у всех наших пользователей, работающих с предыдущими версиями C# компиляторов.
Когда мы создавали первую версию C# анализатора, использовавшую Roslyn 1.0, то постарались сделать наш анализатор как можно более "независимым" решением, не требующим от пользователя наличия в системе каких-либо сторонних установленных компонентов. При этом главным требованием к пользовательской системе является собираемость проверяемого проекта - если проект собирается, то он может быть проверен анализатором. Очевидно, что на Windows, для сборки Visual C# проектов (csproj), требуется наличие как минимум MSBuild и C# компилятора.
Мы сразу же отказались от идеи обязывать наших пользователей устанавливать последние версии MSBuild и Visual C# вместе с C# анализатором. Если у пользователя нормально собирается проект, например, в Visual Studio 2013 (которая в свою очередь использует MSBuild 12), требование установить MSBuild 15 будет выглядеть избыточным. Мы же, наоборот, стараемся снизить "порог" для начала использования нашего анализатора.
Web установщики от Microsoft оказались весьма требовательными к размеру необходимых им загрузок - в то время, как наш дистрибутив имеет размер порядка 50 мегабайт, установщик, например, для Visual C++ 2017 (который также нужен для C++ анализатора) оценивал объём данных для скачивания в размере около 3 гигабайт. В итоге, как мы выяснили позже, наличия этих компонентов всё равно оказалось бы недостаточно для полностью корректной работы C# анализатора.
Когда мы только начинали разрабатывать наш C# анализатор, у нас было 2 пути взаимодействия с платформой Roslyn.
Первым вариантом было использование специально созданного для разработки .NET анализаторов Diagnostics API. Данный API предоставляет возможность, наследуясь от абстрактного класса DiagnosticAnalyzer, реализовать свои "диагностики". С помощью класса CodeFixProvider пользователи могли реализовать автоматические исправления таких предупреждений.
Безусловным достоинством такого подхода является вся мощь уже существующей инфраструктуры Roslyn. Диагностические правила становятся сразу доступными в редакторе кода Visual Studio, могут автоматически применяться как при редактировании кода в IDE редакторе, так и при запуске пересборки проекта. Этот подход не требует от разработчика анализатора самостоятельно открывать проектные файлы и файлы с исходниками - всё будет сделано в рамках работы "родного" компилятора, основанного на Roslyn. Если мы пошли бы изначально этим путём, то проблем с переходом на новый Roslyn у нас, скорее всего, не возникло бы, по крайней мере в таком виде, как сейчас.
Вторым вариантом было реализовать полностью автономный анализатор, по аналогии с тем, как работает PVS-Studio C++. На нем мы и остановились, т.к. решили сделать инфраструктуру C# анализатора как можно более близкой к существующему инструменту для C/C++. В дальнейшем это позволило достаточно быстро адаптировать как действующие C++ диагностики (конечно, не все, а ту их часть, что была актуальна для C#), так и более "продвинутые" методики анализа.
Roslyn предоставляет возможности для реализации такого подхода: мы самостоятельно открываем проектные файлы Visual C#, строим синтаксические деревья из исходного кода и реализуем собственный механизм для их обхода. Всё это осуществляется с помощью API MSBuild и Roslyn. Таким образом, мы получили полный контроль за всеми этапами анализа, и не зависим от работы компилятора или IDE.
Какой бы заманчивой не казалась "бесплатная" интеграция с редактором кода Visual Studio, мы предпочли использовать собственный IDE интерфейс, так как он представляет намного больше возможностей, чем стандартный Error List (куда будут выдаваться такие предупреждения). Использование Diagnostics API также ограничило бы нас версиями компилятора, основанными на Roslyn, т.е. идущими вместе с Visual Studio 2015 и 2017, тогда как независимый анализатор позволил нам поддержать и все предыдущие версии.
В ходе создания C# анализатора мы столкнулись с тем, что Roslyn оказался очень жёстко завязан на MSBuild. Конечно, здесь я говорю про Windows версию Roslyn'а, с Linux версией нам, к сожалению, пока ещё не пришлось поработать, так что не могу сказать, как обстоят дела там.
Скажу сразу, что API Roslyn'а для работы с MSBuild проектами, на мой взгляд, остаётся достаточно сырым даже в версии 2.0. При создании C# анализатора нам пришлось писать много различных "костылей", т.к. Roslyn некорректно делал некоторые вещи (некорректно = "не так, как это делал бы MSBuild при сборке тех же проектов"), что приводило, в конечном итоге, к ложным срабатываниям и ошибкам анализа при проверке исходных файлов.
Именно такая завязанность Roslyn на MSBuild и привела к тем проблемам, с которыми мы столкнулись при обновлении до Visual Studio 2017.
Для работы анализатора нам, по большей части, требуется получить от Roslyn две сущности: синтаксическое дерево проверяемого кода и семантическую модель этого дерева, т.е. значение синтаксических конструкций, представляющих его узлы - типы полей классов, возвращаемые значения и сигнатуры методов и т.п. И если для получения синтаксического дерева с помощью Roslyn достаточно иметь файл с исходным кодом, то для генерации семантической модели этого файла необходимо провести компиляцию проекта, его включающего.
Обновление Roslyn до версии 2.0 привело к появлению ошибок в семантической модели на наших тестах (на это указывают сообщения V051 анализатора). Такие ошибки обычно проявляются в результатах работы анализатора как ложно негативные\ложно позитивные срабатывания, т.е. часть хороших сообщений пропадает и появляются плохие сообщения.
Для получения семантической модели Roslyn предоставляет т.н. Workspace API, который может открывать .NET MSBuild проекты (в нашем случае это csproj и vbproj) и доставать "компиляции" таких проектов. В данном контексте речь идёт об объекте служебного класса Compilation в Roslyn, абстрагирующем в себе подготовку и вызов C# компилятора. Из такой "компиляции" мы можем достать непосредственно семантическую модель. Ошибки в компиляции приводят в конечном итоге к ошибкам в семантической модели.
Рассмотрим теперь, как взаимодействует Roslyn с MSBuild для получения "компиляции" проекта. Далее приведена диаграмма, иллюстрирующая данное взаимодействие в упрощённом виде:
Рисунок 2 - Схема взаимодействия Roslyn и MSBuild
Диаграмма разделена на 2 сегмента - PVS-Studio и Build Tools. Сегмент PVS-Studio содержит компоненты, идущие в дистрибутиве с нашим анализатором - библиотеки MSBuild и Roslyn, реализующие используемые нами API. Сегмент Build Tools включает инфраструктуру сборочной системы, которая должна присутствовать в системе для корректной работы данных API.
После того, как анализатор запросил у Workspace API объект компиляции (для получения семантической модели), Roslyn запускает сборку проекта, или, по терминологии MSBuild -выполнение сборочной задачи (task) csc. После запуска сборки, управление переходит в MSBuild, который производит все подготовительные шаги в соответствии со своими сборочными сценариями.
Нужно отметить, что это не "обычная" сборка (она не приведёт к генерации бинарных файлов), а т.н. design режим. Конечная цель этого шага - получение Roslyn всей информации, которая была бы предоставлена компилятору во время "настоящей" сборки. Если сборка завязана на выполнение каких-то предсборочных шагов (например, запуск скриптов для автогенерации части исходных файлов), все такие действия также будут выполнены MSBuild'ом, как если бы это была бы обычная сборка.
Получив управление, MSBuild, а точнее, библиотека, идущая в составе PVS-Studio, начнёт искать в системе установленные сборочные инструментарии (toolset'ы). Найдя подходящий toolset, она попытается инстанцировать шаги, описанные в сценарии сборки. Toolset'ы соответствуют установленным в системе экземплярам MSBuild. Например, MSBuild 14 (Visual Studio 2015) устанавливает toolset 14.0, MSBuild 12 - 12.0, и т.д.
Toolset содержит все типовые сценарии сборки MSBuild проектов. Проектный файл (например, csproj) обычно содержит лишь список входных файлов сборки (например, файлы с исходным кодом). Toolset содержит все шаги, которые над этими файлами нужно провести: от компиляции и линковки, до публикации результатов сборки. Не будем останавливаться подробно на том, как работает MSBuild, важно лишь понимать, что одного файла проекта и парсера этого проекта (т.е. той MSBuild библиотеки, которая идёт с PVS-Studio) недостаточно для осуществления полноценной сборки.
Переходим к сегменту диаграммы Build Tools. Нас интересует сборочный шаг csc. MSBuild потребуется найти библиотеку, где этот шаг непосредственно реализован, и для этого будет использован tasks файл выбранного toolset'а. Tasks файл - это xml файл, содержащий пути до библиотек, реализующих стандартные сборочные task'и. В соответствии с этим файлом будет найдена и загружена библиотека, содержащая реализацию таска csc. Этот таск подготовит всё для вызова непосредственно самого компилятора (обычно это отдельная command line утилита csc.exe). Как мы помним, у нас "ненастоящая" сборка, и поэтому, когда всё будет готово, вызов компилятора не произойдёт. У Roslyn уже есть вся необходимая информация для получения семантической модели - раскрыты все ссылки на другие проекты и библиотеки (ведь в проверяемом коде могут использоваться типы, объявленные в этих зависимостях), проведены все предсборочные шаги, восстановлены\скопированы все зависимости и т.п.
К счастью, если на каком-то из этих шагов что-то "пошло не так", Roslyn имеет запасной механизм для подготовки семантической модели на основании информации, известной до начала компиляции, т.е. до момента перехода управления в MSBuild Execution API. Обычно это информация из эвалюации проектного файла (которая правда также проводится средствами отдельной MSBuild Evaluation API). Часто этой информации для построения полной семантической модели оказывается недостаточно. Самый яркий пример здесь - новый формат .NET Core проектов, в котором сам проектный файл вообще ничего не содержит - даже списка исходных файлов, не говоря уже о зависимостях. Но даже и в "обычных" csproj файлах мы наблюдали потерю ссылок до зависимостей и символов условной компиляции (define'ов) после неудачной компиляции, хотя их значения были напрямую прописаны в самом проектном файле.
Теперь, когда, как я надеюсь, более или менее стало понятно, что происходит "внутри" PVS-Studio при проверке C# проекта, посмотрим, что же случилось после обновления Roslyn и MSBuild. Из приведённой выше диаграммы хорошо видно, что часть Build Tools с точки зрения PVS-Studio находится "во внешней среде" и, соответственно, не контролируется нами. Как описывалось ранее, мы отказались от идеи нести весь MSBuild в дистрибутиве, поэтому нам приходится полагаться на то, что будет установлено у пользователя в системе. Вариантов может быть много, ведь мы поддерживаем работу со всеми версиями Visual C#, начиная с 2010 версии Visual Studio. При этом Roslyn стал использоваться как основа для C# компилятора начиная только с предыдущей версии Visual Studio - 2015.
Рассмотрим ситуацию, когда в системе, в которой запускают анализатор, не установлен MSBuild 15. Анализатор запускаем для проверки проекта под Visual Studio 2015 (MSBuild 14). И здесь мы сразу же сталкиваемся с первым "косяком" в Roslyn - при открытии MSBuild проекта он не указывает корректный toolset. Если toolset не указан, MSBuild начинает использовать toolset по умолчанию - в соответствии с версией используемой MSBuild библиотеки. И т.к. Roslyn 2.0 скомпилирован с зависимостью от MSBuild 15, то такую версию toolset'а эта библиотека и выбирает.
В связи с тем, что этот toolset в системе отсутствует, MSBuild инстанцирует этот toolset некорректно - получается "мешанина" из несуществующих и некорректных путей, указывающих на toolset 4-ой версии. Почему 4-ой? Потому что такой toolset вместе с 4-ой же версией MSBuild всегда доступен в системе, как часть .NET Framework 4 (в последующих версиях MSBuild отвязали от framework'а). Результатом этого является выбор некорректного targets файла, некорректного csc task'а и, в конечном итоге, ошибки в компиляции и семантической модели.
Почему же мы не сталкивались с такой ошибкой на старой версии Roslyn? Во-первых, судя по статистике использования анализатора, у большинства наших пользователей Visual Studio 2015, т.е. правильная (для Roslyn 1.0) версия MSBuild уже установлена.
Во-вторых, новая версия MSBuild, как я упоминал ранее, перестала использовать реестр для хранения своих конфигураций, и, в частности, информации об установленных toolset'ах. И если все предыдущие версии MSBuild хранили свои toolset'ы в реестре, то MSBuild 15 теперь хранит его в config файле рядом с MSBuild.exe. Также новый MSBuild изменил "адрес прописки" - предыдущие версии ставились единообразно в c:\Program Files (x86)\MSBuild\%VersionNumber%, а новая версия теперь развёртывается по умолчанию в директорию установки Visual Studio (которая тоже поменялась в сравнении с прошлыми версиями).
Данный факт иногда "скрывал" ошибочно выбранный toolset в прошлых версиях - семантическая модель генерировалась корректно и с ним. Более того, даже если в системе требуемый нам новый toolset есть, используемая нами библиотека может его и не найти - ведь теперь он прописан в app.config файле MSBuild.exe, а не в реестре, а библиотека подгружена не из процесса MSBuild.exe, а из PVS-Studio_Cmd.exe. При этом в новом MSBuild есть запасной механизм на этот случай. Если в системе установлен COM сервер, в котором реализован упомянутый мною ранее ISetupConfiguration, MSBuild попробует найти toolset в установочной директории Visual Studio. Однако, standalone инсталлятор MSBuild, конечно же, этот COM интерфейс не регистрирует - это делает только установщик Visual Studio.
И, наконец, в-третьих, наверное, самой главной причиной было, к сожалению, недостаточное тестирование нашего анализатора на различных вариантах поддерживаемых конфигураций, что не позволило нам выявить проблему раньше. Так получилось, что на всех наших машинах для ежедневного тестирования анализаторов, установлены Visual Studio 2015 \ MSBuild 14. К счастью, мы смогли идентифицировать и исправить данную проблему раньше, чем об этом нам написали бы наши клиенты.
Разобравшись почему Roslyn не работает, мы решили попробовать указывать при открытии проекта правильный toolset. Отдельный вопрос, какой именно toolset считать "правильным"? Мы задались им, когда использовали те же MSBuild API для открытия C++ проектов для нашего C++ анализатора. Т.к. этому вопросу можно посвятить целую статью, не будем сейчас на нём останавливаться. К сожалению, Roslyn не предоставляет возможности выбрать, какой toolset он будет использовать, поэтому пришлось модифицировать его собственный код (дополнительное неудобство для нас, т.к. не получится просто взять готовые NuGet пакеты). После этого проблемы исчезли в некоторых проектах из нашей тестовой базы. Однако, в ещё большем количестве проектов появились новые проблемы. Что же пошло не так теперь?
Тут стоит отметить, что все процессы, описанные на приведённой выше диаграмме, происходят в рамках одного процесса операционной системы - PVS-Studio_Cmd.exe. Оказалось, что при выборе корректного toolset'а произошёл конфликт при загрузке dll модулей. Наша тестовая версия использует Roslyn 2.0, частью которого является, например, библиотека Microsoft.CodeAnalysis.dll, имеющая также версию 2.0. Эта библиотека на момент начала анализа проекта уже загружена в память процесса PVS-Studio_Cmd.exe (наш C# анализатор). Напомню, что мы проверяем проект под 2015 Visual Studio, поэтому при открытии указываем toolset 14.0. Далее, MSBuild находит правильный tasks файл и начинает компиляцию. Т.к. C# компилятор в данном toolset'е (напоминаю, что мы используем Visual Studio 2015) использует Roslyn версии 1.3, то, соответственно, MSBuild пытается загрузить в память процесса Microsoft.CodeAnalysis.dll версии 1.3. И отваливается на этом шаге, т.к. в памяти процесса уже загружен такой же модуль более высокой версии.
Что можем сделать мы в этой ситуации? Пытаться получить семантическую модель в отдельном процессе или AppDomain'е? Но для получения модели нужен Roslyn (т.е. все те библиотеки, которые и вызывают конфликт), а перебросить модель из одного процесса\домена в другой может оказаться нетривиальной задачей, т.к. этот объект держит в себе ссылки на проекты, компиляции и workspace'ы, из которых он был получен.
Более правильным вариантом будет выделить C# анализатор в отдельный backend процесс из нашего общего для C++ и C# анализаторов solution парсера, и сделать 2 версии таких backend'ов - использующих Roslyn 1.0 и 2.0 соответственно. Но и у такого решения есть существенные недостатки:
Давайте рассмотрим последний пункт более подробно. За время существования Visual Studio 2015 было выпущено 3 обновления, в каждом из которых обновлялась и версия Roslyn компилятора - с 1.0 до 1.3. При выходе обновления, например, до 2.1 нам придётся, в случае выделения backend'а, делать отдельные версии анализатора для каждого минорного обновления студии, либо возможность повторения ошибки с конфликтом версий будет оставаться у пользователей, использующих не самую свежую версию Visual Studio.
Замечу также, что компиляция отваливалась и когда мы пытались работать с toolset'ами, не использующими Roslyn, например, версии 12.0 (Visual Studio 2013). Причина была другой, но мы не стали копать глубже, т.к. и уже известных проблем было достаточно, чтобы отказаться от подобного решения.
Разобравшись в причинах возникновения ошибок, мы пришли к необходимости "поставлять" toolset версии 15.0 с анализатором. Его наличие избавляет нас от проблем с конфликтом версий компонентов Roslyn и позволяет проверять проекты для всех прошлых версий Visual Studio (последняя версия компилятора обратно совместима со всеми предыдущими версиями языка C#). Выше я уже описывал, почему мы решили не тянуть в наш инсталлятор "полноценный" MSBuild 15:
Однако, во время исследования проблем, возникавших в Roslyn при компиляции проектов, мы поняли, что наш дистрибутив уже содержал практически все необходимые для такой компиляции библиотеки Roslyn и MSBuild (напомню, что Roslyn проводит "ненастоящую" компиляцию, поэтому сам компилятор csc.exe ему не нужен). Фактически, для полноценного toolset'а нам не хватало только нескольких props и targets файлов, в которых этот toolset и описан. А это обычные xml файлы в формате MSBuild проектов, весящие суммарно всего несколько мегабайт - у нас нет проблемы включить эти файлы в дистрибутив.
Главной же проблемой стала, фактически, необходимость "обмануть" библиотеки MSBuild и заставить из подхватывать "наш" toolset как родной. В коде MSBuild есть такой комментарий: Running without any defined toolsets. Most functionality limited. Likely will not be able to build or evaluate a project. (e.g. reference to Microsoft.*.dll without a toolset definition or Visual Studio instance installed). Этот комментарий описывает режим, в котором работает MSBuild библиотека, когда она добавлена в какой-то проект просто как ссылка, а не используется из MSBuild.exe. И этот комментарий не внушает оптимизма, особенно его часть про то, что "скорее всего ничего не будет работать".
Как же заставить библиотеки MSBuild 15 использовать сторонний toolset? Напомню, что этот toolset объявлен в app.config у файла MSBuild.exe. Оказалось, что можно добавить содержимое конфига в конфиг нашего приложения (PVS-Studio_Cmd.exe) и задать переменную окружения MSBUILD_EXE_PATH у нашего процесса с указанием на наш исполняемый файл. И такой метод заработал! В этот момент последняя версия MSBuild находилась в стадии Release Candidate 4. На всякий случай мы решили посмотреть, как обстоят дела в master ветке MSBuild на GitHub'е. И как будто по закону Мерфи, в master'е в коде выбора toolset'а была добавлена проверка - брать из appconfig'а toolset только в случае, когда именем исполняемого файла является MSBuild.exe. Так в нашем дистрибутиве появился файл размером 0 байт с именем MSBuild.exe, на который теперь указывает переменная окружения MSBUILD_EXE_PATH у процесса PVS-Studio_Cmd.exe.
На этом наши приключения с MSBuild не закончились. Оказалось, что одного toolset'а недостаточно для проектов, использующих расширения MSBuild - дополнительные сборочные шаги. Например, к таким типам проектов относятся WebApplication, Portable, .NET Core проекты. При установке соответствующего компонента Visual Studio, эти расширения определяются в отдельную директорию рядом с MSBuild. В нашей "установке" MSBuild этого всего, понятное дело, не было. Решение мы нашли благодаря возможности легко править "наш собственный" toolset. Для этого мы завязали пути поиска (свойство MSBuildExtensionsPath) нашего toolset'а на специальную переменную окружения, которую процесс PVS-Studio_Cmd.exe выставляет в зависимости от типа проверяемого проекта. Например, если у нас есть WebApplication проект для Visual Studio 2015, мы, ожидая, что проект у пользователя компилируем, ищем, где лежат extension'ы для toolset'а версии 14.0, и указываем путь до них в нашу специальную переменную окружения. Эти пути нужны MSBuild только для включения в сборочный сценарий дополнительных props\targets файлов, поэтому проблем с конфликтами версий не возникает.
В итоге C# анализатор может работать на системе с любой из поддерживаемых нами версий Visual Studio, независимо от наличия каких-либо версий MSBuild. Потенциальной проблемой для нас может теперь стать наличие у пользователя каких-то собственных модификаций сборочных сценариев MSBuild, но благодаря независимости нашего toolset'а, эти же модификации при необходимости можно будет внести и в работу PVS-Studio.
Одним из новшеств Visual Studio 2017, позволяющих оптимизировать работу с решениями, содержащими большое число проектов, является режим отложенной загрузки - "lightweight solution load".
Рисунок 3 - lightweight solution load
Данный режим может быть включен в IDE как для отдельного решения, так и для всех загружаемых решений принудительно. Особенностью использования режима "lightweight solution load" является только показ дерева проектов (без их загрузки) в обозревателе решений Visual Studio. Загрузка выбранного проекта (раскрытие его внутренней структуры и загрузка содержащихся в проекте файлов) производится только по запросу: после соответствующего действия пользователя (раскрытие узла проекта в дереве), либо программно. Подробное описание режима отложенной загрузки приведено в документации.
В процессе работы по поддержке данного режима мы столкнулись с рядом сложностей:
Здесь я хотел бы отметить недостаточный, на мой взгляд, объем технической документации по вопросу использования режима отложенной загрузки. Фактически, вся документация, раскрывающая особенности внутренних механизмов работы с новыми возможностями Visual Studio 2017 в отношении режима "lightweight solution load", ограничена одной статьёй.
Что же касается "подводных камней", то в процессе доведения версии RC Visual Studio "до ума" в Microsoft не только устраняли недочеты, но и переименовывали некоторые методы во вновь добавленных интерфейсах, которые мы как раз использовали. Как результат - потребовалась доработка уже, казалось бы, отлаженного механизма поддержки режима отложенной загрузки проектов в релизной версии PVS-Studio.
Почему в релизной? Дело в том, что один из используемых нами интерфейсов оказался объявленным в библиотеке, которая включена в Visual Studio 2 раза - один раз в основной установке Visual Studio, и 2-ой раз - как часть пакета Visual Studio SDK (пакет для разработки Visual Studio расширений). По какой-то причине разработчики Visual Studio SDK не обновили RC версию данной библиотеки в релизе Visual Studio 2017. И т.к. у нас практически на всех машинах этот SDK был установлен (в том числе на машине, запускающей ночные тесты – она же используется и как сборочный сервер), ошибок при компиляции и работе мы не обнаружили. К сожалению, данную ошибку мы поправили уже после релиза PVS-Studio, получив багрепорт от пользователя. Что же касается статьи, о которой я писал выше, упоминания этого интерфейса в ней на момент публикации данного текста всё ещё остаются со старым именем.
Релиз Visual Studio 2017 оказался самым "дорогостоящим" для поддержки в PVS-Studio за все время существования продукта. Причиной этого стало совпадение нескольких факторов - существенные изменения в работе MSBuild\Visual Studio, появление C# анализатора в составе PVS-Studio (который также необходимо теперь поддерживать).
Когда полтора года назад мы начинали создание статического анализатора для C#, то рассчитывали, что платформа Roslyn позволит нам сделать это очень быстро. Эти надежды во многом оправдались - релиз первой версии анализатора состоялся спустя 4 месяца. Также мы рассчитывали, что, по сравнению с нашим C++ анализатором, использование стороннего решения позволит нам значительно сэкономить на поддержке новых возможностей при развитии языка C#. Это ожидание также подтвердилось. Несмотря на всё это, использование готовой платформы для статического анализа оказалось не таким "безболезненным", как показал наш опыт поддержки новых версий Roslyn / Visual Studio. Решая вопросы совместимости с новыми возможностями C#, использование Roslyn, из-за своей завязанности на сторонние компоненты (в частности MSBuild и Visual Studio), создаёт трудности в совсем других сферах. Завязанность Roslyn на MSBuild существенно затрудняет его использование в standalone анализаторе кода.
Нам часто задают вопрос, почему мы также не хотим "переписать" наш C++ анализатор на основе какого-нибудь готового решения, например, Clang. И действительно, это позволило бы нам снять ряд существующих сейчас проблем в нашем C++ ядре. Однако, помимо необходимости переписывать существующие сейчас механизмы и диагностики, нельзя также забывать, что при использовании стороннего решения всегда будут возникать подобные "подводные камни", которые невозможно будет полностью предусмотреть заранее.