Код, понятный IDE и AI: использование атрибутов из JetBrains.Annotations

Код, понятный IDE и AI: использование атрибутов из JetBrains.Annotations

У JetBrains есть фреймворк JetBrains.Annotations для .NET, который предоставляет набор полезных атрибутов. Они выступают дополнительными метаданными как для самих разработчиков, так и для статического анализатора JB, который включён в их IDE и ReSharper.

JetBrains.Annotations доступен в nuget, но может ограниченно работать вне продуктов JetBrains. Тем не менее в System.Diagnostics.CodeAnalysis тоже есть набор стандартных полезных и похожих атрибутов.

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

В текущих реалиях у этих атрибутов появилось ещё одно полезное свойство – они учитываются нейросетью и помогают ей быстрее ориентироваться в проекте. И, как мне показалось, автокомплит тоже становится более точным (про инструменты, доступные в Rider, делал отдельную, ещё не сильно устаревшую, подборку).

Пример использования атрибутов в поиске через AI
Пример использования атрибутов в поиске через AI

Важно отметить, что атрибуты — это лишь мета-информация для анализатора, которая не влияет на runtime.

Например, использование атрибута [NotNull] для аргумента метода не добавляет автоматической проверки. В метод всё ещё можно будет передать null. И если в методе этот кейс не будет обработан, то случится NullReferenceException. Атрибут лишь подскажет IDE, что в этот метод не нужно позволять передавать null на этапе разработки.

Пример подсказок от IDE
Пример подсказок от IDE

Также важно отметить, что этот фреймворк развивается давно, а языки программирования не стоят на месте. Поэтому какие-то атрибуты для определённых версий языка могут оказаться избыточными. Так, например, появление nullable reference type в C# 8 позволяет выразить намерения по null-спецификациям средствами самого языка.

Про этот кейс, атрибуты от JB и MS, операторы ? и !, когда что и для чего использовать, можно подробнее почитать в этом лонгриде с Хабра.

Ещё один нюанс связан с тем, что для Unity-проектов атрибуты Jetbrains поддерживаются по умолчанию, но не в полном объёме. Так, например, не удастся применить [NonNegativeValue] или [ValueRange]. В теории, можно выкачать более актуальный Jetbrains.Annotations из nuget и добавить в проект. Но лично у меня не было необходимости этим заниматься.

Примеры атрибутов

Всем, кто работает с инструментами JB, рекомендую ознакомиться с полным списком атрибутов. А здесь оставлю наиболее часто встречаемые и работающие в Unity.

⚠ Дисклеймер ⚠

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

// ------------------------------------------------------------------- // 1. Анализ на Null // ------------------------------------------------------------------- // Возвращаемое значение может быть null. [CanBeNull] public User FindUser(int id) { return _users.TryGetValue(id, out var user) ? user : null; } // Параметр не может быть null. public void LogMessage([NotNull] string message) { // Полезно для FailFast в runtime. if (message == null) throw new ArgumentNullException(nameof(message)); Console.WriteLine(message); } // Коллекция не может быть null, но могут быть null её элементы. [NotNull, ItemCanBeNull] public List<string> GetUserNamesWithNulls() { return new List<string> { "Alice", null, "Bob" }; } // Коллекция и её элементы не могут быть null. public void ProcessUserNames([NotNull, ItemNotNull] IEnumerable<string> names) { foreach (var name in names) Console.WriteLine(name.ToUpper()); }
// ------------------------------------------------------------------- // 2. Контракты Параметров // ------------------------------------------------------------------- // Если 'obj' равен null, метод останавливает выполнение. [ContractAnnotation("obj:null => halt")] public void GuardNotNull(object obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); } // Если 's' равен null, метод вернет true. [ContractAnnotation("s:null => true")] public bool IsNullOrEmpty(string s) { return s == null || s.Length == 0; } // Если 'input' не null, результат тоже не null. [ContractAnnotation("input:notnull => notnull")] public string Decorate(string input) { return input == null ? null : quot;--{input}--"; } // IDE предлагает значения из списка констант. public void FindControl([ValueProvider("UiConstants")] string id) { // ... } public static class UiConstants { public const string PanelId = "mainPanel"; public const string ButtonId = "okButton"; }
// ------------------------------------------------------------------- // 3. Поведение Методов // ------------------------------------------------------------------- // Метод не имеет побочных эффектов. // Его вызов без использования результата бессмыслен. [Pure] public int CalculateSum(int a, int b) { return a + b; } // Метод может иметь побочные эффекты. // Но его вызов без использования результата бессмыслен. [MustUseReturnValue] public string GetAndRemoveFirst(Queue<string> queue) { return queue.Dequeue(); // побочный эффект: меняет очередь } // Помечают метод как Assert-метод для параметров. [AssertionMethod] public void MyAssert( [AssertionCondition(AssertionConditionType.IS_TRUE)] bool condition) { if (!condition) throw new InvalidOperationException("Assertion failed"); } // Метод безусловно завершает программу. // Устаревший. Аналог: [ContractAnnotation("=> halt")] [TerminatesProgram] public void FatalError(string msg) { Console.Error.WriteLine(msg); Environment.Exit(1); } // Вызывающий код обязан вызвать Dispose. [MustDisposeResource] public System.IO.FileStream OpenFile(string path) { return new System.IO.FileStream(path, System.IO.FileMode.Open); }
// ------------------------------------------------------------------- // 4. Работа со Строками // ------------------------------------------------------------------- // 'format' - это строка формата. [StringFormatMethod("format")] public void Log(string format, params object[] args) { Console.WriteLine(string.Format(format, args)); } // Строка является паттерном регулярного выражения. public void FindMatches([RegexPattern] string regexPattern, string text) { var matches = Regex.Matches(text, regexPattern); Console.WriteLine(quot;Found {matches.Count}"); } // Строка является ссылкой на путь (файл/папка). // IDE включит проверку пути и автодополнение. public void LoadConfig([PathReference] string configPath) { // ... } // Строка должна быть локализована. public void SetWindowTitle([LocalizationRequired] string title) { Console.Title = title; } // Параметр должен быть именем одного из параметров вызывающего метода. public void GuardNotNull(object arg, [InvokerParameterName] string paramName) { if (arg == null) throw new ArgumentNullException(nameof(paramName)); }
// ------------------------------------------------------------------- // 5. Управление Коллекциями // ------------------------------------------------------------------- // Как метод, конструктор или свойство влияет на коллекцию внутри. // - None: никак не влияет; // - Read: только читает; // - ModifyExistingContent: изменяет элементы коллекции; // - UpdatedContent: изменяет состав коллекции. [CollectionAccess(CollectionAccessType.Read)] public void ReadOnlyAccess(IReadOnlyList<int> list) { foreach (var item in list) Console.WriteLine(item); } // IEnumerable не будет перечислен (MoveNext, foreach, Linq, ...) public void CheckCountFast([NoEnumeration] IEnumerable items) { if (items is ICollection collection) // без перечисления Console.WriteLine(collection.Count); } // IEnumerable будет обработан немедленно (NoLazy). public void ProcessItemsImmediately([InstantHandle] IEnumerable items) { foreach (var item in items) { // ... } } // Метод является частью цеопчки Linq-вызовов. [LinqTunnel] public static T AndLog<T>(this T obj) { Console.WriteLine(obj); return obj; }
// ------------------------------------------------------------------- // 6. Контроль использования // ------------------------------------------------------------------- // Подавляет "unused" при неявном использовании // (рефлексия, DI, сериализация, ...) [UsedImplicitly] public class MyApiController { // ... } // Помечает кастомный атрибут как [UsedImplicitly]. [MeansImplicitUse(ImplicitUseTargetFlags.WithMembers)] [AttributeUsage(AttributeTargets.Class)] public class CustomAttribute : Attribute { // ... } // Класс или его методы являются публичным API. // Подавляет "unsed". [PublicAPI] public class MyLibraryFacade { // ... }
2
Начать дискуссию