Релиз .NET 6 вышел достаточно громким – если вы пишете под эту платформу, то навряд ли могли пропустить такое событие. Мы тоже не смогли пройти мимо и решили проверить, что интересного удастся найти в исходном коде библиотек .NET. Исходники я взял из ветки релиза .NET 6 на GitHub. В статье приводятся подозрительные места только из состава библиотек (то, что лежит в src/libraries). Сам рантайм не анализировал – может быть, в следующий раз. :) Код проверял с помощью статического анализатора PVS-Studio. Кстати, как вы могли догадаться из этой статьи, PVS-Studio 7.16 поддерживает анализ проектов под .NET 6. О том, что ещё нового появилось в этом релизе, можно почитать здесь. К слову, сам анализатор PVS-Studio C#, если говорить про версии под Linux и macOS, теперь тоже работает под .NET 6. К слову, кроме поддержки .NET 6 в C# анализаторе за год появилось много нового: были добавлены security-диагностики, плагин для Visual Studio 2022, сам анализатор стал работать значительно быстрее на крупных проектах. Но вы же пришли сюда почитать про .NET 6, верно? Что ж, тогда не будем терять времени. В этом разделе я собрал различные интересные места, которые не смог объединить какой-то общей категорией. Issue 1 Начнём с чего-нибудь попроще. Предупреждение PVS-Studio: V3139 Two or more case-branches perform the same actions. ZipPackage.cs 402 Фактически данный метод осуществляет маппинг с CompressionOption на CompressionLevel. Подозрительно здесь то, что значения CompressionOption.Normal и CompressionOption.Maximum маппятся на значение CompressionLevel.Optimal.  Возможно, CompressionOption.Maximum должен соответствовать CompressionLevel.SmallestSize. Issue 2 Теперь давайте немного попрактикуемся. Для экспериментов возьмём тип System.Text.Json.Nodes.JsonObject. При желании вы можете повторить описанные операции, если возьмёте релизную версию .NET 6 SDK. У типа JsonObject есть 2 конструктора – один принимает только опции, другой – свойства и опции. В принципе, понятно, чего от них ждать. Документация доступна здесь. Создадим два экземпляра типа JsonObject, использовав каждый из конструкторов. Теперь проверим состояние созданных объектов.  Если состояние jsonObject1 ожидаемое, то, почему в поле _options объекта jsonObject2 записано значение null, не очень понятно. Что ж, заглянем в исходный код и посмотрим на эти конструкторы. Во втором конструкторе параметр options попросту затерялся – он никуда не передаётся и никак не используется. В то время как в первом конструкторе options передаются в конструктор базового класса, где и записываются в поле: Соответствующее предупреждение PVS-Studio: V3117 Constructor parameter 'options' is not used. JsonObject.cs 35 Issue 3 Кстати, если говорить про забытые параметры, нашлось ещё одно интересное место. Предупреждение PVS-Studio: V3117 Constructor parameter 'additionalCapacity' is not used. ServiceNameCollection.cs 46 Как видно из кода, параметр additionalCapacity последнего конструктора проверяется в Debug.Assert, но больше ни для чего не используется. Выглядит подозрительно. Особенно забавно, что этот параметр вполне себе используется другими конструкторами, которые передают какие-то значения для additionalCapacity. Issue 4 Тест на способность предвидения (упс, спойлеры). Попробуйте угадать, что насторожило анализатор в следующем коде. Казалось бы, просто выбрасывается исключение. Чтобы понять, что здесь не так, нужно посмотреть на конструктор XsltException. Если сопоставить порядок аргументов и параметров, становится понятно, что насторожило анализатор. Похоже, что позиция и номер строки перепутаны местами. Порядок аргументов: Порядок параметров: Предупреждение PVS-Studio: V3066 Possible incorrect order of arguments passed to 'XsltException' constructor: '_linePosition' and '_lineNumber'. Compiler.cs 1187 Issue 5 Ниже привожу достаточно объёмный фрагмент кода. Наверняка там запряталась какая-то опечатка… Не хотите попробовать её найти? Ну что, как успехи? А может здесь и вовсе нет никакой опечатки?  Давайте для начала взглянем на предупреждение анализатора: V3080 Possible null dereference of method return value. Consider inspecting: Resolve(...). JsonSourceGenerator.Parser.cs 203 Метод Resolve может вернуть значение null. Об этом говорит в том числе и его сигнатура. Об этом же предупреждает и PVS-Studio, межпроцедурно обнаружив возможность возврата значения null. Спускаемся ниже, в другую перегрузку Resolve. Обратите внимание, что typeSymbol записан как допускающий null-значение: INamedTypeSymbol?. Опускаемся ниже – в метод AsType. Как видно, если первый аргумент – нулевая ссылка, то и из метода возвращается значение null. А теперь вернёмся в конструктор типа Parser. В нём обычно результат вызова метода Resolve просто записывается в какое-то поле. Но обнаружилось исключение, о котором и предупреждает PVS-Studio: Здесь для результата вызова метода Resolve будет вызван экземплярный метод MakeArrayType. Как следствие, если Resolve вернёт null, возникнет исключение NullReferenceException. Issue 6 Предупреждение PVS-Studio: V3079 'ThreadStatic' attribute is applied to a non-static 'ts_tags' field and will be ignored Instrument.netfx.cs 20 Процитируем документацию: Note that in addition to applying the ThreadStaticAttribute attribute to a field, you must also define it as a static field (in C#) or a Shared field (in Visual Basic). Как видно из кода, поле ts_tags – экземплярное. Следовательно, вешать на него атрибут ThreadStatic нет смысла. Или же здесь творится какая-то чёрная магия…  Issue 7 Предупреждение PVS-Studio: V3146 Possible null dereference of 'propertyValueNode'. The 'childNodes.ElementAtOrDefault' can return default null value. JsonSourceGenerator.Parser.cs 560 Если в коллекции childNodes меньше двух элементов, вызов ElementAtOrDefault вернёт значение default(SyntaxNode) (то есть null, так как SyntaxNode – класс). В таком случае на следующей строке будет выброшено исключение NullReferenceException. Особенно странно, что propertyValueNode – nullable reference type, однако разыменовывается без проверки. Возможно, здесь есть какой-то неявный контракт на то, что в childNodes всегда больше одного элемента. Например, что если есть propertyNameNode, то будет и propertyValueNode. В таком случае можно было бы использовать вызов метода ElementAt, чтобы не было лишних вопросов. Issue 8 Есть такая структура – Microsoft.Extensions.FileSystemGlobbing.FilePatternMatch. Она, что логично, переопределяет метод Equals(Object). Документация с описанием метода.  Допустим, у нас есть такой код, который вызывает этот метод: Как вы думаете, что произойдёт, если FPM_Test вызвать со значением null? В переменную eq будет записано значение false? Ну, почти.  Похожая история будет, если мы передадим в качестве аргумента экземпляр типа, отличного от FilePatternMatch. Например… Пусть будет массив какой-нибудь.  Уже догадались, почему так происходит? Дело в том, что в методе Equals аргумент никак не проверяется ни на null-значение, ни на совместимость типов, а просто безусловно распаковывается: Предупреждение PVS-Studio: V3115 Passing 'null' to 'Equals' method should not result in 'NullReferenceException'. FilePatternMatch.cs 61 Конечно, если судить по документации, никто не обещал нам, что Equals(Object) будет возвращать false, если принимает не FilePatternMatch. Но, наверное, это было бы наиболее ожидаемым поведением. С дублирующимися проверками есть интересный момент: со стороны не всегда может быть понятно — просто это избыточный код или вместо одной из них должно быть что-то иное. Так или иначе, предлагаю взглянуть на несколько примеров. Issue 9 Предупреждение PVS-Studio: V3021 There are two 'if' statements with identical conditional expressions. The first 'if' statement contains method return. This means that the second 'if' statement is senseless DeflateManagedStream.cs 27 В начале метода идёт ряд проверок. Но, вот незадача, одна из них (!stream.CanRead) полностью продублирована (как условие, так и then-ветвь оператора if). Issue 10 Предупреждение PVS-Studio: V3021 There are two 'if' statements with identical conditional expressions. The first 'if' statement contains method return. This means that the second 'if' statement is senseless JsonSerializer.Read.String.cs 163 Ага, похожая ситуация, но уже совсем в другом месте. Перед использованием проверяют параметр returnType на null. Дело хорошее, вот только делают это 2 раза.  Issue 11 Предупреждение PVS-Studio: V3021 There are two 'if' statements with identical conditional expressions. The first 'if' statement contains method return. This means that the second 'if' statement is senseless XmlSerializationWriterILGen.cs 102 Здесь ситуация немного интереснее. Если предыдущие дублирующиеся проверки следовали одна за другой, то здесь они расположены на разных концах метода – почти в 20 строках друг от друга. Однако проверяемая локальная переменная hasDefault за это время не изменяется. Соответственно, либо исключение будет выброшено уже в первый раз, либо так и не будет выброшено вовсе. Issue 12 Предупреждение PVS-Studio: V3022 Expression 'fk.DeleteRule != Rule.Cascade' is always false. xmlsaver.cs 1708 Вопрос традиционный – нужно было проверять другое значение или это просто избыточный код? Сначала посмотрим на пару найденных предупреждений, а затем я расскажу небольшую историю. Issue 13 Предупреждение PVS-Studio: V3138 String literal contains potential interpolated expression. Consider inspecting: _pressureHigh. PhysicalMemoryMonitor.cs 110 Очень похоже, что здесь хотели залогировать поля _pressureHigh и _pressureLow. Однако подстановка значений не сработает, так как строка не интерполирована. Зато символ интерполяции стоит на первом аргументе метода Dbg.Trace, где подставлять нечего. :) Issue 14 Предупреждение PVS-Studio: V3138 String literal contains potential interpolated expression. Consider inspecting: spec. MetricsEventSource.cs 381 В коде пытаются распарсить строку specString. Если не получается – нужно залогировать исходную строку, если получается – залогировать результат (переменная spec) и выполнить ещё какие-то операции. Проблема опять в том, что и в первом, и во втором случае пропущен символ интерполяции. Как следствие – значения переменных specString и spec подставлены не будут. А теперь обещанная история. Ранее я уже писал о том, что в 2019 году проверял библиотеки .NET Core. Тогда тоже нашлись несколько строк, которые должны были быть интерполированными, но не были таковыми из-за пропущенного символа '$'. Соответствующие предупреждения в той статье описаны как issue 10 и issue 11. Я отписал баг-репорт на GitHub, после чего команда разработки .NET поправила некоторые описанные в статье места. В их числе – ошибки с интерполированными строками. Соответствующий pull request. Более того – в issue-трекере Roslyn Analyzers была открыта задача по разработке новой диагностики, которая отлавливала бы подобные кейсы.  Немного более подробно вся эта история изложена здесь. Возвращаемся в текущее время. Я всё это знал и помнил, поэтому очень удивился, когда вновь наткнулся на ошибки с пропущенной интерполяцией. Как же так? Ведь уже "из коробки" должна быть доступна соответствующая диагностика, которая помогла бы избежать этих ошибок. Я решил проверить тот issue по разработке диагностики от 15 августа 2019 и оказалось… что диагностика-то ещё не готова. Вот и ответ на вопрос, откуда снова ошибки с интерполяцией. PVS-Studio обнаруживает подобные проблемы с релиза 7.03 (25 июня 2019) – пользуйтесь. ;) В ходе проверки я несколько раз натыкался на предупреждения, показавшиеся мне смутно знакомыми. Оказалось, что я уже описывал их в прошлый раз. Раз они так и остались в коде, предположу, что это были не ошибки.  Например, приведённый ниже код, похоже, действительно необычный способ выбросить ArgumentOutOfRangeException. Это issue 30 из прошлой проверки. Однако у меня есть несколько вопросов по поводу других фрагментов, уже обнаруженных ранее. Например, issue 25. В цикле обходят коллекцию seq, но постоянно обращаются только к её первому элементу – seq[0]. Это выглядит… необычно. Предупреждение PVS-Studio: V3102 Suspicious access to element of 'seq' object by a constant index inside a loop. XmlQueryRuntime.cs 729 Меня такой код немного сбивает с толку, а вас? Или вот возьмём issue 34. Предупреждение PVS-Studio: V3009 It's odd that this method always returns one and the same value of 'true'. MaskedTextProvider.cs 1531 Метод как раньше всегда возвращал true, так и сейчас. При этом в комментарии всё также указано, что метод может вернуть и false: Returns true on success, false otherwise. С документацией та же история. Следующий пример, который также был описан ещё в предыдущей статье, я даже вынесу в отдельный раздел. Мы немного порассуждаем не только над самим фрагментом кода, но и над используемой в нём фишкой – nullable reference types. В общем и целом, я пока ещё не понял, нравятся мне ссылочные типы, допускающие null, или нет.  С одной стороны – огромное их преимущество в более информативной сигнатуре методов. Одного взгляда достаточно, чтобы понять, может ли метод возвращать null, может ли определённый параметр иметь null-значение и т.п.  С другой стороны – всё это строится на доверии. Никто не запрещает написать код вот так: Да-да-да, это синтетика, но ведь можно так сделать! Если такой код написан у вас внутри компании – идём (условно) к автору GetStr и проводим беседу. Однако, если GetStr подтягивается из какой-то библиотеки, исходников которой у вас нет, сюрприз будет не то чтобы очень приятным. Вернёмся от синтетических примеров к нашей основной теме – .NET 6. И с ним есть нюансы. Например, разные библиотеки разбиты по разным solution. И я, просматривая их, не раз задавался вопросом – а включен ли nullable context в данном проекте? А вот то, что здесь нет проверки на null – это ожидаемо или нет? Наверное, это не проблема при работе в рамках одного проекта, но при беглом анализе всех создаёт определённые трудности. Дальше – больше. Разного рода штуки начинаются, когда происходит переход на nullable context. Вроде переменная и не может иметь значения null, а вроде и проверка есть. И в .NET таких мест, скажем прямо, немало. Давайте я покажу несколько. Предупреждение PVS-Studio: V3095 The 'schemaAttribute' object was used before it was verified against null. Check lines: 438, 439. DocumentSchemaValidator.cs 438 Символ '!' намекает, что здесь мы работаем с nullable-контекстом. Окей. 1. Почему для приведения используется оператор 'as', а не прямой каст? Если есть уверенность в том, что schemaAttribute не null (я так читаю неявный контракт с '!'), значит, _defaultAttributes[i] точно имеет тип XmlSchemaAttribute. Ну допустим, такой синтаксис нравится больше – ладно. 2. Если schemaAttribute не null, зачем ниже в Debug.Assert проверка на null? 3. Если проверка актуальна и schemaAttribute всё же может иметь значение null (вопреки семантике nullable reference types), то до Debug.Assert исполнение не дойдёт, так как будет выброшено исключение при обращении к schemaAttribute.QualifiedName. Такой небольшой фрагмент кода лично у меня порождает сразу кучу вопросов.  Похожая история: С одной стороны, newCurrent.Left может иметь значение null, так как в него записывается результат выполнения оператора ?. (originalCurrent.Left?.ShallowClone()). С другой стороны, в последней строке мы видим аннотацию, что newCurrent.Left не null. А теперь тот фрагмент кода из .NET 6, ради которого я этот раздел, по факту, и начал писать. Реализация IStructuralEquatable.Equals(object? other, IEqualityComparer comparer) в типе ImmutableArray<T>. Если взглянуть на последние строчки кода в Visual Studio, редактор услужливо подскажет, что ours не null. Оно и видно из кода – self.array также размечен как не хранящий null-значения.  Хорошо, давайте напишем такой код: Запускаем его на исполнение и видим NullReferenceException.  Уууупс. Похоже, что переменная ours, которая не null, по факту всё-таки оказалось нулевой ссылкой. Давайте разберём, как так получилось: Здесь можно контраргументировать тем, что иммутабельный массив находится в некорректном состоянии, так как был создан не через специальные методы/свойства, а через вызов оператора default. Но получить NRE на вызове Equals для такого объекта всё равно немного странно.  Однако дело даже не в этом. Из кода, аннотаций и подсказок следует, что ours не null. По факту же переменная имеет значение null. Лично для меня это немного подрывает доверие к nullable reference types. PVS-Studio выдаёт здесь предупреждение: V3125 The 'ours' object was used after it was verified against null. Check lines: 1144, 1136. ImmutableArray_1.cs 1144 Кстати, об этой проблеме я писал в прошлой статье (issue 53). Тогда, правда, здесь ещё не было nullable-разметки. Примечание. Возвращаясь к разговору про операции над экземплярами ImmutableArray<T> в дефолтном состоянии, в некоторых методах/свойствах используются специальные методы: ThrowNullRefIfNotInitialized и ThrowInvalidOperationIfNotInitialized, которые сообщают о непроинициализированном состоянии объекта. Причём в явных реализациях интерфейсных методов используется ThrowInvalidOperationIfNotInitialized. Возможно, он должен был использоваться и в описанном выше случае. Тут хочу обратиться к читателям – а какой у вас опыт работы с nullable reference types? Нравится, не нравится? Внедряли ли на своих проектах? Что пошло нормально, какие трудности были? Интересен ваш опыт. Кстати, про nullable reference types уже была пара статей от моих коллег: раз, два. Время идёт, но вопрос всё ещё остаётся дискуссионным. В заключение я хотел бы ещё раз поздравить команду разработки .NET 6 с релизом, а также сказать спасибо всем тем, кто вносит вклад в развитие этого проекта. Уверен, что недочёты будут исправлены, а впереди ещё много свершений. Надеюсь также, что мне удалось ещё раз напомнить про пользу статического анализа в процессе разработки. Если интересно – можете попробовать PVS-Studio и на своём проекте. Кстати, при переходе по этой ссылке будет доступна расширенная лицензия, которая действует 30 дней, а не 7. Это ли не повод? ;) И по доброй традиции приглашаю подписываться на мой Twitter, чтобы не пропустить ничего интересного. Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Sergey Vasiliev. Errors and suspicious code fragments in .NET 6 sources. Независимый эксперт