Публикация источников проектов Microsoft - хороший повод провести их анализ. Этот раз не стал исключением, и сегодня мы рассмотрим подозрительные места, обнаруженные в коде Infer.NET. Долой резюме - переходите к делу!

Кратко о проекте и анализаторе

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

Проект был проверен статическим анализатором кода PVS-Studio 6.26. Напомню, что PVS-Studio ищет ошибки в коде C \ C ++ \ C # (а вскоре и Java) под Windows, Linux, macOS. Код C # пока анализируется только под Windows. Вы можете скачать и попробовать анализатор на своем проекте.

Сама проверка была довольно простой и беспроблемной. Перед проверкой я скачал исходники проекта с GitHub, восстановил необходимые пакеты (зависимости) и убедился, что проект был успешно собран. Это необходимо для того, чтобы анализатор имел доступ ко всей необходимой информации для проведения полноценного анализа. После сборки в пару кликов я запустил анализ решения через плагин PVS-Studio для Visual Studio.

Кстати, это не первый проект от Microsoft, проверенный с помощью PVS-Studio - были и другие: Roslyn, MSBuild, PowerShell, CoreFX и другие.

Примечание. Если вас или ваших друзей интересует анализ кода Java, вы можете написать в нашу службу поддержки, выбрав Я хочу проанализировать Java. Публичной бета-версии пока нет, но она скоро появится. Кто-то в секретной лаборатории (по соседству) над этим активно работают.

Тем не менее, хватит философских разговоров - давайте посмотрим на проблемы в коде.

Это ошибка или особенность?

Предлагаю найти ошибку самостоятельно - это вполне возможная задача. Обещаю, что никаких ожогов не будет в соответствии с тем, что было в статье 10 главных ошибок в проектах C ++ 2017 года. Итак, не торопитесь, чтобы прочитать предупреждение анализатора, которое выдается после фрагмента кода.

private void MergeParallelTransitions()
{
  ....
  if (   transition1.DestinationStateIndex == 
         transition2.DestinationStateIndex 
      && transition1.Group == 
         transition2.Group) 
  {
    if (transition1.IsEpsilon && transition2.IsEpsilon)
    {
      ....
    }
    else if (!transition1.IsEpsilon && !transition2.IsEpsilon) 
    {
      ....
      if (double.IsInfinity(transition1.Weight.Value) &&    
          double.IsInfinity(transition1.Weight.Value))
      {
        newElementDistribution.SetToSum(
          1.0, transition1.ElementDistribution,
          1.0, transition2.ElementDistribution);
      }
      else
      { 
        newElementDistribution.SetToSum(
          transition1.Weight.Value, transition1.ElementDistribution,
          transition2.Weight.Value, transition2.ElementDistribution);
      }
  ....
}

Предупреждение PVS-Studio: V3001 Слева и справа от оператора && есть идентичные подвыражения double.IsInfinity (transition1.Weight.Value). Среда выполнения Automaton.Simplification.cs 479

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

Так случилось при проверке чисел на бесконечность (double.IsInfinity). Из-за ошибки дважды проверялось значение одной и той же переменной transition1.Weight.Value. Переменная transition2.Weight.Value во втором подвыражении должна была стать проверяемым значением.

Еще один похожий подозрительный код.

internal MethodBase ToMethodInternal(IMethodReference imr)
{
  ....
  bf |=   BindingFlags.Public 
        | BindingFlags.NonPublic 
        | BindingFlags.Public
        | BindingFlags.Instance;
  ....
}

Предупреждение PVS-Studio: V3001 Слева и справа от оператора | есть идентичные подвыражения BindingFlags.Public. Компилятор CodeBuilder.cs 194

При формировании значения переменной bf перечислитель BindingFlags.Public используется дважды. Либо этот код содержит операцию установки избыточного флага, либо вместо второго использования BindingFlags.Public здесь должен иметь место другой перечислитель.

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

Давайте двигаться дальше. Я цитирую все тело метода и предлагаю снова найти ошибку (или ошибки) самостоятельно.

private void ForEachPrefix(IExpression expr,
                           Action<IExpression> action)
{
  // This method must be kept consistent with GetTargets.
  if (expr is IArrayIndexerExpression)
    ForEachPrefix(((IArrayIndexerExpression)expr).Target,
                  action);
  else if (expr is IAddressOutExpression)
    ForEachPrefix(((IAddressOutExpression)expr).Expression,
                  action);
  else if (expr is IPropertyReferenceExpression)
    ForEachPrefix(((IPropertyReferenceExpression)expr).Target,  
                  action);
  else if (expr is IFieldReferenceExpression)
  {
    IExpression target = ((IFieldReferenceExpression)expr).Target;
    if (!(target is IThisReferenceExpression))
      ForEachPrefix(target, action);
  }
  else if (expr is ICastExpression)
    ForEachPrefix(((ICastExpression)expr).Expression,
                  action);
  else if (expr is IPropertyIndexerExpression)
    ForEachPrefix(((IPropertyIndexerExpression)expr).Target, 
                  action);
  else if (expr is IEventReferenceExpression)
    ForEachPrefix(((IEventReferenceExpression)expr).Target,
                  action);
  else if (expr is IUnaryExpression)
    ForEachPrefix(((IUnaryExpression)expr).Expression,
                  action);
  else if (expr is IAddressReferenceExpression)
    ForEachPrefix(((IAddressReferenceExpression)expr).Expression, 
                  action);
  else if (expr is IMethodInvokeExpression)
    ForEachPrefix(((IMethodInvokeExpression)expr).Method,
                  action);
  else if (expr is IMethodReferenceExpression)
    ForEachPrefix(((IMethodReferenceExpression)expr).Target,
                  action);
  else if (expr is IUnaryExpression)
    ForEachPrefix(((IUnaryExpression)expr).Expression,
                  action);
  else if (expr is IAddressReferenceExpression)
    ForEachPrefix(((IAddressReferenceExpression)expr).Expression, 
                  action);
  else if (expr is IDelegateInvokeExpression)
    ForEachPrefix(((IDelegateInvokeExpression)expr).Target,
                  action);
  action(expr);
}

Нашли? Давай проверим!

Предупреждения PVS-Studio:

V3003 Использование шаблона if (A) {…} else if (A) {…}. Есть вероятность наличия логической ошибки. Проверьте строки: 1719, 1727. Компилятор CodeRecognizer.cs 1719

V3003 Использование шаблона if (A) {…} else if (A) {…}. Есть вероятность наличия логической ошибки. Проверьте строки: 1721, 1729. Компилятор CodeRecognizer.cs 1721

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

private void ForEachPrefix(IExpression expr,
                           Action<IExpression> action)
{
  if (....)
  ....
  else if (expr is IUnaryExpression)
    ForEachPrefix(((IUnaryExpression)expr).Expression,
                  action);
  else if (expr is IAddressReferenceExpression)
    ForEachPrefix(((IAddressReferenceExpression)expr).Expression, 
                  action);
  ....
  else if (expr is IUnaryExpression)
    ForEachPrefix(((IUnaryExpression)expr).Expression,
                  action);
  else if (expr is IAddressReferenceExpression)
    ForEachPrefix(((IAddressReferenceExpression)expr).Expression, 
                   action)
  ....
}

Условные выражения и ветви then нескольких операторов if дублируются. Возможно, этот код был написан методом copy-paste, что привело к проблеме. Теперь выясняется, что then -ветвь дубликатов никогда не будет выполняться, потому что:

  • Если условное выражение истинно, тело первого оператора if выполняется из соответствующей пары;
  • Если условное выражение ложно в первом случае, оно будет ложным и во втором.

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

Давай продолжим.

public int Compare(Pair<int, int> x, Pair<int, int> y)
{
  if (x.First < y.First)
  {
    if (x.Second >= y.Second)
    {
      // y strictly contains x
      return 1;
    }
    else
    {
      // No containment - order by left bound
      return 1;
    }
  }
  else if (x.First > y.First)
  {
    if (x.Second <= y.Second)
    {
      // x strictly contains y
      return -1;
    }
    else
    {
      // No containment - order by left bound
      return -1;
    }
  }
  ....
}

Предупреждения PVS-Studio:

V3004 Оператор then эквивалентен оператору else. Среда выполнения RegexpTreeBuilder.cs 1080

V3004 Оператор then эквивалентен оператору else. Среда выполнения RegexpTreeBuilder.cs 1093

Код выглядит очень подозрительно, потому что он содержит два условных оператора с одинаковыми телами ветвей then и else. Вероятно, в обоих случаях стоит возвращать разные значения. С другой стороны, если это задуманное поведение, будет полезно удалить избыточные условные операторы.

Наткнулся на еще несколько интересных петель. Пример приведен ниже:

private static Set<StochasticityPattern> 
IntersectPatterns(IEnumerable<StochasticityPattern> patterns)
{
    Set<StochasticityPattern> result 
      = new Set<StochasticityPattern>();
    result.AddRange(patterns);
    bool changed;
    do
    {
        int count = result.Count;
        AddIntersections(result);
        changed = (result.Count != count);
        break;
    } while (changed);
    return result;
}

Предупреждение PVS-Studio: V3020 Безусловный разрыв внутри цикла. Компилятор DefaultFactorManager.cs 474

Из-за безусловного оператора break выполняется ровно одна итерация цикла, а управляющая переменная changed даже не используется. Вообще код выглядит странно и подозрительно.

Тот же метод (точная копия) имел место в другом классе. Соответствующее предупреждение анализатора: V3020 Безусловный разрыв внутри цикла. Визуализаторы.Windows FactorManagerView.cs 350

Кстати, я наткнулся на безусловный оператор continue в цикле (анализатор обнаружил его по той же диагностике), но выше был комментарий о том, что это специальное временное решение:

// TEMPORARY
continue;

Напомню, что рядом с безусловным оператором break таких комментариев не было.

Давайте двигаться дальше.

internal static DependencyInformation GetDependencyInfo(....)
{
  ....
  IExpression resultIndex = null;
  ....
  if (resultIndex != null)
  {
    if (parameter.IsDefined(
          typeof(SkipIfMatchingIndexIsUniformAttribute), false))
    {
      if (resultIndex == null)
        throw new InferCompilerException(
                     parameter.Name 
                 + " has SkipIfMatchingIndexIsUniformAttribute but " 
                 + StringUtil.MethodNameToString(method) 
                 + " has no resultIndex parameter");
      ....
     }
     ....
  }
  ....
}

Предупреждение PVS-Studio: V3022 Выражение resultIndex == null всегда ложно. Компилятор FactorManager.cs 382

Сразу отмечу, что между объявлением и данной проверкой значение переменной resultIndex может измениться. Однако между проверками resultIndex! = Null и resultIndex == null значение не может измениться. Следовательно, результатом выражения resultIndex == null всегда будет false, и поэтому исключение никогда не будет сгенерировано.

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

public static Tuple<int, string> ComputeMovieGenre(int offset,
                                                   string feature)
{
  string[] genres = feature.Split('|');
  if (genres.Length < 1 && genres.Length > 3)
  {
    throw 
      new ArgumentException(string.Format(
            "Movies should have between 1 and 3 genres; given {0}.",
            genres.Length));
  }
  double value = 1.0 / genres.Length;
  var result 
    = new StringBuilder(
            string.Format(
              "{0}:{1}",
              offset + MovieGenreBuckets[genres[0]],
              value));
  for (int i = 1; i < genres.Length; ++i)
  {
    result.Append(
      string.Format(
        "|{0}:{1}", 
        offset + MovieGenreBuckets[genres[i].Trim()],
        value));
  }
  return 
    new Tuple<int, string>(MovieGenreBucketCount, result.ToString());
}

Посмотрим, что здесь происходит. Входная строка анализируется символом «|». Если длина массива не соответствует ожидаемой, должно быть сгенерировано исключение. Подождите секунду ... жанры.Длина ‹1 && жанров.Длина› 3? Поскольку не существует числа, которое подходило бы для обоих диапазонов значений, требуемых выражением ([int.MinValue..1) и (3..int.MaxValue]), результат выражения всегда будет false. Следовательно, эта проверка ни от чего не защищает, и ожидаемое исключение не генерируется.

Вот что нам мешает анализатор: V3022 Выражение ‘жанры.Длина‹ 1 && жанры.Длина ›3’ всегда ложно. Вероятно, здесь следует использовать оператор ||. Evaluator Features.cs 242

Я наткнулся на подозрительную операцию по разделению.

public static void CreateTrueThetaAndPhi(....)
{
  ....
  double expectedRepeatOfTopicInDoc 
    = averageDocLength / numUniqueTopicsPerDoc;
  ....
  int cnt = Poisson.Sample(expectedRepeatOfTopicInDoc);
  ....
}

Предупреждение PVS-Studio: V3041 Выражение было неявно приведено из типа int в тип double. Рассмотрите возможность использования явного приведения типа, чтобы избежать потери дробной части. Пример: double A = (double) (X) / Y ;. LDA Utilities.cs 74

Вот что в этом месте подозрительного: выполняется целочисленное деление (переменные averageDocLength и numUniqueTopicsPerDoc имеют тип int), но результат записывается в переменная типа double. Возникает вопрос: было ли это сделано намеренно или подразумевалось деление действительных чисел? Если бы переменная expectedRepeatOfTopicInDoc имела тип int, это исключило бы возможные проблемы.

В других местах используется метод Poisson.Sample, аргументом которого является подозрительная переменная expectedRepeatOfTopicInDoc,, например, как описано ниже.

int numUniqueWordsPerTopic 
  = Poisson.Sample((double)averageWordsPerTopic);

averageWordsPerTopic относится к типу int, который приводится к double вместо его использования.

А вот еще одно место использования:

double expectedRepeatOfWordInTopic 
  = ((double)numDocs) * averageDocLength / numUniqueWordsPerTopic;
....
int cnt = Poisson.Sample(expectedRepeatOfWordInTopic);

Обратите внимание, что переменные имеют те же имена, что и в исходном примере, только для инициализации expectedRepeatOfWordInTopic используется деление вещественных чисел (из-за явного преобразования numDocs в двойной тип).

В целом, упомянутый выше стартовый фрагмент исходного кода, выделенный анализатором с предупреждением, заслуживает внимания.

Давайте оставим размышления о том, исправят это или нет, авторам кода (они знают лучше), а мы пойдем дальше. К следующему подозрительному разделению.

public static NonconjugateGaussian BAverageLogarithm(....)
{
  ....
  double v_opt = 2 / 3 * (Math.Log(mx * mz / Ex2 / 2) - m);
  if (v_opt != v)
  {
    ....
  }
  ....
}

Предупреждение PVS-Studio: V3041 Выражение было неявно приведено из типа int в тип double. Рассмотрите возможность использования явного приведения типа, чтобы избежать потери дробной части. Пример: double A = (double) (X) / Y ;. Среда выполнения ProductExp.cs 137

Анализатор снова обнаружил подозрительную операцию целочисленного деления, поскольку 2 и 3 - целочисленные числовые литералы, а результат выражения 2/3 будет 0. В результате выражение выглядит следующим образом:

double v_opt = 0 * expr;

Согласитесь, это немного странно. Несколько раз я возвращался к этому предупреждению, пытаясь найти трюк, не пытаясь добавить его в статью. Метод наполнен математикой и формулами (разбирать которые, честно говоря, не очень увлекательно), отсюда можно многого ожидать. Кроме того, я стараюсь максимально скептически относиться к предупреждениям, которые включаю в статью, и описываю их только предварительно глубоко изучив их.

Тогда меня осенило - зачем вам такой множитель, как 0, записанный как 2/3? Поэтому это место в любом случае стоит посмотреть.

public static void 
  WriteAttribute(TextWriter writer,
                 string name,
                 object defaultValue, 
                 object value, 
                 Func<object, string> converter = null)
{
  if (   defaultValue == null && value == null 
      || value.Equals(defaultValue))
  {
    return;
  }
  string stringValue = converter == null ? value.ToString() : 
                                           converter(value);
  writer.Write($"{name}=\"{stringValue}\" ");
}

Предупреждение PVS-Studio: V3080 Возможное нулевое разыменование. Рассмотрите возможность проверки ценности. Компилятор WriteHelpers.cs 78

Достаточно честное предупреждение анализатора, основанное на условии. Разыменование нулевой ссылки может произойти в выражении value.Equals (defaultValue), если value == null. Поскольку это выражение является правым операндом оператора ||, для его оценки левый операнд должен иметь значение false, и для этого достаточно хотя бы одной из переменных defaultValue \ value не должно быть равно null. В конце концов, если defaultValue! = Null и value == null:

  • defaultValue == null - ›false;
  • defaultValue == null && value == null - ›false; (проверка значения не выполнена)
  • value.Equals (defaultValue) - ›NullReferenceException, как value - null.

Давайте посмотрим на другой случай:

public FeatureParameterDistribution(
         GaussianMatrix traitFeatureWeightDistribution, 
         GaussianArray biasFeatureWeightDistribution)
{
  Debug.Assert(
    (traitFeatureWeightDistribution == null && 
     biasFeatureWeightDistribution == null)
     ||
     traitFeatureWeightDistribution.All(
       w =>    w != null 
            && w.Count == biasFeatureWeightDistribution.Count),
    "The provided distributions should be valid 
     and consistent in the number of features.");
  ....
}

Предупреждение PVS-Studio: V3080 Возможное нулевое разыменование. Рассмотрите возможность проверки "traitFeatureWeightDistribution". Рекомендуемый FeatureParameterDistribution.cs 65

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

(traitFeatureWeightDistribution == null && 
 biasFeatureWeightDistribution == null)
||
traitFeatureWeightDistribution.All(
  w =>   w != null 
      && w.Count == biasFeatureWeightDistribution.Count)

Опять же, правый операнд оператора || оценивается только в том случае, если результатом вычисления левого является false. Левый операнд может принимать значение false, в том числе когда traitFeatureWeightDistribution == null и biasFeatureWeightDistribution! = Null. Тогда правый операнд оператора || будет оцениваться, и вызов traitFeatureWeightDistribution.All приведет к выбросу ArgumentNullException.

Еще один интересный фрагмент кода:

public static double GetQuantile(double probability,
                                 double[] quantiles)
{
  ....
  int n = quantiles.Length;
  if (quantiles == null)
    throw new ArgumentNullException(nameof(quantiles));
  if (n == 0)
    throw new ArgumentException("quantiles array is empty", 
                                nameof(quantiles));
  ....
}

Предупреждение PVS-Studio: V3095 Объект quantiles использовался до того, как он был проверен на соответствие нулю. Проверьте строки: 91, 92. Среда выполнения OuterQuantiles.cs 91

Обратите внимание, что осуществляется доступ к свойству quantiles.Length, а затем проверяется равенство quantiles null. В конце концов, если quantiles == null, метод выдаст исключение, но неправильное и не в том месте. Вероятно, линии были перевернуты.

Если вы успешно справились с поиском ошибок, перечисленных ранее, предлагаю приготовить себе чашку кофе и попытаться повторить подвиг, обнаружив ошибку в приведенном ниже методе. Чтобы было немного интереснее, я приведу код метода целиком.

(Полноразмерный)

Ладно, ладно, это была шутка (или ты это сделал ?!). Давайте упростим задачу:

if (sample.Precision < 0)
{
  precisionIsBetween = true;
  lowerBound = -1.0 / v;
  upperBound = -mean.Precision;
}
else if (sample.Precision < -mean.Precision)
{
  precisionIsBetween = true;
  lowerBound = 0;
  upperBound = -mean.Precision;
}
else
{
  // in this case, the precision should NOT be in this interval.
  precisionIsBetween = false;
  lowerBound = -mean.Precision;
  lowerBound = -1.0 / v;
}

Это лучше? На этот код анализатор выдал следующее предупреждение: V3008 Переменной lowerBound дважды подряд присваиваются значения. Возможно, это ошибка. Проверить строки: 324, 323. Время выполнения GaussianOp.cs 324

Действительно, в последней ветви else значение переменной lowerBound присваивается дважды подряд. Видимо (судя по приведенному выше коду) переменная upperBound должна участвовать в одном из присваиваний.

Давайте двигаться дальше.

private void WriteAucMatrix(....)
{
  ....
  for (int c = 0; c < classLabelCount; c++)
  {
    int labelWidth = labels[c].Length;
    columnWidths[c + 1] = 
      labelWidth > MaxLabelWidth ? MaxLabelWidth : labelWidth;
    for (int r = 0; r < classLabelCount; r++)
    {
      int countWidth = MaxValueWidth;
      if (countWidth > columnWidths[c + 1])
      {
        columnWidths[c + 1] = countWidth;
      }
    }
  ....
}

Предупреждение PVS-Studio: V3081 Счетчик r не используется внутри вложенного цикла. Рассмотрите возможность использования счетчика c. CommandLine ClassifierEvaluationModule.cs 459

Обратите внимание, что счетчик внутреннего цикла - r не используется в теле этого цикла. Из-за этого получается, что на всех итерациях внутреннего цикла выполняются одни и те же операции с одними и теми же элементами - в индексах также используется счетчик внешнего цикла ©, а не внутреннего цикла (r ).

Посмотрим другие интересные вопросы.

public RegexpFormattingSettings(
         bool putOptionalInSquareBrackets,
         bool showAnyElementAsQuestionMark,
         bool ignoreElementDistributionDetails,
         int truncationLength,
         bool escapeCharacters,
         bool useLazyQuantifier)
{
  this.PutOptionalInSquareBrackets = putOptionalInSquareBrackets;
  this.ShowAnyElementAsQuestionMark = showAnyElementAsQuestionMark;
  this.IgnoreElementDistributionDetails = 
    ignoreElementDistributionDetails;
  this.TruncationLength = truncationLength;
  this.EscapeCharacters = escapeCharacters;
}

Предупреждение PVS-Studio: параметр конструктора V3117 useLazyQuantifier не используется. Среда выполнения RegexpFormattingSettings.cs 38

В конструкторе не используется один параметр - useLazyQuantifier. Это выглядит особенно подозрительно в свете того, что в классе свойство определяется с соответствующим именем и типом - UseLazyQuantifier. Видимо, через соответствующий параметр забыли провести его инициализацию.

Я также встретил несколько потенциально опасных обработчиков событий. Пример одного из них приведен ниже:

public class RecommenderRun
{
  ....
  public event EventHandler Started;
  ....
  public void Execute()
  {
    // Report that the run has been started
    if (this.Started != null)
    {
      this.Started(this, EventArgs.Empty);
    }
      ....
  }
  ....
}

Предупреждение PVS-Studio: V3083 Небезопасный вызов события Started, возможен NullReferenceException. Рассмотрите возможность присвоения события локальной переменной перед ее вызовом. Evaluator RecommenderRun.cs 115

Дело в том, что между проверкой неравенства null и вызовом обработчика может произойти отмена подписки на событие, если во время между проверкой на null и вызовом обработчиков событий, событие не будет иметь подписчиков, будет выброшено исключение NullReferenceException. Чтобы избежать таких проблем, вы можете, например, сохранить ссылку на цепочку делегатов в локальной переменной или использовать оператор «?.» Для вызова обработчиков.

Помимо приведенного выше фрагмента кода, было найдено еще 35 таких мест.

Кстати, предупреждений 785 V3024 было. Предупреждение V3024 выдается при сравнении действительных чисел с использованием операторов ! = Или ==. Не буду останавливаться на том, почему такие сравнения не всегда корректны. Подробнее об этом написано в документации, также есть ссылка на StackOverflow.

Принимая во внимание, что формулы и расчеты часто встречались, эти предупреждения могли быть важны даже на 3-м уровне (поскольку они не актуальны для всех проектов).

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

Заключение

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

Желаю разработчикам скорейшего исправления проблемных мест и напомню, что ошибаться - это нормально, ведь мы люди. Вот почему нам нужны дополнительные инструменты, такие как статические анализаторы, чтобы найти то, что человек упустил, верно? В любом случае, удачи в вашем проекте и спасибо за вашу работу!

Кроме того, помните, что максимальное использование статического анализатора достигается при его регулярном использовании.

Всего наилучшего!