Мы используем куки, чтобы пользоваться сайтом было удобно.
Хорошо
to the top

Вебинар: Зачем тестировщику нужна безопасность? - 15.04

>
>
>
Разработка нового статического...

Разработка нового статического анализатора: PVS-Studio JavaScript

Вот уже 18 лет статический анализатор кода PVS-Studio находится на рынке. За это время он обзавёлся поддержкой языков C, C++, C# и Java. Разумеется, останавливаться на этих языках мы не планируем, и в этой статье расскажем про разработку нового JavaScript/TypeScript анализатора, который выйдет уже совсем скоро.

Введение

Поддержка анализатора для одной из самых популярных семей языков ECMAScript была лишь вопросом времени, учитывая их доминирующую популярность у программистов на протяжении долгих лет. Разумеется, с популярностью стека растёт и его экосистема, и в случае с JavaScript/TypeScript ей можно только позавидовать. Как говорил один знакомый программист: "Если перед тобой стоит какая-то проблема, то для JavaScript уже есть библиотека, которая её решает".

Конкуренция с платными и бесплатными инструментами серьёзная, поэтому наша цель сейчас — создать устойчивую и надёжную платформу, которую можно будет стабильно развивать и расширять, чтобы со временем сравняться с другими решениями на рынке. В том числе по этой причине мы намерены рано выйти в свет: EAP уже стартовал, а релиз MVP версии анализатора намечен на этот август.

В статье рассказываем, как устроен новый анализатор и какие новые архитектурные решения мы решили в нём применить.

Модель языка

TypeScript дерево

Любой анализатор начинается с модели языка, а если конкретнее — абстрактного синтаксического дерева. Ведь анализатору нужно как-то работать с исходным кодом.

И здесь мы начали с решения этой же задачи. Думать, впрочем, долго не пришлось: если мы хотим иметь и синтаксическое дерево, и семантическую модель для JavaScript и TypeScript разом, да ещё и с поддержкой от разработчиков языка, то у компилятора TypeScript конкурентов нет. Он умеет разбирать оба языка, а его поддержка гарантирована до тех пор, пока развивается сам TypeScript.

Традиционно рассмотрим полученный AST на примере факториала:

function factorial(n) {
  let result = 1;
  for (let i = 2; i <= n; i++) {
    result *= i;
  }
  return result;
}

Для него мы получим следующее дерево, которое можно посмотреть в спойлере.

SourceFile:
  FunctionDeclaration:
    Identifier
    Parameter:
      Identifier
    Block:
      VariableStatement:
        VariableDeclarationList:
          VariableDeclaration:
            Identifier
            NumericLiteral
      ForStatement:
        VariableDeclarationList:
          VariableDeclaration:
            Identifier
            NumericLiteral
      BinaryExpression:
        Identifier
        LessThanEqualsToken
        Identifier
      PostfixUnaryExpression:
        Identifier
      Block
        ExpressionStatement:
          BinaryExpression:
            Identifier
            AsteriskEqualsToken
            Identifier
      ReturnStatement:
        Identifier
  EndOfFileToken

Поиграться самостоятельно, кстати, можно здесь.

А ещё уже можно начать отмечать и небольшие отличия этого AST от более "академических". Во-первых, в дереве присутствуют токены, хотя чаще им место в дереве разбора. Во-вторых, называются они не по семантическому значению, а по визуальному представлению. В примере выше это AsteriskEqualsToken, но в TypeScript компиляторе есть и токен с прекрасным названием DotDotDotToken, обозначающий spread-оператор.

Семантика

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

function replaceFoo(variable: string): string {
    variable.replace("foo", "bar")
    return variable
}

Здесь результат replace никуда не сохраняется. Мы, конечно, можем ругаться на любой replace в программе, но тогда получим слишком много ложноположительных срабатываний. Чтобы гарантировать, что это именно тот replace, нам надо удостовериться, что он вызывается на объекте нужного типа, а также что типы переданных аргументов совпадают с типами параметров.

Как нам узнать, какой тип у идентификатора variable? Искать определение руками для всех узлов идентификаторов избыточно, а за пределами таких простых случаев ещё и нетривиально — с поднятиями было бы сложнее.

Эту задачу решает семантический анализ кода, который может нам помочь найти типы переменных и области их видимости. И это ещё одна причина, по которой был выбран именно TypeScript компилятор. Он может:

  • разрешать идентификаторы и их типы для TypeScript;
  • разрешать идентификаторы и выводить типы для JavaScript, если это возможно (был явно присвоен литерал, либо использовался JSDoc).

Словом, в компиляторе и правда есть всё, что нам нужно. Но приключения только начинаются.

gRPC

Новый анализатор поручили делать нам — Java команде. Возможно, по принципу "JavaScript с Java совпадает на 50%". Шутка. Или не совсем.

В любом случае, компилятор TypeScript написан, ожидаемо, на TypeScript. И что не очень ожидаемо, скоро он будет окончательно переписан на Go. Тут в полный рост встаёт две проблемы:

  • Полная зависимость от стороннего фреймворка — проблема сама по себе. Застигни нас переход на Go врасплох, пришлось бы спешно переписывать всю кодовую базу на другой язык или сидеть на legacy-платформе. Это не говоря о том, что на горизонте в десятилетия открытые решения могут быть заброшены.
  • Как уже было обозначено, мы — Java команда. Наша экспертность сосредоточена именно вокруг этого языка, ведь мы пишем на нём и анализируем его уже не первый год. Соответственно, переход на новый язык разработки обнулил бы всё это.

Решение напрашивалось само собой — архитектура анализатора разделяется на два приложения:

  • Обёртка над компилятором TypeScript, которая собирает и передаёт модель.
  • Анализатор на Java. Он запрашивает у обёртки сначала AST, а затем, по требованию, семантическую информацию.

В итоге модель, собранную TypeScript обёрткой, мы заворачиваем в protobuf, после чего передаём по gRPC. Про хитросплетения этого процесса было написано аж две статьи (раз, два). Из ключевых моментов:

  • Во время передачи мы транслируем исходное представление в своё собственное.
  • Также проводим нормализацию — упрощение дерева — для облегчения анализа. Например, делаем так, что у веток if всегда есть фигурные скобки, даже если в языке это опционально.
  • Трансляцию осуществляем автоматизировано из сериализованной в protobuf модели при помощи кодогенерации — благодаря этому легко реагировать на изменения и поддерживать новые версии языка.

При таком подходе вышеобозначенные минусы удалось нивелировать: основная разработка ведётся на Java, а вместо текущей обёртки для TypeScript компилятора можно подставить любую другую. Сделать это придётся с релизом TypeScript 7, когда нашу обёртку надо будет переписать на Go вслед за компилятором.

Общее абстрактное дерево

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

Переиспользование в основном достигалось за счёт экосистемы вокруг анализа — plog-converter и иже с ним. Хотя в отдельных случаях заимствовались и технологии анализа, вроде анализа потока данных из C++ в нашем родном Java анализаторе. Плюсы у традиционного подхода были:

  • Это довольно быстро: proof of concept можно сделать за считанные дни, если не часы.
  • Если разработчик пишет анализатор на том же языке, для которого он предназначен, это повышает и качество анализатора, и знание языка разработчиком.

Но и минусы тоже:

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

И вот последний пункт для нас оказался решающим. Помимо JavaScript/TypeScript и Go будет поддержка других языков? Что? Да! Следите за обновлениями :)

И на этом этапе закралась мысль: а почему бы не сделать общую инфраструктуру для всех языков в целом? Ведь у большинства языков программирования много общего: переменные, управляющие конструкции, операторы и прочее. Соответственно, при наличии обобщённой модели языков не понадобится дублировать диагностические правила, а при наличии единого ядра не понадобится дублировать всю остальную инфраструктуру.

CAT

Мысль с обобщённым деревом, разумеется, не революционная. Обычно его называют UAST, и, например, его используют JetBrains для своих IDE. Нам же так понравился акроним CAT, что мы решили использовать его — Common Abstract Tree.

Замысел простой:

  • От AST для конкретного языка мы не отказываемся. Он нам всё ещё нужен, если мы хотим не терять специфику языка.
  • Однако все узлы конкретного AST наследуют общие интерфейсы, определённые в CAT. Если в диагностическом правиле не нужна языковая специфика, то можно работать только с этими интерфейсами.
  • Так как мы не хотим терять в точности анализа, а языковая специфика бывает нужна часто, в диагностических правилах предусмотрена возможность расширения обобщённых правил конкретными.

Думаю, в будущем мы ещё выпустим подробный материал на эту тему, а пока ограничимся общими принципами. Подход можно рассмотреть на примере упрощённого аналога V6001, который ищет одинаковые операнды у бинарной операции, т. е. ошибки вида a == a. Очевидно, что такая конструкция валидна почти в любом языке. Сама по себе она раскладывается на типичные "кирпичики" языка:

  • Всё выражение в целом так и называется — "выражение".
  • Операция сравнения здесь — это "бинарная операция" с типом "равно".
  • Слева и справа также "выражения", которые в нашем случае имеют более узкий тип "идентификатор".

Что перед нами, примерно ясно, но не ясно, что с этим делать. Поэтому далее рассмотрим поиск ошибок на дереве.

Что из себя представляет диагностическое правило?

Диагностические правила работают следующим образом:

  • Обходчик сканирует AST конкретного языка, останавливаясь на каждом элементе кода и запуская на них проверки;
  • Для оптимизации у проверок есть предварительная фильтрация в зависимости от того, на какой тип элемента кода они должны сработать;
  • Если диагностическое правило нашло подозрительный паттерн, оно выдаёт предупреждение на этот участок кода.

Вернёмся к ошибке выше. Чтобы её найти, нужно правило, которое проверит: тип бинарного выражения (умножение, скажем, нас не интересует), что левый и правый операнд имеют одинаковый тип, а также равенство этих операндов. Обработка случаев цепочки бинарных операндов вроде a == 0 && b == 0 && c == 0 менее тривиальна, но суть примерно та же.

По такому паттерну можно найти эту ошибку в любом языке, но это не значит, что в них не может быть своей специфики. В JavaScript существует паттерн вида foo && foo.bar && foo. Он встречается в return, где таким образом возвращают значение переменной из условия после предварительной проверки.

Для таких случаев мы предусмотрели несколько точек расширения общего диагностического правила: перед тем, как оно сработает; фильтрация промежуточных результатов и после того, как правило сработало. В нашем случае после того, как правило сработает на одинаковые идентификаторы переменных, можно проверить, является ли один из них последним в условии, исключая таким образом ложноположительные результаты.

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

Как доктор Франкенштейн собирал из частей анализатор

Из-за того, что мы пошли непривычным путём, наша архитектура оказалась заметно сложнее обычной:

  • В корне мы имеем обычное CLI-приложение, написанное на Java.
  • В ядре имеется абстрактная модель CAT и правила для неё.
  • Для JavaScript/TypeScript есть свои модули, содержащие конкретную модель языка и уточнения правил.
  • Вместо JavaScript/TypeScript модуля можно подставить модуль для другого языка, переиспользуя обобщённое ядро при анализе.
  • Также ядро и языковые слои делят инфраструктуру вокруг анализа, например формирование отчётов.
  • Незаменимой частью для нового анализатора является TypeScript-сервер, который при помощи API компилятора собирает модель программы и отдаёт AST и семантику нам.

Упрощённо её можно представить так:

Несмотря на повышенную комплексность системы, уже с первых тестов она показывала себя хорошо.

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

К слову, в этой истории есть занятная ирония. С предыдущей Java командой, создавшей Java анализатор, преемственности у нас нет — мы не пересекались во времени. Однако духовная, видимо, имеется, ведь мы независимо пришли к схожему решению — идти не проторённой дорогой, а исследовать новые способы создания анализаторов и пробовать переиспользовать наработки. Только они интегрировали анализ потока данных для C++ в Java, а мы попытались выстроить единую платформу для анализа разных языков программирования.

Плагины

Для максимально простой интеграции статического анализатора в процесс разработки мы разрабатываем плагины для инструментов, которыми пользуются разработчики. В рамках EAP вместе с анализатором будет доступен плагин для запуска JS/TS анализатора из IDE WebStorm.

На текущем этапе плагин позволяет:

  • запускать анализ всего проекта;
  • фильтровать выданные предупреждения;
  • осуществлять навигацию по коду, просматривая срабатывания;
  • просматривать самые интересные срабатывания, используя механизм Best Warnings;
  • размечать ложные срабатывания.

В будущем, к выходу MVP, запуск статического анализатора для JS/TS будет доступен в нашем расширении для Visual Studio Code, а плагин для WebStorm наполнится более широким функционалом.

Если вы разрабатываете не в WebStorm, вам будет доступна возможность использовать ядро нашего анализатор через CLI и просматривать отчёты, к примеру, в нашем инструменте PVS-Studio Atlas или в браузере.

Наша система тестирования

Для тестирования JS/TS анализатора мы переиспользовали наш специальный инструмент, именуемый Self-Tester. Он позволяет работать с базой открытых проектов, проводя тестирование анализатора на регрессию. Происходит это следующим образом:

  • тестируемый проект определённой версии выкачивается с репозитория на GitHub;
  • выполняется сборка проекта;
  • выполняется запуск статического анализатора при помощи CLI;
  • получившийся отчёт сравнивается с эталоном для этого проекта.

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

В настоящий момент список проектов для JavaScript Selft-Tester'а следующий:

Для TypeScript анализатора список проектов ещё формируется.

У нас есть статья, в которой мы подробно описываем, как у нас происходит процесс разработки и тестирования диагностических правил. Глобально для JS/TS анализатора ничего не изменилось.

Примеры ошибок

Ну какой рассказ про новый статический анализатор без демонстрации того, что он умеет находить? Далее мы покажем вам, что нашёл статический анализатор в базе нашего Self-Tester'а, а также в различных open source проектах.

Несмотря на то, что анализатор для языка TypeScript в рамках EAP будет доступен позже, чем для JavaScript, у нас есть уникальная возможность воспользоваться им раньше остальных. Только никому не говорите :)

Так что далее мы рассмотрим ошибки и в JavaScript, и в TypeScript коде.

Срабатывания в ESLint

Открывает подборку ошибка, обнаруженная в линтере JavaScript кода ESLint:

function isFirstBangInBangBangExpression(node) {
  return (
    node &&
    node.type === "UnaryExpression" &&
    node.argument.operator === "!" &&
    node.argument &&
    node.argument.type === "UnaryExpression" &&
    node.argument.operator === "!"
  );
}

Предупреждение PVS-Studio: V7001 The operands of the '&&' operator are equivalent. space-unary-ops.js 109

Анализатор сообщает, что операнды бинарного выражения "И" одинаковые.

Сначала может показаться, что проверка просто лишняя, и ничего критичного здесь не происходит. Ясность вносит название метода и его JsDoc:

/**
* Check if the node is the first "!" in a "!!" convert to Boolean expression
* @param {ASTnode} node AST node
* @returns {boolean} Whether or not the node is first "!" in "!!"
*/

Этот метод должен проверять, что рассматриваемое выражение — унарный оператор !, внутри которого располагается ещё один унарный оператор !. Такая конструкция из унарных операторов в JavaScript приводит выражение к Boolean-типу.

Но что в этой ситуации идёт не так? Выражение !!1 на уровне АСТ будет выглядеть вот так:

UnaryExpression (!)
└── argument: UnaryExpression (!)
    └── argument: value (1)

И в приведённом фрагменте кода вместо того, чтобы проверить оператор внешнего унарного выражения, мы дважды проверяем только оператор внутреннего унарного выражения !.

Срабатывания в Visual Studio Code

Перейдём к небезызвестному Visual Studio Code:

this._dispooables.add(
  Event.any<....>(
    _fileService.onDidChangeFileSystemProviderRegistrations,
    _fileService.onDidChangeFileSystemProviderCapabilities
)(e => {
  const oldIgnorePathCasingValue = schemeIgnoresPathCasingCache.get(e.scheme);
  if (oldIgnorePathCasingValue === undefined) {
    return;
  }
  schemeIgnoresPathCasingCache.delete(e.scheme);
  const newIgnorePathCasingValue = ignorePathCasing(URI.from(....);
  if (newIgnorePathCasingValue === newIgnorePathCasingValue) {
    return;
  }
  for (const [key, entry] of this._canonicalUris.entries()) {
    if (entry.uri.scheme !== e.scheme) {
      continue;
    }
    this._canonicalUris.delete(key);
  }
}));

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

А что говорит анализатор? Предупреждение PVS-Studio: V7001 The operands of the '===' operator are equivalent. uriIdentityService.ts 63

Чтобы было понятнее, о чём речь, приведу именно фрагмент с ошибкой:

const oldIgnorePathCasingValue = schemeIgnoresPathCasingCache.get(e.scheme);
if (oldIgnorePathCasingValue === undefined) {
  return;
}
schemeIgnoresPathCasingCache.delete(e.scheme);
const newIgnorePathCasingValue = ignorePathCasing(URI.from(....);
if (newIgnorePathCasingValue === newIgnorePathCasingValue) { // <=
  return;
}
....

В условном операторе одна и та же константа сравнивается сама с собой.

Судя по всему, это последствие неудачного рефакторинга. Изменения появились в этом коммите. В нём аналогичный код был частью метода _handleFileSystemProviderChangeEvent и выглядел следующим образом:

if (currentCasing === undefined) {
  return;
}
const newCasing = this._calculateIgnorePathCasing(event.scheme);
if (currentCasing === newCasing) {
  return;
}

Фрагмент с ошибкой и этот, по сути, аналогичные, только именование объектов отличается. Ну и в старом фрагменте такой ошибки не было: сравнивались разные константы.

Это не единственное, что нашёл анализатор. Посмотрим на ещё один фрагмент кода:

private renderQuotaItem(
    container: HTMLElement, 
    label: string, 
    quota: IQuotaSnapshot, 
    overageEnabled: boolean = false
    ): void {
  const quotaItem = DOM.append(container, $('.quota-item'));
  const quotaItemHeader = DOM.append(quotaItem, $('.quota-item-header'));
  const quotaItemLabel = DOM.append(quotaItemHeader, $('.quota-item-label'));
  quotaItemLabel.textContent = label;
  const quotaItemValue = DOM.append(quotaItemHeader, $('.quota-item-value'));
  if (quota.unlimited) {
    quotaItemValue.textContent = localize('plan.included', 'Included');
  } else {
    quotaItemValue.textContent = localize('plan.included', 'Included');
  }
  // Progress bar - using same structure as chat status
  const progressBarContainer = DOM.append(quotaItem, $('.quota-bar'));
  const progressBar = DOM.append(progressBarContainer, $('.quota-bit'));
  const percentageUsed = this.getQuotaPercentageUsed(quota);
  progressBar.style.width = percentageUsed + '%';
  
  if (percentageUsed >= 90 && !overageEnabled) {
    quotaItem.classList.add('error');
  } else if (percentageUsed >= 75 && !overageEnabled) {
    quotaItem.classList.add('warning');
  }
}

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

Предупреждение PVS-Studio: V7004 The 'then' statement is equivalent to the 'else' statement. chatUsageWidget.ts 102

Речь про следующее условие:

if (quota.unlimited) {
    quotaItemValue.textContent = localize('plan.included', 'Included');
  } else {
    quotaItemValue.textContent = localize('plan.included', 'Included');
  }
  ....
}

Что then, что else ветка одинаковые, и эта проверка на бессмысленна. Что конкретно должно быть здесь, известно одним лишь авторам проекта.

А мы двигаемся дальше. Напоследок, рассмотрим в VS Code два интересных момента.

Первый из них:

for (const namedImport of namedImports) {
  const isTarget = 
    namedImport.name.getText() === functionName || (namedImport.propertyName &&
        namedImport.propertyName.getText() === functionName);
  if (!isTarget) {
    continue;
  }
  const searchName = namedImport.propertyName 
                        ? namedImport.name 
                        : namedImport.name;
  const refs = service.getReferencesAtPosition(
    filename, 
    searchName.pos + 1
  ) ?? [];
  for (const ref of refs) {
    if (ref.isWriteAccess) {
      continue;
    }
    const calls = collect(
        sourceFile, 
        n => isCallExpressionWithinTextSpanCollectStep(ref.textSpan, n)
    );
    const lastCall = calls[calls.length - 1] as ts.CallExpression | undefined;
    if (lastCall) {
      localizeCallExpressions.push(lastCall);
    }
  }
}

Предупреждение PVS-Studio: V7012 The conditional expression always returns the same value. nls-analysis.ts 186

Речь идёт о следующей строке:

const searchName = namedImport.propertyName 
                      ? namedImport.name 
                      : namedImport.name;

Вне зависимости от условия searchName будет равен namedImport.name. Судя по всему, этот фрагмент должен выглядеть следующим образом:

const searchName = namedImport.propertyName 
                      ? namedImport.propertyName 
                      : namedImport.name;

Ну и второе — аналогичное, но в другом месте:

const passiveStyles = {
    borderColor: hcBorderColor 
                    ? hcBorderColor.toString() 
                    : observeColor(
                        editorHoverForeground, 
                        this._themeService
                    ).map(c => c.transparent(0.2).toString())
                     .read(reader),
    backgroundColor: getEditorBackgroundColor(this._viewData.editorType),
    color: '',
    opacity: '0.7',
};
const editorBackground = getEditorBackgroundColor(this._viewData.editorType);
const primaryActionStyles = derived(
    this, 
    r => alternativeActionActive.read(r) 
        ? primaryActiveStyles 
        : primaryActiveStyles
);
const secondaryActionStyles = derived(
    this, 
    r => alternativeActionActive.read(r) 
        ? secondaryActiveStyles 
        : passiveStyles
);
// TODO@benibenj clicking the arrow does not accept suggestion anymore
return ....

Предупреждение PVS-Studio: V7012 The conditional expression always returns the same value. inlineEditsWordReplacementView.ts 222

Здесь речь идёт о следующей строке:

const primaryActionStyles = derived(
    this, 
    r => alternativeActionActive.read(r) 
        ? primaryActiveStyles 
        : primaryActiveStyles
);

По мере рассмотрения исходного кода сложилось впечатление, что это последствия неудачного рефакторинга. В результате следующего коммита было много изменений, и эти строки — одно из них. До исправления этот фрагмент выглядел так:

const primaryActionStyles = derived(
    this, 
    r => alternativeActionActive.read(r) 
        ? passiveStyles 
        : activeStyles
);
const secondaryActionStyles = derived(
    this, 
    r => alternativeActionActive.read(r) 
        ? activeStyles 
        : passiveStyles 
);

Теперь же в первом тернарном операторе одинаковые then и else выражения.

Срабатывания в Overleaf

Переходим к проекту Overleaf.

Помните, выше мы обсуждали разметку replace-методов? Это было не просто так. Перед нами следующий фрагмент кода:

/**
 * Sanitize a translation string to prevent injection attacks
 *
 * @param {string} input
 * @returns {string}
 */
function sanitize(input) {
  // Block Angular XSS
  // Ticket: https://github.com/overleaf/issues/issues/4478
  input = input.replace(/'/g, ''')
  // Use left quote where (likely) appropriate.
  input.replace(/ '/g, ' '')                       // <=
  ....
}

Предупреждение PVS-Studio: V7010. The return value of function 'replace' is required to be utilized. sanitize.js 14

Если посмотреть на то место, которое выделил анализатор, становится понятно, что перед нами опечатка: изменённое значение replace во второй строке метода никем не используется.

Изначально нам эта ошибка показалась серьёзной, поскольку по комментариям и JSDoc видно, что этот метод занимается санитизацией внешних данных. Но затем стало ясно, что строка, на которую указал анализатор, представляет собой лишь косметическую правку. Этот replace форматирует кавычки, заменяя закрывающие в начале фраз на открывающие. Тем не менее, случись ошибка на строку раньше, последствия были бы серьёзнее.

Анализатор обнаружил ещё одну ошибку в этом проекте:

if (change.isIntersecting) {
  videoIsVisible = true
  if (videoEl.readyState >= videoEl.HAVE_FUTURE_DATA) {
    if (!videoEl.ended) {
      videoEl
        .play()
        .catch(error =>
          debugConsole.error('Video autoplay failed:', error)
        )
    } else {
      videoEl
        .play()
        .catch(error =>
          debugConsole.error('Video autoplay failed:', error)
        )
    }
  }
}

Предупреждение PVS-Studio: V7004. The 'then' statement is equivalent to the 'else' statement. index.js 39

Здесь абсолютно одинаковое поведение и в then, и в else ветках исполнения. Вероятно, последствие копирования кода, и поведение в одной из веток должно отличаться.

В последующем коммите, который затрагивает этот файл, поведение было изменено, и проблема исчезла.

Срабатывание в Prisma

Следующий проект — Prisma.

Ошибка была обнаружена в этом коде:

export function strongGreen(str: string): string {
  return `\u001b[1;32;48;5;22m${str}\u001b[m`
}

export function strongRed(str: string): string {
  return `\u001b[1;31;48;5;52m${str}\u001b[m`
}

export function strongBlue(str: string): string {
  return `\u001b[1;31;48;5;52m${str}\u001b[m`
}

Предупреждение PVS-Studio: V7002 The body of a function is fully equivalent to the body of another function. customColors.ts 5

Анализатор сообщает, что содержимое функций strongRed и strongBlue абсолютно одинаковое. А что в них происходит?

Эти функции возвращают строки, специально отформатированные для отображения стилизованного текста в различных терминалах. Они называются управляющими последовательностями ANSI. Если вы никогда про них не слышали, давайте посмотрим, как это работает на примере возвращаемого значения у функции strongRed:

\u001b[ — команда, которую терминал воспринимает как специальный сигнал, мол "сейчас будет команда для форматирования текста".

Далее идёт строка [1;31;48;5;52m. Это настройки отображения текста:

  • 1 — выделить текст как жирный;
  • 31 — делает текст красным;
  • 48 — сообщает, что следующие настройки будут отвечать за цвет фона;
  • 5;52 — ставит фону цвет из RGB-палитры в 255 цветов, соответствующий цвету под индексом 52;
  • m – применяет эти стили к тексту;
  • ${str} — текст, к которому все эти стили применяются;
  • \u001b[m — команда, которая сбрасывает для последующего текста все применённые ранее стили.

Вернёмся к ошибке. Что для функции strongRed, что для strongBlue выставляется один и тот же красный цвет текста (31) и один и тот же цвет заднего фона (52).

С цветом текста в strongBlue понятно — нужно заменить на голубой (индекс 34), а над цветом фона нужно подумать авторам.

Если вам интересно, то здесь можно подробнее ознакомиться с тем, что такое управляющие последовательности ANSI и как они работают.

Срабатывания в React

Продолжает нашу демонстрацию небезызвестный проект React.

Итак, сам код:

for (const property of value.properties) {
  if (property.kind === 'ObjectProperty') {
    effects.push({
      kind: 'Capture',
      from: property.place,
      into: lvalue,
    });
  } else {
    effects.push({
      kind: 'Capture',
      from: property.place,
      into: lvalue,
    });
  }
}

Предупреждение PVS-Studio: V7004. The 'then' statement is equivalent to the 'else' statement. InferMutationAliasingEffects.ts 1771

То же срабатывание, что мы рассматривали ранее, но уже на TypeScript коде. Выражения в then и else ветках абсолютно одинаковые. Либо это последствия неудачного рефакторинга, что может сильно сбивать с толку, либо же действительно ошибка.

Срабатывание в jQuery

Ну и напоследок несложная ошибка из проекта jQuery:

var fullscreenSupported = document.exitFullscreen ||
  document.exitFullscreen ||
  document.msExitFullscreen ||
  document.mozCancelFullScreen ||
  document.webkitExitFullscreen;

Предупреждение PVS-Studio: V7001 The operands of the '||' operator are equivalent. gh-1764-fullscreen.js 13

Здесь два раза в условии фигурирует document.exitFullscreen.

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

Заключение

Мы продемонстрировали вам нашу большую работу — новый анализатор PVS-Studio для языков JavaScript и TypeScript. Но, на самом деле, то, что мы имеем сейчас — это лишь верхушка айсберга. Хоть и, как вы могли заметить, уже умеющая искать ошибки.

Чтобы анализ был более крутым и продвинутым, помимо AST и семантики нам необходимо реализовать ещё множество различных технологий внутри анализатора. Существует целый бездонный океан того, что анализатор может уметь:

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

И тем не менее уже положено отличное начало: у нас подготовлена база для анализаторов JavaScript и TypeScript, базовый набор диагностик, а также платформа для реализации новых анализаторов. Помимо доработок самого анализатора, мы будем реализовывать различные интеграции и в целом улучшать опыт его использования.

В рамках EAP также доступен анализатор для языка Go. Мы уже выпустили несколько статей о том, какие ошибки нашли с его помощью:

А также подобную этой статью о том, как реализовать свой анализатор для Go.

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

А на этом мы будет с вами прощаться. До скорых встреч!

Подписаться на рассылку
Хотите раз в месяц получать от нас подборку вышедших в этот период самых интересных статей и новостей? Подписывайтесь!
Популярные статьи по теме

Комментарии (0)

Следующие комментарии next comments
close comment form