>
>
>
Поддержка Visual Studio 2019 в PVS-Stud…

Сергей Васильев
Статей: 96

Поддержка Visual Studio 2019 в PVS-Studio

Поддержка Visual Studio 2019 в PVS-Studio затронула сразу несколько различных компонентов: сам плагин для IDE, command line приложение для анализа, ядра C++ и C# анализаторов, а также несколько утилит. О том, с какими проблемами мы столкнулись в ходе поддержки новой версии IDE и как их решали, я кратко расскажу в данной статье.

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

Начиная с первой версии анализатора PVS-Studio, в которой появился плагин для среды Visual Studio (тогда ещё это была версия Visual Studio 2005), поддержка новых версий Visual Studio была для нас достаточно простой задачей - в основном она сводилась к обновлению проектного файла плагина и зависимостей различных API расширений Visual Studio. Иногда требовалось дополнительно поддерживать новые возможности языка C++, которым постепенно учился компилятор Visual C++, однако это тоже обычно не доставляло проблем непосредственно перед релизом очередной редакции Visual Studio. Да и анализатор в PVS-Studio тогда был только один - для языков C и C++.

Всё изменилось к релизу Visual Studio 2017. Помимо того, что в данной версии очень существенно поменялись многие API расширения данной IDE, после обновления мы столкнулись с проблемами обеспечения обратной совместимости работы появившегося у нас к тому моменту нового C# анализатора (а также нашей новой прослойки C++ анализатора для MSBuild проектов) со старыми версиями MSBuild \ Visual Studio.

Поэтому перед чтением данной статьи я настоятельно рекомендую ознакомиться с родственной статьей про поддержку Visual Studio 2017: "Поддержка Visual Studio 2017 и Roslyn 2.0 в PVS-Studio: иногда использовать готовые решения не так просто, как кажется на первый взгляд". В упомянутой выше статье описываются проблемы, с которыми мы столкнулись в прошлый раз, а также схемы взаимодействия разных компонентов (например, PVS-Studio, MSBuild, Roslyn). Понимание этого взаимодействия будет полезно при чтении данной статьи.

В конечном счёте решение этих проблем привнесло существенные изменения в наш анализатор, и, как мы надеялись, новые подходы, которые мы применили тогда, позволят в будущем значительно проще и быстрее поддерживать обновлённые версии Visual Studio \ MSBuild. Отчасти это предположение уже нашло своё подтверждение при выходе многочисленных обновлений Visual Studio 2017. Помог ли нам этот новый подход при поддержке Visual Studio 2019? Об этом ниже.

Плагин PVS-Studio для Visual Studio 2019

Началось всё, казалось бы, неплохо. Достаточно легко удалось портировать плагин под Visual Studio 2019, где он запускался и нормально работал. Несмотря на это, сразу обнаружились 2 проблемы, который сулили неприятности в будущем.

Первая - интерфейс IVsSolutionWorkspaceService, используемый для поддержки режима Lightweight Solution Load, который, кстати, был отключен в одном из прошлых обновлений ещё в Visual Studio 2017, был декорирован атрибутом Deprecated, что пока являлось только предупреждением при сборке, но в будущем сулило большие проблемы. Быстро же Microsoft ввели этот режим и отказались от него же... С этой проблемой разобрались достаточно просто - отказались от использования соответствующего интерфейса.

Вторая - при загрузке Visual Studio с плагином появилось следующее сообщение: Visual Studio has detected one or more extensions that are at risk or not functioning in a feature VS update.

Просмотр логов запуска Visual Studio (ActivityLog файла) окончательно расставил точки над 'i':

Warning: Extension 'PVS-Studio' uses the 'synchronous auto-load' feature of Visual Studio. This feature will no longer be supported in a future Visual Studio 2019 update, at which point this extension will not work. Please contact the extension vendor to get an update.

Для нас это означало одно - изменение способа загрузки плагина на асинхронный режим. Надеюсь, вы не расстроитесь, если я не буду перегружать вас подробностями о взаимодействии с COM интерфейсами Visual Studio, и по изменениям пройдусь достаточно кратко.

У Microsoft есть статья на тему создания асинхронно загружаемых плагинов: "How to: Use AsyncPackage to load VSPackages in the background". При этом для всех было очевидно, что этими изменениями дело не ограничится.

Одно из основных изменений - способ загрузки, а точнее - инициализации. Ранее необходимая инициализация проходила в двух методах - переопределённом методе Initialize нашего класса-наследника Package, и в методе OnShellPropertyChange. Необходимость выноса части логики в метод OnShellPropertyChange связана с тем, что при синхронной загрузке плагина Visual Studio может быть ещё не полностью загружена и проинициализирована, и как следствие этого - не все необходимые действия можно было выполнить на этапе инициализации плагина. Вариант решения такой проблемы - ожидание выхода Visual Studio из 'zombie' состояния и отложенное выполнение этих действий. Это логика и была вынесена в OnShellPropertyChange с проверкой 'zombie' состояния.

В абстрактном классе AsyncPackage, от которого наследуются асинхронно загружаемые плагины, метод Initialize имеет модификатор sealed, так что инициализацию необходимо проводить в переопределённом методе InitializeAsync, что и было сделано. Логику с отслеживанием 'zombie' состояния Visual Studio тоже пришлось менять, так как эту информацию в плагине мы получать перестали. Однако ряд действий, которые необходимо было выполнять после инициализации плагина, никуда не делся. Выходом стало использование метода OnPackageLoaded интерфейса IVsPackageLoadEvents, где и выполнялись действия, требующие отложенного исполнения.

Другая проблема, логично вытекшая из факта асинхронной загрузки плагина - отсутствие в момент запуска Visual Studio команд плагина PVS-Studio. При открытии лога анализатора по "двойному клику" из файлового менеджера (если его нужно открывать через Visual Studio) запускалась необходимая версия devenv.exe с командой открытия отчёта анализатора. Команда запуска выглядела примерно так:

"C:\Program Files (x86)\Microsoft Visual Studio\
 2017\Community\Common7\IDE\devenv.exe"
/command "PVSStudio.OpenAnalysisReport 
C:\Users\vasiliev\source\repos\ConsoleApp\ConsoleApp.plog"

Флаг "/command" здесь используется для вызова команды, зарегистрированной в Visual Studio. Теперь такой подход не работал, так как команды не были доступны до момента загрузки плагина. В итоге пришлось остановится на "костыле" с разбором строки запуска devenv.exe после загрузки плагина, и в случае наличия строкового представления команды открытия лога - собственно, загрузка лога. Таким образом, отказавшись в данном случае от использования "правильного" интерфейса для работы с командами, удалось сохранить необходимую функциональность, отложив загрузку лога до момента полной загрузки плагина.

Фух, вроде бы разобрались и всё работает - всё загружается и открывается корректно, никаких предупреждений нет - наконец-то.

И тут происходит неожиданное - Павел (привет!) устанавливает себе плагин, после чего спрашивает, почему мы асинхронную загрузку то не сделали?

Сказать, что мы были удивлены - ничего не сказать - как так-то? Нет, действительно - вот установлена новая версия плагина, вот сообщение о том, что пакет - синхронно загружаемый. Устанавливаем с Александром (и тебе тоже привет) ту же версию плагина на свои машины - всё хорошо. Ничего непонятно - решили посмотреть, какие версии библиотек PVS-Studio загружены в Visual Studio. И внезапно оказывается, что используются версии библиотек PVS-Studio для Visual Studio 2017, при том, что в VSIX-пакете лежат правильные версии библиотек - для Visual Studio 2019.

Повозившись с VSIXInstaller, удалось найти причину проблемы - кэш пакетов. Подтверждалась теория и тем, что при ограничении прав доступа к пакету в кэше (C:\ProgramData\Microsoft\VisualStudio\Packages) VSIXInstaller писал информацию об ошибке в лог. Что удивительно, если ошибки нет, никакой информации о том, что по факту пакет устанавливается из кэша, в лог не пишется.

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

В итоге происходило следующее - при установке плагина VSIXInstaller видел, что соответствующий пакет уже есть в кэше (там находился .vsix пакет для Visual Studio 2017), и при установке использовал его, а не фактически устанавливаемый пакет. Почему при этом не учитываются ограничения / требования, описанные в .vsixmanifest (например, версия Visual Studio, для которой можно устанавливать расширение) - вопрос открытый. Из-за этого получилось, что, хотя .vsixmanifest содержал необходимые ограничения, плагин, предназначенный для Visual Studio 2017, устанавливался на Visual Studio 2019.

Самое ужасное - такая установка ломала граф зависимостей Visual Studio, и, хотя внешне даже могло показаться, что среда разработки работает нормально, на самом деле всё было очень плохо. Нельзя было устанавливать и удалять расширения, производить обновления, и так далее. Процесс 'восстановления' тоже был достаточно неприятным, т.к. необходимо было самостоятельно удалять расширение (соответствующие файлы), а также вручную редактировать конфигурационные файлы, хранящие информацию об установленном пакете. В общем - приятного мало.

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

И раз речь пошла про неприятные сюрпризы, упомяну ещё один - после обновления на Preview 2 пункт меню 'переехал' под вкладку 'Extensions'. Казалось бы - ничего страшного, но доступ к функциям плагина стал менее удобным. На последующих версиях Visual Studio 2019, включая релизную, это поведение сохранилось. Упоминаний об этой 'фиче' на момент её выпуска в документации или блоге я не нашёл.

Теперь, казалось бы, всё работает, и с поддержкой плагина для Visual Studio 2019 закончено. На следующий день после релиза PVS-Studio 7.02 с поддержкой Visual Studio 2019 оказалось, что это не так – обнаружила себя ещё одна проблема с асинхронным плагином. Для пользователя это могло выглядеть так: при открытии окна с результатами анализа (или запуске анализа), наше окно иногда отображалось "пустым" - в нём отсутствовало содержимое: кнопки, таблица с предупреждениями анализатора и т.п.

На самом деле эта проблема иногда повторялась в ходе работы. Однако повторялась она только на одной машине, и начала появляться только после обновления Visual Studio в одной из первых версий 'Preview' – были подозрения, что что-то сломалось при установке / обновлении. Со временем проблема, однако, перестала повторяться даже на этой машине, и мы решили, что оно "починилось само собой". Оказалось, что нет – просто так везло. Точнее, не везло.

Дело оказалось в порядке инициализации самого окна среды (наследник класса ToolWindowPane) и его содержимого (собственно, наш контрол с гридом и кнопками). При определённых условиях, инициализация контрола происходила раньше инициализации pane, и, несмотря на то, что всё работало без ошибок, метод FindToolWindowAsync (создающий окно при первом обращении) корректно отрабатывал, но контрол оставался невидимым. Мы исправили это добавлением lazy инициализации для нашего контрола в код заполнения pane.

Поддержка C# 8.0

У использования Roslyn в качестве основы для анализатора есть весомое преимущество - нет необходимости вручную поддерживать новые конструкции языка. Всё это поддерживается и реализуется в рамках библиотек Microsoft.CodeAnalysis - мы же используем уже готовые результаты. Таким образом, поддержка нового синтаксиса имплементируется за счёт обновления библиотек.

Конечно, что касается непосредственно статического анализа, здесь уже приходится всё делать самостоятельно, в частности - обрабатывать новые конструкции языка. Да, новое синтаксическое дерево мы получаем автоматически за счёт использования более свежей версии Roslyn, но нужно научить анализатор, как именно воспринимать и обрабатывать новые / изменённые узлы дерева.

Думаю, наиболее обсуждаемым нововведением C# 8 являются nullable reference типы. Писать про них здесь не буду - это достаточно большая тема, достойная отдельной статьи (которая уже находится в процессе написания). В целом же мы пока что остановились на игнорировании nullable аннотаций в нашем dataflow механизме (т.е. мы их понимаем, разбираем и пропускаем). Дело в том, что несмотря на non-nullable reference тип переменной, в неё всё равно можно вполне просто (или по ошибке) записать значение null, что может привести к NRE при разыменовании соответствующей ссылки. В таком случае наш анализатор может увидеть подобную ошибку и выдать предупреждение на использование потенциально нулевой ссылки (конечно, если он увидит такое присвоение в коде) несмотря на non-nullable reference тип переменной.

Хочу отметить, что использование nullable reference типов и сопутствующего синтаксиса открывает возможности написания очень интересного кода. Про себя мы назвали это 'эмоциональным синтаксисом'. Код, представленный ниже, вполне себе компилируется:

obj.Calculate();
obj?.Calculate();
obj.Calculate();
obj!?.Calculate();
obj!!!.Calculate();

Кстати, в ходе работы я нашёл пару способов 'завалить' Visual Studio, используя новый синтаксис. Дело в том, что вы можете не ограничивать количество символов одним, когда ставите '!'. То есть можно написать не только код вида:

object temp = null!

но и:

object temp = null!!!;

Можно извратиться, пойти дальше и написать так:

object temp = null!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!;

Этот код успешно компилируется. Но если запросить информацию о синтаксическом дереве, используя Syntax Visualizer из состава .NET Compiler Platform SDK, Visual Studio упадёт.

Из Event Viewer можно вытащить информацию о проблеме:

Faulting application name: devenv.exe,
version: 16.0.28803.352, time stamp: 0x5cc37012
Faulting module name: WindowsBase.ni.dll,
version: 4.8.3745.0, time stamp: 0x5c5bab63
Exception code: 0xc00000fd
Fault offset: 0x000c9af4
Faulting process id: 0x3274
Faulting application start time: 0x01d5095e7259362e
Faulting application path: C:\Program Files (x86)\
Microsoft Visual Studio\2019\Community\Common7\IDE\devenv.exe
Faulting module path: C:\WINDOWS\assembly\NativeImages_v4.0.30319_32\
WindowsBase\4480dfedf0d7b4329838f4bbf953027d\WindowsBase.ni.dll
Report Id: 66d41eb2-c658-486d-b417-02961d9c3e4f
Faulting package full name: 
Faulting package-relative application ID:

Если пойти дальше и увеличить количество восклицательных знаков в несколько раз, Visual Studio упадёт сама по себе - помощь Syntax Visualizer для этого уже не понадобится. Библиотеки Microsoft.CodeAnalysis и компилятор csc.exe также не переваривают этот код.

Безусловно, это примеры синтетические, но всё же данный факт показался мне забавным.

Toolset

Примечание. В который раз сталкиваюсь с проблемой перевода слова 'evaluation' в контексте разговора про MSBuild проекты. Перевод, показавшийся наиболее близким по смыслу и при этом нормально звучащим - "построение модели проекта". Если у вас есть альтернативные варианты перевода - можете написать мне, будет интересно почитать.

Было очевидно, что обновление toolset'a станет наиболее трудозатратной задачей. Точнее, так казалось изначально, однако теперь я склоняюсь к тому, что наиболее проблемной была всё же поддержка плагина. В частности, это было связано с уже существующим toolset'ом и механизмом построения модели проектов MSBuild, который успешно работал и сейчас, хоть и требовал расширения. Отсутствие необходимости писать алгоритмы с нуля сильно упростило задачу. Наша ставка на "свой" toolset, сделанная ещё на этапе поддержки Visual Studio 2017, в очередной раз оправдалась.

Традиционно всё начинается с обновления NuGet пакетов. На вкладке управления NuGet пакетами для решения есть кнопка 'Update'... пользоваться которой не выходит. При обновлении всех пакетов возникали множественные конфликты версий, и разрешать их все выглядело путём не очень верным. Более болезненный, но, кажется, и более надёжный путь - 'поштучное' обновление целевых пакетов Microsoft.Build / Microsoft.CodeAnalysis.

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

Напомню, что в ходе работы мы тестируем анализаторы (C#, C++, Java) на open source проектах. Это позволяет хорошо протестировать диагностические правила - найти, например, false positives, или получить идею того, какие ещё случаи не были рассмотрены (уменьшить количество false negatives). Эти тесты также хорошо помогают отслеживать возможную регрессию на начальном этапе обновления библиотек / toolset'а. И этот раз исключением не стал, так как всплыл ряд проблем.

Одной из проблем стало ухудшение поведения внутри библиотек CodeAnalysis. Конкретнее - на ряде проектов в коде библиотек стали возникать исключения при различных операциях - получении семантической информации, открытии проектов и т.п.

Внимательные читатели статьи про поддержку Visual Studio 2017 помнят, что в составе нашего дистрибутива есть заглушка - файл MSBuild.exe размером 0 байт.

В этот раз пришлось пойти дальше - теперь в составе дистрибутива также присутствуют пустые заглушки компиляторов - csc.exe, vbc.exe, VBCSCompiler.exe. Зачем? Путь к этому начался с анализа одного из проектов в тестовой базе, на котором появились 'диффы' отчётов - ряд предупреждений отсутствовал при использовании новой версии анализатора.

Проблема оказалась в символах условной компиляции - при анализе проекта с использованием новой версии анализатора часть символов извлекалась неправильно. Чтобы лучше понять, из-за чего возникла эта проблема, пришлось погрузиться в библиотеки Roslyn.

Для парсинга символов условной компиляции используется метод GetDefineConstantsSwitch класса Csc из библиотеки Microsoft.Build.Tasks.CodeAnalysis. Парсинг выполняется с использованием метода String.Split по ряду разделителей:

string[] allIdentifiers 
  = originalDefineConstants.Split(new char[] { ',', ';', ' ' });

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

Следующая ключевая точка - вызов метода ComputePathToTool класса ToolTask. Данный метод выстраивает путь до исполняемого файла (csc.exe) и проверяет его наличие. Если файл существует, путь до него и возвращается, иначе возвращается null.

Вызывающий код:

....
string pathToTool = ComputePathToTool();
if (pathToTool == null)
{
    // An appropriate error should have been logged already.
    return false;
}
....

Так как файла csc.exe нет (казалось бы - зачем он нам?), pathToTool на данном этапе имеет значение null, и текущий метод (ToolTask.Execute) завершает своё исполнение с результатом false. Как следствие - результаты работы таска, включая полученные символы условной компиляции, игнорируются.

Хорошо, посмотрим, что будет, если подложить файл csc.exe в ожидаемое место.

В таком случае pathToTool указывает на фактическое расположение существующего файла и исполнение метода ToolTask.Execute продолжается. Следующая ключевая точка - вызов метода ManagedCompiler.ExecuteTool. И начинается он следующим образом:

protected override int ExecuteTool(string pathToTool, 
                                   string responseFileCommands, 
                                   string commandLineCommands)
{
  if (ProvideCommandLineArgs)
  {
    CommandLineArgs = GetArguments(commandLineCommands, responseFileCommands)
      .Select(arg => new TaskItem(arg)).ToArray();
  }

  if (SkipCompilerExecution)
  {
    return 0;
  }
  ....
}

Свойство SkipCompilerExecution имеет значение true (логично, мы же не выполняем фактическую компиляцию). В итоге вызывающий метод (уже упоминавшийся ToolTask.Execute) проверяет, что код возврата метода ExecuteTool - 0, и, если это так, завершает своё исполнение со значением true. Что у вас там за csc.exe лежал - настоящий компилятор или 'Война и мир' Льва Толстого в текстовом изложении - неважно.

В итоге, основная проблема проистекает из того, что последовательность шагов определена в следующем порядке:

  • проверить существование компилятора;
  • проверить необходимость запуска компилятора;

а не наоборот. Заглушки компиляторов эту проблему успешно решают.

Хорошо, а как же получались символы успешной компиляции, если файла csc.exe не обнаруживалось (и результат работы таска игнорировался)?

Есть метод и на такой случай - CSharpCommandLineParser.ParseConditionalCompilationSymbols из библиотеки Microsoft.CodeAnalysis.CSharp. Парсинг также выполняется методом String.Split с рядом разделителей:

string[] values 
  = value.Split(new char[] { ';', ',' } /*, 
                StringSplitOptions.RemoveEmptyEntries*/);

Заметили разницу с набором разделителей из метода Csc.GetDefineConstantsSwitch? В данном случае пробельный символ не служит разделителем. Таким образом, если символы условной компиляции были записаны через пробел, данный метод разберёт их неправильно.

Такая ситуация и возникла на проблемных проектах - в них символы условной компиляции были записаны через пробел, и успешно парсились методом GetDefineConstantsSwitch, но не ParseConditionalCompilationSymbols.

Другой проблемой, которая обнаружила себя после обновления библиотек, стало ухудшение поведения в ряде случаев, в частности - на проектах, которые не собирались. Проблемы возникали в библиотеках Microsoft.CodeAnalysis и возвращались к нам в виде разных исключений - ArgumentNullException (какой-то внутренний логгер не проиницализировался), NullReferenceException и прочих.

Об одной из таких проблем хочу рассказать ниже - она мне показалась достаточно интересной.

С этой проблемой мы столкнулись при проверке свежей версии проекта Roslyn - из кода одной из библиотек выбрасывалось исключение NullReferenceException. За счёт достаточно подробной информации о месте возникновения проблемы мы быстро нашли проблемный код и ради интереса решили попробовать, повторится ли проблема при работе из Visual Studio.

Что ж - её удалось воспроизвести в Visual Studio (эксперимент проводился на Visual Studio 16.0.3). Для этого нам понадобится определение класса следующего вида:

class C1<T1, T2>
{
  void foo()
  {
    T1 val = default;
    if (val is null)
    { }
  }
}

Также нам понадобится Syntax Visualizer (входит в состав компонента .NET Compiler Platform SDK). Необходимо запросить TypeSymbol (пункт меню "View TypeSymbol (if any)") у узла синтаксического дерева типа ConstantPatternSyntax (null). После этого Visual Studio перезапустится, а в Event Viewer можно будет посмотреть информацию о проблеме, в частности - найти stack trace:

Application: devenv.exe
Framework Version: v4.0.30319
Description: The process was terminated due to an unhandled exception.
Exception Info: System.NullReferenceException
   at Microsoft.CodeAnalysis.CSharp.ConversionsBase.
        ClassifyImplicitBuiltInConversionSlow(
          Microsoft.CodeAnalysis.CSharp.Symbols.TypeSymbol, 
          Microsoft.CodeAnalysis.CSharp.Symbols.TypeSymbol, 
          System.Collections.Generic.HashSet'1
            <Microsoft.CodeAnalysis.DiagnosticInfo> ByRef)
   at Microsoft.CodeAnalysis.CSharp.ConversionsBase.ClassifyBuiltInConversion(
        Microsoft.CodeAnalysis.CSharp.Symbols.TypeSymbol, 
        Microsoft.CodeAnalysis.CSharp.Symbols.TypeSymbol, 
        System.Collections.Generic.HashSet'1
          <Microsoft.CodeAnalysis.DiagnosticInfo> ByRef)
   at Microsoft.CodeAnalysis.CSharp.CSharpSemanticModel.GetTypeInfoForNode(
        Microsoft.CodeAnalysis.CSharp.BoundNode,
        Microsoft.CodeAnalysis.CSharp.BoundNode,
        Microsoft.CodeAnalysis.CSharp.BoundNode)
   at Microsoft.CodeAnalysis.CSharp.MemberSemanticModel.GetTypeInfoWorker(
        Microsoft.CodeAnalysis.CSharp.CSharpSyntaxNode,
        System.Threading.CancellationToken)
   at Microsoft.CodeAnalysis.CSharp.SyntaxTreeSemanticModel.GetTypeInfoWorker(
        Microsoft.CodeAnalysis.CSharp.CSharpSyntaxNode,
        System.Threading.CancellationToken)
   at Microsoft.CodeAnalysis.CSharp.CSharpSemanticModel.GetTypeInfo(
        Microsoft.CodeAnalysis.CSharp.Syntax.PatternSyntax, 
        System.Threading.CancellationToken)
   at Microsoft.CodeAnalysis.CSharp.CSharpSemanticModel.GetTypeInfoFromNode(
        Microsoft.CodeAnalysis.SyntaxNode, System.Threading.CancellationToken)
   at Microsoft.CodeAnalysis.CSharp.CSharpSemanticModel.GetTypeInfoCore(
        Microsoft.CodeAnalysis.SyntaxNode, System.Threading.CancellationToken)
....

Как видим, причина проблемы – разыменование нулевой ссылки.

Как я упоминал ранее, с такой же проблемой мы столкнулись в ходе тестирования анализатора. Если использовать отладочные библиотеки Microsoft.CodeAnalysis для сборки анализатора, можно путём отладки прийти точно в проблемное место, запросив TypeSymbol у нужного узла синтаксического дерева.

В итоге приходим в упоминавшийся в приведённом выше stack trace метод ClassifyImplicitBuiltInConversionSlow:

private Conversion ClassifyImplicitBuiltInConversionSlow(
  TypeSymbol source,
  TypeSymbol destination,
  ref HashSet<DiagnosticInfo> useSiteDiagnostics)
{
  Debug.Assert((object)source != null);
  Debug.Assert((object)destination != null);

  if (source.SpecialType == SpecialType.System_Void ||
      destination.SpecialType == SpecialType.System_Void)
  {
    return Conversion.NoConversion;
  }

  Conversion conversion 
    = ClassifyStandardImplicitConversion(source, destination,
                                         ref useSiteDiagnostics);
  if (conversion.Exists)
  {
    return conversion;
  }

  return Conversion.NoConversion;
}

Проблема в том, что параметр destination в данном случае имеет значение null. Соответственно, при обращении к destination.SpecialType выбрасывается исключение NullReferenceException. Да, выше разыменования стоит выражение Debug.Assert, но этого недостаточно, так как по факту оно ни от чего не защищает - только помогает выявить проблему в отладочных версиях библиотек. Или не помогает.

Изменения построения модели C++ проектов

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

Первый - пришлось модифицировать алгоритмы, полагающиеся на то, что значение ToolsVersion будет записано в числовом формате. Не вдаваясь в подробности - есть несколько случаев, когда необходимо сравнивать toolset'ы и выбирать, например, более актуальную новую версию. Эта версия, соответственно, имела более высокое числовое значение. Был расчёт на то, что ToolsVersion, соответствующий новой версии MSBuild / Visual Studio, будет равен 16.0. Как бы не так... Ради интереса привожу таблицу, как изменялись значения различных свойств в разных версиях Visual Studio:

Visual Studio product name

Visual Studio version number

Tools Version

PlatformToolset version

Visual Studio 2010

10.0

4.0

100

Visual Studio 2012

11.0

4.0

110

Visual Studio 2013

12.0

12.0

120

Visual Studio 2015

14.0

14.0

140

Visual Studio 2017

15.0

15.0

141

Visual Studio 2019

16.0

Current

142

Шутка, конечно, устаревшая, но нельзя не вспомнить про изменение версий Windows и Xbox, чтобы понять, что предсказание будущих значений (неважно, чего именно - именования, версий), в случае Microsoft - вещь зыбкая. :)

Решение было достаточной простым - введение приоритезации toolset'ов (выделение отдельной сущности приоритета).

Второй момент - проблемы при работе в Visual Studio 2017 или смежном окружении (например, наличии переменной окружения VisualStudioVersion). Дело в том, что вычисление параметров, необходимых для построения модели C++ проекта - куда более сложное, чем для построения модели .NET проекта. В случае с .NET мы используем собственный toolset и соответствующее ему значение ToolsVersion. В случае с C++ можем отталкиваться как от своего, так и от существующих в системе toolset'ов. Начиная с Build Tools в составе Visual Studio 2017 toolset'ы прописываются в файле MSBuild.exe.config, а не в реестре. Соответственно, мы не можем достать их из общего списка toolset'ов (например, через Microsoft.Build.Evaluation.ProjectCollection.GlobalProjectCollection.Toolsets), в отличии от тех toolset'ов, которые записаны в реестре (соответствующие <= Visual Studio 2015).

Как следствие всего вышесказанного - не получится построить модель проекта, используя ToolsVersion 15.0, так как система не будет видеть необходимого toolset'a. Наиболее актуальный toolset - Current - при этом будет доступен, так как это наш собственный toolset, следовательно, для Visual Studio 2019 такой проблемы нет. Решение оказалось простым и позволило решить проблему, не меняя существующих алгоритмов построения модели проекта – добавление в список собственных toolset'ов помимо Current ещё одного - 15.0.

Изменения построения модели C# .NET Core проектов

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

  • после добавления 'Current' toolset'a перестал работать анализ .NET Core проектов под Visual Studio 2017;
  • не работал анализ .NET Core проектов на системе, где не установлена хотя бы одна версия Visual Studio.

Проблема в обоих случаях была одинаковой - одни из базовых .targets / .props файлов искались не по тем путям. Это приводило к тому, что не удавалось построить модель проекта с использованием нашего toolset'а.

При отсутствии Visual Studio можно было увидеть такую ошибку (с предыдущей версией toolset'a – 15.0):

The imported project "C:\Windows\Microsoft.NET\Framework64\
15.0\Microsoft.Common.props" was not found.

При построении модели C# .NET Core проекта в Visual Studio 2017 можно было увидеть следующую проблему (с актуальной версией toolset'а - Current):

The imported project 
"C:\Program Files (x86)\Microsoft Visual Studio\
2017\Community\MSBuild\Current\Microsoft.Common.props" was not found. 
....

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

Ниже я описываю, как были решены эта проблемы, не вдаваясь в технические подробности. Эти самые подробности (про построение моделей C# .NET Core проектов, а также изменение построения моделей в нашем toolset'e) ждите в одной из наших будущих статей. Кстати, если вы внимательно читали текст выше, то могли заметить, что это уже вторая отсылка на будущие статьи. :)

Итак, как же мы решили эту проблему? Решением стало расширение нашего собственного toolset'a за счёт основных .targets / .props файлов из .NET Core SDK (Sdk.props, Sdk.targets). Это позволило иметь больший контроль над ситуацией, большую гибкость в управлении импортами, а также в построении модели .NET Core проектов в целом. Да, наш toolset опять немного вырос, а также пришлось добавить логику по выставлению необходимого для построения модели .NET Core проектов окружения, но похоже, что оно того стоило.

Раньше принцип работы при построении модели .NET Core проектов был следующим: мы просто запрашивали это построение, а дальше всё работало за счёт средств MSBuild.

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

  • подготовка окружения, необходимого для построения модели .NET Core проектов;
  • построение модели:
    • начало построения с использованием .targets / .props файлов из нашего toolset'a;
    • продолжение построения с использованием внешних файлов.

Из описанных выше этапов очевидно, что выставление необходимого окружения преследует две основные цели:

  • инициировать построение модели с использованием .targets / .props файлов из собственного toolset'a;
  • перенаправить дальнейшие операции на внешние .targets / .props файлы.

Для поиска .targets / .props файлов, необходимых для построения модели .NET Core проектов, используется специальная библиотека - Microsoft.DotNet.MSBuildSdkResolver. Инициация построения с использованием файлов из нашего toolset'a была решена за счёт использования специальной переменной окружения, используемой этой библиотекой - мы подсказываем, откуда нужно импортировать необходимые файлы (из нашего toolset'а). Так как библиотека идёт в составе нашего дистрибутива, опасений за то, что логика внезапно поменяется и перестанет работать, нет.

Теперь первыми импортируются Sdk файлы из нашего toolset'а, а так как мы их можем спокойно менять, управление дальнейшей логикой построения модели переходит в наши руки. Следовательно, мы можем сами определять, какие файлы необходимо импортировать и откуда. Это касается и упомянутого выше Microsoft.Common.props. Этот и другие базовые файлы мы импортируем из собственного toolset'а с уверенностью в их наличии и содержимом.

После этого, выполнив необходимые импорты и выставив ряд свойств, мы передаём дальнейшее управление построением модели в фактический .NET Core SDK, где и происходят остальные необходимые действия.

Заключение

В целом поддержка Visual Studio 2019 пошла легче, чем поддержка Visual Studio 2017, что, как я вижу, обусловлено несколькими факторами. Первый - Microsoft не изменяли такого количества вещей, как между Visual Studio 2015 и Visual Studio 2017. Да, поменяли основной toolset'a, стали ориентировать плагины для Visual Studio на асинхронность, но тем не менее. Второй - у нас уже было готово решение с собственным toolset'ом и построением моделей проектов - не было нужды изобретать всё сначала, достаточно было только расширить существующее решение. Относительно простая поддержка анализа .NET Core проектов для новых условий (а также для случаев анализа на машине, где отсутствуют установленные экземпляры Visual Studio) за счёт расширения нашей системы построения моделей проектов также дарит надежду на то, что мы сделали правильный выбор, решив взять часть контроля в свои руки.

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