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

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

![1363_JavaScript_analyzer_ru/image1.png](https://import.viva64.com/docx/blog/1363_JavaScript_analyzer_ru/image1.png)

## Введение

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

Конкуренция с платными и бесплатными инструментами серьёзная, поэтому наша цель сейчас — создать устойчивую и надёжную платформу, которую можно будет стабильно развивать и расширять, чтобы со временем сравняться с другими решениями на рынке\. В том числе по этой причине мы намерены рано выйти в свет: EAP [уже стартовал](https://pvs-studio.ru/ru/blog/posts/js/1351/), а релиз MVP версии анализатора намечен на этот август\.

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

## Модель языка

### TypeScript дерево

Любой анализатор начинается с модели языка, а если конкретнее — [абстрактного синтаксического дерева](https://pvs-studio.ru/ru/blog/terms/0004/)\. Ведь анализатору нужно как\-то работать с исходным кодом\.

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

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

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

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

<details>
   <summary>AST для факториала</summary>

```cpp
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
```




</details>
Поиграться самостоятельно, кстати, можно [здесь](https://ts-ast-viewer.com/#code/GYVwdgxgLglg9mABMAhtOAnGKA2AKMASkQG8AoRRHAUykQ2oGcQc6BeRARgG4LlNEeGnRiIOAJm6JRAHg5gpMANRLi5SpQbNWiAFQcYvSgF8+DKCAxItLKL2NA)\. 

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

### Семантика

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

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

Здесь результат `replace` никуда не сохраняется\. Мы, конечно, можем ругаться на любой `replace` в программе, но тогда получим слишком много [ложноположительных срабатываний](https://pvs-studio.ru/ru/blog/terms/6461/)\. Чтобы гарантировать, что это именно тот `replace`, нам надо удостовериться, что он вызывается на объекте нужного типа, а также что типы переданных аргументов совпадают с типами параметров\.

Как нам узнать, какой тип у идентификатора `variable`? Искать определение руками для всех узлов идентификаторов избыточно, а за пределами таких простых случаев ещё и нетривиально — с [поднятиями](https://developer.mozilla.org/ru/docs/Glossary/Hoisting) было бы сложнее\. 

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

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

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

### gRPC

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

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

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

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

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

![1363_JavaScript_analyzer_ru/image2.png](https://import.viva64.com/docx/blog/1363_JavaScript_analyzer_ru/image2.png)

В итоге модель, собранную TypeScript обёрткой, мы заворачиваем в protobuf, после чего передаём по gRPC\. Про хитросплетения этого процесса было написано аж две статьи \([раз](https://pvs-studio.ru/ru/blog/posts/java/1311/), [два](https://pvs-studio.ru/ru/blog/posts/java/1345/)\)\. Из ключевых моментов:

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

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

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

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

Переиспользование в основном достигалось за счёт экосистемы вокруг анализа — [plog\-converter](https://pvs-studio.ru/ru/docs/manual/0038/) и иже с ним\. Хотя в отдельных случаях заимствовались и технологии анализа, вроде анализа потока данных из C\+\+ в нашем родном Java анализаторе\. Плюсы у традиционного подхода были:

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

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

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

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

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

### CAT

Мысль с обобщённым деревом, разумеется, не революционная\. Обычно его называют UAST, и, например, его [используют](https://plugins.jetbrains.com/docs/intellij/uast.html) JetBrains для своих IDE\. Нам же так понравился акроним CAT, что мы решили использовать его — Common Abstract Tree\.

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

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

Думаю, в будущем мы ещё выпустим подробный материал на эту тему, а пока ограничимся общими принципами\. Подход можно рассмотреть на примере упрощённого аналога [V6001](https://pvs-studio.ru/ru/docs/warnings/v6001/), который ищет одинаковые операнды у бинарной операции, т\. е\. ошибки вида `a == a`\. Очевидно, что такая конструкция валидна почти в любом языке\. Сама по себе она раскладывается на типичные "кирпичики" языка:

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

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

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

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

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

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

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

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

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

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

![1363_JavaScript_analyzer_ru/image3.png](https://import.viva64.com/docx/blog/1363_JavaScript_analyzer_ru/image3.png)

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

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

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

![1363_JavaScript_analyzer_ru/image4.png](https://import.viva64.com/docx/blog/1363_JavaScript_analyzer_ru/image4.png)

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

Нововведения на этом не закончились: мы также используем компиляцию в [нативный образ через GraalVM](https://www.graalvm.org/latest/reference-manual/native-image/), благодаря чему перешли на последние версии Java; используем DI на основе [Micronaut](https://micronaut.io/) и в целом стараемся не отставать от новых веяний в индустрии\. 

К слову, в этой истории есть занятная ирония\. С предыдущей Java командой, создавшей Java анализатор, преемственности у нас нет — мы не пересекались во времени\. Однако духовная, видимо, [имеется](https://pvs-studio.ru/ru/blog/posts/java/0572/), ведь мы независимо пришли к схожему решению — идти не проторённой дорогой, а исследовать новые способы создания анализаторов и пробовать переиспользовать наработки\. Только они интегрировали анализ потока данных для C\+\+ в Java, а мы попытались выстроить единую платформу для анализа разных языков программирования\.

## Плагины

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

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

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



![1363_JavaScript_analyzer_ru/image5.png](https://import.viva64.com/docx/blog/1363_JavaScript_analyzer_ru/image5.png)

В будущем, к выходу MVP, запуск статического анализатора для JS/TS будет доступен в [нашем расширении для Visual Studio Code](https://pvs-studio.ru/ru/docs/manual/6646/), а плагин для WebStorm наполнится более широким функционалом\.

Если вы разрабатываете не в WebStorm, вам будет доступна возможность использовать ядро нашего анализатор через CLI и просматривать отчёты, к примеру, в нашем инструменте [PVS\-Studio Atlas](https://pvs-studio.ru/ru/blog/video/11588/) или в браузере\. 

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

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

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

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

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

* [eslint](https://github.com/eslint/eslint);
* [gutenberg](https://github.com/WordPress/gutenberg);
* [htmx](https://github.com/bigskysoftware/htmx);
* [jquery](https://github.com/jquery/jquery);
* [overleaf](https://github.com/overleaf/overleaf);
* [pdf\.js](https://github.com/mozilla/pdf.js);
* [prettier](https://github.com/prettier/prettier);
* [react](https://github.com/facebook/react);
* [three\.js](https://github.com/mrdoob/three.js);
* [webpack](https://github.com/webpack/webpack)\.

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

У нас [есть статья](https://pvs-studio.ru/ru/blog/posts/java/0752/), в которой мы подробно описываем, как у нас происходит процесс разработки и тестирования диагностических правил\. Глобально для JS/TS анализатора ничего не изменилось\.

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

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

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

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

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

Открывает подборку ошибка, обнаруженная в линтере JavaScript кода [ESLint](https://github.com/eslint/eslint):

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

Предупреждение PVS\-Studio: [V7001](https://pvs-studio.ru/ru/docs/warnings/v7001/) The operands of the '&&' operator are equivalent\. [space\-unary\-ops\.js 109](https://github.com/eslint/eslint/blob/811f4930f82ee2b6ac8eae75cade9bed63de0781/lib/rules/space-unary-ops.js#L109)

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

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

```cpp
/**
* 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` на уровне АСТ будет выглядеть вот так:

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

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

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

Перейдём к небезызвестному [Visual Studio Code](https://github.com/microsoft/vscode): 

```cpp
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](https://pvs-studio.ru/ru/docs/warnings/v7001/) The operands of the '\=\=\=' operator are equivalent\. [uriIdentityService\.ts 63](https://github.com/microsoft/vscode/blob/main/src/vs/platform/uriIdentity/common/uriIdentityService.ts#L63)

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

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

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

Судя по всему, это последствие неудачного рефакторинга\. Изменения появились в [этом коммите](https://github.com/microsoft/vscode/commit/781f31906ffff2447c8c3e77a49f7526f7f59a54#diff-e3e08baa4e6f82efaf0799a1ffb79b9238d51c6e0da031b257e2a13e205db28d)\. В нём аналогичный код был частью метода `_handleFileSystemProviderChangeEvent` и выглядел следующим образом:

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

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

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

```cpp
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](https://pvs-studio.ru/ru/docs/warnings/v7004/) The 'then' statement is equivalent to the 'else' statement\. [chatUsageWidget\.ts 102](https://github.com/microsoft/vscode/blob/576cc0a5e1960ad40c8c88425e9271c79a085e05/src/vs/workbench/contrib/chat/browser/chatManagement/chatUsageWidget.ts#L102)

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

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

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

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

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

```cpp
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](https://pvs-studio.ru/ru/docs/warnings/v7012/) The conditional expression always returns the same value\. [nls\-analysis\.ts 186](https://github.com/microsoft/vscode/blob/13956d46e519a5e3ab9cccf0461c9df5b5e5fd24/build/lib/nls-analysis.ts#L186)

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

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

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

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

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

```cpp
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](https://pvs-studio.ru/ru/docs/warnings/v7012/) The conditional expression always returns the same value\. [inlineEditsWordReplacementView\.ts 222](https://github.com/microsoft/vscode/blob/576cc0a5e1960ad40c8c88425e9271c79a085e05/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordReplacementView.ts#L222)

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

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

По мере рассмотрения исходного кода сложилось впечатление, что это последствия неудачного рефакторинга\. В результате следующего [коммита](https://github.com/microsoft/vscode/commit/0e4c00ab6aae9af2543215d167ff8dad46ab15e4#diff-0679a044e5a22245e5f544ec9de5a6e11b55f9d797dc9c0d6741952adb359b2eR136) было много изменений, и эти строки — одно из них\. До исправления этот фрагмент выглядел так:

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

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

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

Переходим к проекту [Overleaf](https://github.com/overleaf/overleaf)\.

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

```cpp
/**
 * 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](https://pvs-studio.ru/ru/docs/warnings/v7010/)\. The return value of function 'replace' is required to be utilized\. [sanitize\.js 14](https://github.com/overleaf/overleaf/blob/1b70f09bee2f9fbc61ba08ca4970f172d7dd0d30/services/web/scripts/translations/sanitize.js#L14)

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

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

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

```cpp
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](https://pvs-studio.ru/ru/docs/warnings/v7004/)\. The 'then' statement is equivalent to the 'else' statement\. [index\.js 39](https://github.com/overleaf/overleaf/blob/1b70f09bee2f9fbc61ba08ca4970f172d7dd0d30/services/web/frontend/js/features/autoplay-video/index.js#L39)

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

В [последующем коммите](https://github.com/overleaf/overleaf/blob/ec5a283e9ed160c587f452c742314f1f4f3c5645/services/web/frontend/js/features/autoplay-video/index.js#L63), который затрагивает этот файл, поведение было изменено, и проблема исчезла\.

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

Следующий проект — [Prisma](https://github.com/prisma/prisma)\. 

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

```cpp
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](https://pvs-studio.ru/ru/docs/warnings/v7002/) The body of a function is fully equivalent to the body of another function\. [customColors\.ts 5](https://github.com/prisma/prisma/blob/ecae3b65bbed748445c6230de583c51bb921e7df/packages/migrate/src/utils/customColors.ts#L5)

Анализатор сообщает, что содержимое функций `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\), а над цветом фона нужно подумать авторам\. 

Если вам интересно, то [здесь](https://ru.wikipedia.org/wiki/%D0%A3%D0%BF%D1%80%D0%B0%D0%B2%D0%BB%D1%8F%D1%8E%D1%89%D0%B8%D0%B5_%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%D0%B4%D0%BE%D0%B2%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D1%81%D1%82%D0%B8_ANSI) можно подробнее ознакомиться с тем, что такое управляющие последовательности ANSI и как они работают\. 

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

Продолжает нашу демонстрацию небезызвестный проект [React](https://github.com/facebook/react)\.

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

```cpp
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](https://pvs-studio.ru/ru/docs/warnings/v7004/)\. The 'then' statement is equivalent to the 'else' statement\. [InferMutationAliasingEffects\.ts 1771](https://github.com/facebook/react/blob/3cb2c42/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts#L1771)

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

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

Ну и напоследок несложная ошибка из проекта [jQuery](https://github.com/jquery/jquery):

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

Предупреждение PVS\-Studio: [V7001](https://pvs-studio.ru/ru/docs/warnings/v7001/) The operands of the '\|\|' operator are equivalent\. [gh\-1764\-fullscreen\.js 13](https://github.com/jquery/jquery/blob/6cb507faf3b6b25f0f3b5870e40a615fc19af9b1/test/integration/data/gh-1764-fullscreen.js#L13)

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

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

## Заключение

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

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

* [анализ потока данных \(data\-flow analysis\)](https://pvs-studio.ru/ru/blog/terms/7004/);
* [аннотирование функций](https://pvs-studio.ru/ru/blog/posts/1048/#ID51D242EE71);
* [межпроцедурный контекстно\-чувствительный анализ](https://pvs-studio.ru/ru/blog/terms/6785/);
* [межмодульный анализ](https://pvs-studio.ru/ru/blog/terms/6791/);
* [анализ псевдонимов](https://pvs-studio.ru/ru/blog/terms/7009/);
* [taint\-анализ \(анализ помеченных данных, taint checking\)](https://pvs-studio.ru/ru/blog/terms/6496/);
* и много\-много чего ещё\.

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

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

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

* [Go vet не поможет\. Статический анализ Golang проектов с помощью PVS\-Studio](https://pvs-studio.ru/ru/blog/posts/go/1342/)\.
* [^ \!\= <<](https://pvs-studio.ru/ru/blog/posts/go/1350/)\.

А также подобную этой [статью](https://pvs-studio.ru/ru/blog/posts/go/1329/) о том, как реализовать свой анализатор для Go\. 

Если вы хотите попробовать наши новые анализаторы, сделать это можно [здесь](https://pvs-studio.ru/ru/pvs-studio-eap/)\.

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