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

Для нашего статического анализатора мы поддерживаем довольно большое количество интеграций в различные инструменты, в том числе в IDE, чтобы разработчики могли без проблем пользоваться инструментом в процессе разработки. Одна из таких интеграций — расширение для Visual Studio Code, написанное на JavaScript и TypeScript.

Примечание. О том, как пользоваться расширением PVS-Studio для Visual Studio Code, можно прочитать в соответствующем разделе нашей документации.

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

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

Расследование ведут единороги

Итак, начнём наш небольшой детектив с того, что именно сломалось.

Для запуска различных действий в плагине используется панель команд Visual Studio Code. Она открывается по сочетанию клавиш Ctrl+Shift+P. Соответственно, во фреймворке для тестирования, который мы используем, предусмотрен функционал для взаимодействия с этой панелью.

Говорю я про неё, потому что на скриншотах, сохранившихся в CI/CD после падения тестов, было видно, что панель команд в Visual Studio Code открылась, но дальше ничего не происходило:

Стало понятно, в какую сторону копать, но всё ещё неясно, в чём конкретно заключается проблема.

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

Итак, вот метод, отвечающий за взаимодействие с панелью команд:

async openCommandPrompt(): Promise<QuickOpenBox | InputBox> {
  const webview = await new EditorView()
                    .findElements(
                      EditorView.locators.EditorView.webView 
                    );
  if (webview.length > 0) {
    const tab = await new EditorView().getActiveTab();
    if (tab) {
      await tab.sendKeys(Key.F1);
      return await InputBox.create();
    }
  }
  const driver = this.getDriver();
  await driver.actions()
          .keyDown(Workbench.ctlKey)
          .keyDown(Key.SHIFT)
          .sendKeys('p')
          .perform();

  if (Workbench.versionInfo.version >= '1.44.0') {
    return await InputBox.create();
  }
  return await QuickOpenBox.create();
}

В этом методе фреймворк получает нужные объекты, эмулирует нажатие необходимого сочетания клавиш с помощью веб-движка, а после, в зависимости от используемой версии Visual Studio Code, создаёт объекты, в которые будут переданы необходимые команды: InputBox для более новых версий или QuickOpenBox для версий постарше.

Всё выглядит довольно прилично, но всего одна строка и привела к поломке:

...
if (Workbench.versionInfo.version >= '1.44.0') {...}
...

Здесь мы, собственно, сравниваем версии, чтобы понять, какой объект для взаимодействия с панелью команд необходимо использовать. Обратите внимание, что версия указана строкой.

А теперь мы зайдём на сайт Visual Studio Code и посмотрим, какая версия является самой новой:

Давайте попробуем выполнить сравнение с этим значением:

console.log("1.105.0" >= "1.44.0") // false

Поскольку версия хранится в строке, мы сравниваем числа лексикографически. Т. е. мы посимвольно двигаемся слева направо, сравнивая коды символов в Unicode.

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

В результате такого сравнения фреймворк выбрал неправильный способ передачи команд в панель команд Visual Studio Code, и тесты намертво зависли.

Решением этой проблемы могло бы стать хранение версии в объекте, а не в строке:

interface VSCodeVersion {
  major: Number,
  minor: Number
}

И данная проблема была пофикшена в новых версиях фреймворка использованием отдельной библиотеки для проверки версий.

Динамическая типизация

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

JavaScript — это язык с динамической типизацией, поэтому с типами в нём порой творится настоящая магия.

Например, если мы хотим максимально точно сравнить два значения, следует использовать оператор ===. При его использовании мы также проверяем, что типы объектов одинаковые.

Более привычный же для других языков оператор == при сравнении выполняет приведение типов, что позволяет даже значениям разных типов при сравнении выдавать true.

Например, мы можем сравнить true и 1:

console.log(true == 1);   // true 
console.log(true === 1);  // false

Поскольку true при приведении типов будет равно 1, результатом сравнения с приведением типов будет true.

Таким же образом мы можем сравнить число и строку:

console.log("5" == 5);   // true
console.log("5" === 5);  // false

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

Подобные проблемы в проекте может помочь выловить, например, статический анализатор кода.

Заключение

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

А статья про JavaScript, кстати, не просто же так попала в блог PVS-Studio. Кто знает, может, совсем скоро в PVS-Studio появится анализатор для JavaScript и TypeScript...

Чистого вам кода, друзья!

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Valerii Filatov. JavaScript failed your tests.

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


  1. fransua
    17.11.2025 12:00

    Динамическая типизация тут ни при чем. Хранить версию в виде строки можно и в java и в C.
    PVS-Studio умеет отлавливать такие "ошибки"?


    1. feeelin Автор
      17.11.2025 12:00

      Отвечу цитатой из текста выше:

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

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

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

      В PVS-Studio (для C, C++, C# или Java) у нас пока нет подобных диагностических правил, но мы отложили идею в копилку. Отмечу, что у нас есть множество других правил, которые можно посмотреть на этой странице.


  1. jooher
    17.11.2025 12:00

    Непонятно, к чему вся эта драматическая история про тесты, если все сводится ровно к "мы тупо сравнили строки, не учитывая их специфики". При чем тут вообще js? А в каком другом языке есть встроенный тип "несколько целых через точку"?

    Решением этой проблемы могло бы стать хранение версии в объекте, а не в строке:

    interface VSCodeVersion {  major: Number,  minor: Number}

    А ничо тот факт, что у вас там номер версии из 3х чисел? И вообще-то это банальные массивы интов - которые и нужно сравнивать как таковые


    1. Goodzonchik
      17.11.2025 12:00

      Хранить версию в строке, это нормально. Потому что будут всякие alpha, релиз-кандидаты и прочие версии, например "1.0.0rc".

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


      1. aamonster
        17.11.2025 12:00

        Магию строки не надо, а то окажется, что 106<44.


    1. feeelin Автор
      17.11.2025 12:00

      Непонятно, к чему вся эта драматическая история про тесты, если все сводится ровно к "мы тупо сравнили строки, не учитывая их специфики".

      Драматическая история про тесты — реальный опыт, которым принято делиться в сообществе разработчиков, вы так не считаете?

      Отмечу, что ошибка, про которую вы написали, была не в нашем коде, а во внешнем инструменте. Также мы написали о том, как разработчики этого инструмента её пофиксили.

      При чем тут вообще js? А в каком другом языке есть встроенный тип "несколько целых через точку"?

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

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

      А ничо тот факт, что у вас там номер версии из 3х чисел? И вообще-то это банальные массивы интов - которые и нужно сравнивать как таковые

      Окак…

      Я предложил один из вариантов. Конечно, не могу вам запрещать использовать массив в таком случае. В моём представлении решение проблемы через небольшой объект будет лучше с точки зрения читаемости кода.

      Два числа в объекте версии из-за того, что Visual Studio Code (про который и была речь) третьим числом в версии всегда имеет 0, и в этом контексте его можно смело пропустить.


      1. jooher
        17.11.2025 12:00

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

        Но в заголовке прямо обвинен именно js. Пинать js за неявное и неочевидное новичку приведение типов, конечно, очень модно и весело, но в данном случае даже оно не при делах.

        Для JS/TS в тексте упомянута решающая проблему библиотека, к которой и прибегли разработчики фреймворка

        Описанная "проблема" не стоит выеденного яйца. Функция, корректно сравнивающая строки с учётом специфики формата, пишется на js в пару строк.

        решение проблемы через небольшой объект будет лучше с точки зрения читаемости кода.

        Не будет. Такой формат с точками семантически означает именно массив убывающих по значимости "слогов" (числовых ли, числоподобных ли, с буквами или без - это уж как захотите) в общем случае произвольной длины. Объект с двумя полями - это высосанное из пальца ограничение, ломающееся в вашем же собственном примере.


  1. acsent1
    17.11.2025 12:00

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


  1. SergeyEgorov
    17.11.2025 12:00

    Блин! Всегда считал PVS-Studio серьезной компанией, где работают настоящие профессионалы. И тут такой разочаровывающий заголовок!


    1. rock
      17.11.2025 12:00

      Почти слово в слово, что я хотел написать.