Daily MaverickFormer ‘crown prince of the Kruger’ Park found guilty of rhino poachingThe Jerusalem PostTwo Arab Israelis, one PA resident indicted for stealing IDF shoulder-fired MATADOR missileBollywood HungamaRonit Roy leases Andheri west office space for Rs 1.39 crores over five yearsESPN📈 NFL draft QB Hot Board: Ranking the top 15 passersInquirerAnalyst: VP Duterte didn’t declare P207-M cash, total assets reach P817MESPN Deportes¿Qué necesita tu equipo de NBA? El mayor hueco en cada plantillaRTP DesportoNuno Espírito Santo eleito melhor treinador de setembro do ChampionshipBillboardHow Live Nation’s Hans Schafer Redefined Latin Touring’s Global Potential: ‘It’s Not One-Size-Fits-All’ZDF heuteAktuelle Pressemitteilungen des ZDFCollider‘Harry Potter’ Star Confirms Massive Update for HBO SeriesThe Hollywood ReporterDespite BTS Boycott, Grammys Double Down on Controversial New Asian Pop CategoryDeadlineSadie Sink And Lorene Scafaria Eyeing Adaptation Of Emma Cline’s ‘The Guest’ At A24 And Square Peg
The Daily Newsstand · Free, Always
Friday, October 9, 2026

Как мы научили JavaScript анализатор понимать поток управления

Translate

Недавно в статический анализатор PVS-Studio была добавлена поддержка JavaScript/TypeScript, и он на старте умеет искать ошибки, связанные с потоком управления. Причём тут граф потока управления, как мы его добавили и чем это полезно — читайте в этой статье.

Граф потока управления?

Я уже затрагивал эту тему, в частности, в моей серии про taint-анализ в Java, но кратко напомню. Исходный код, попадая в статический анализатор, преобразуется в абстрактное синтаксическое дерево (AST). Оно почти полностью отображает структуру кода, как он написан в редакторе. Для задач анализа потока управления, таких как поиск недостижимого кода, оно подходит плохо. На AST можно написать проверку, которая его ищет, но:

  1. Это будет работать ненадёжно в граничных случаях.

  2. Это будет плохо масштабироваться на другие подобные правила.

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

Для решения этих задач и существует граф потока управления (Control Flow Graph, CFG), который отвечает за отображение всех возможных путей в программе.

Состоит он из трёх составляющих:

  1. Узлы входа и выхода.

  2. Узлы базовых блоков, внутри которых расположены линейные инструкции.

    • Базовые блоки заканчиваются терминаторами (либо ничем). Терминаторы — особые инструкции, выражающие определённую семантику потока выполнения: ветвления, break/continue и т.п.

  3. Рёбра между узлами графа, отображающие возможные переходы потока управления.

Как мы это сделали?

Обобщённый CFG

В статье про разработку JavaScript/TypeScript анализатора мы уже упоминали, что инструмент включает в себя не только синтаксическое дерево для конкретного языка, но и обобщённое для мультиязыкового анализа. Мы назвали его CAT (Common Abstract Tree).

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

Примера таких специальных семантик для JavaScript/TypeScript два:

  • try блоки, в которых исключения не типизированные, и больше одного catch быть не может.

  • Метки вешаются на конкретную инструкцию, и к ней можно перейти только из вложенного в неё break. Также можно перейти по метке через continue, если она стоит на цикле.

Подход к тестированию

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

  • Граф постоянно меняется при разработке. Придётся переписывать падающие тесты руками либо сжигать токены ИИ-агентов.

  • Пропустить баг в эталоне очень легко. Если бы люди не делали ошибок, то индустрия статического анализа бы не существовала.

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

В итоге мы сменили подход и написали мини-интерпретатор нашего CAT на базе CFG. Идея простая: если наш интерпретатор, обойдя граф, даст тот же результат, что и интерпретатор JavaScript, то граф построен верно. Проверяется это простым Assertion API такого вида:

@Test
void branching() {
    EvaluationAssert.evaluate("branching")
                    .withParam("param", true)
                    .expect("a", 2);

    EvaluationAssert.evaluate("branching")
                    .withParam("param", false)
                    .expect("a", 0);
}

Этот тест запускает интерпретатор и сверяет результаты выполнения с эталоном, который можно получить, предварительно выполнив TypeScript код:

function branching(param: boolean) {
    let a = 1;
    if (param) {
        a++;
    } else {
        a--;
    }
}

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

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

По итогу имеем работающий строитель графов для JavaScript/TypeScript, строящийся за один проход по AST, что довольно шустро — для React все графы строится за четверть секунды.

Что уже ищем?

Линейный код

Самые рядовые опечатки, которые можно было бы искать и через AST, конечно, идут первыми:

if ( curveLengths[ i ] >= d ) {
    diff = curveLengths[ i ] - d;
    curve = this.curves[ i ];
    var u = 1 - diff / curve.getLength();
    return curve.getPointAt( u );
    break;
}

Предупреждение PVS-Studio: V7039 Unreachable code detected. Control flow never reaches this statement. three.js 29417.

Граф, который строит анализатор:

Что за merge узлы?

merge — это вспомогательный узел для построения графа, который упрощает работу с ним, поскольку обрабатывать узлы с множественными предшественниками неудобно Анализ с merge узлами (пока) не взаимодействует.

На примере выше merge имеет один вход, а не два, как на картинке ранее, так как в true есть return, который обязывает создать ребро сразу к выходу.

На нём отлично видно, что в break нет ни одного пути, поэтому он помечается как недостижимый.

Маловероятно, что эта ошибка на что-то влияет, и, скорее всего, недостижимый break просто является лишним. Но что занятно: этот файл — библиотека Three.js, скопированная внутрь Juice Shop, и такой же ошибки внутри актуального репозитория Three.js мы не нашли.

Кстати, классический баг с Automatic Semicolon Insertion вида:

function foo() {
    return // asi happens here
        this.bar
}

Будет ловиться точно таким же образом, ведь вставленная ; создаст терминатор в блоке с return и вынесет this.bar в следующий блок:

Условия

Занятный случай “защитного программирования” произошёл в Phaser:

if (!childA.parentContainer && !childB.parentContainer)
{
    return this.displayList.getIndex(childB)
      - this.displayList.getIndex(childA);
}
else if (childA.parentContainer === childB.parentContainer) {
// more branches ending with return statements here
} else
{
    var listA = childA.getIndexList();
    var listB = childB.getIndexList();
    var len = Math.min(listA.length, listB.length);

    for (var i = 0; i < len; i++)
    {
        var indexA = listA[i];
        var indexB = listB[i];

        if (indexA === indexB)
        {
            continue;
        }
        else
        {
            return indexB - indexA;
        }
    }
    return listB.length - listA.length;
}
//  Technically this shouldn't happen, but ...
// eslint-disable-next-line no-unreachable
return 0;

Предупреждение PVS-Studio: V7039 Unreachable code detected. Control flow never reaches this statement. InputPlugin.js 2981

Веток else-if было больше, но я их убрал для краткости и чтобы уменьшить граф:

Если вчитаться в комментарий и посмотреть на код, то опасение программиста должно стать понятным: выше сложная логика с 5+ ветками, каждая из которых завершает поток выполнения, поэтому он решил перестраховаться, не доверившись даже ESLint. Мы же вслед за ним по топологии графа увидели, что пути в return 0 просто нет.

Циклы

“Бесплатно” мы получаем и нахождение циклов, которые выполняются бесконечно или, наоборот, всего один раз. Такой случай попался в уже упомянутом Three.js:

loop:
for (var pos = 0; pos < limit; pos++) {
    for (; pos < limit; pos++) {                 // <=
        for (var k = 0; k < needleLength; k++) {
            if (haystack[pos + k] !== needle[k]) {
                continue loop;
            }
        }

        return pos;
    }
}

Предупреждение PVS-Studio: V7039 Unreachable code detected. Control flow never reaches this statement. opentype.module.js 6109.

Граф потока управления:

Диагностика обнаружила, что на графе инкремент переменной pos в цикле недостижим. Посмотрев на код, легко заметить, что все ветки действительно прерывают второй for, и у него будет лишь одна итерация. А на графе это подтверждается — пути в инкремент нет.

Попал этот код в проект из OpenType, но ныне скопированная зависимость уже удалена.

Обработка исключений

Случаи с вложенными try и прерыванием потока выполнения из finally не только экзотические, но часто и считаются code smell, так что сходу найти срабатывания в Open Source не вышло.

Однако это был один из самых нетривиальных случаев для обработки из-за запутанности семантики try-catch-finally. Нужно учитывать:

  • Есть ли в try секции catch, finally или обе.

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

  • Перезаписывать прерывание потока управления своим внутри finally, когда оно произошло в try или catch.

  • Каскадно пробрасывать прерывание потока управления сквозь try, если оно произошло в finally.

  • И ещё множество других граничных кейсов, а также их комбинаторные сочетания.

Так что покуда этот случай показать хочется, приведу синтетический пример:

function tryCatchFinallyCase() {
    try {
        try {
            throw new Error("Whoops");
        } finally {
            console.log("inner")
        }
        return true // V7039
    } finally {
        console.log("outer")
    }
    return false; // V7039
}

Для такой функции анализатор построит следующий граф:

На графе можно чётко увидеть, что сами рёбра несут в себе информацию о том, какую операцию они “запомнили” до выхода из функции. В то же время недостижимые return узлы идентифицируются анализатором.

Что дальше?

Мы рассмотрели ошибки, которые анализатор способен находить при помощи нового механизма. И действительно, он уже показал свою эффективность, позволив достаточно тривиально реализовать диагностики V7039 (недостижимый код) и V7040 (бесконечная рекурсия) к релизу PVS-Studio 8.00. Но помимо диагностик, перед нами открываются и новые перспективы:

  • Углубить построение CFG, распространив его на short-circuit выражения.

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

  • Сделав основу для движка анализа потока данных, можно будет начать с taint-анализа (анализ помеченных данных).

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

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

Итоги

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

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Konstantin Volohovsky. How we made JavaScript analyzer understand control flow.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.