Анализ7 мин чтения2026-03-06

Динамический анализ кода: как он работает и почему статический недостаточно

Честно? Большинство команд думают, что запустить linter и отправить код на ревью — это достаточно. А потом в продакшене находятся баги, которые анализаторы не п

Честно? Большинство команд думают, что запустить linter и отправить код на ревью — это достаточно. А потом в продакшене находятся баги, которые анализаторы не поймали. Потому что они смотрели на код как на текст, а не как на живую программу.

Вот в чём разница. Статический анализ говорит: "Здесь переменная может быть null". Динамический анализ говорит: "Я запустил код, и вот именно в этой ситуации она действительно null, и программа падает".

Давайте разбираться, как это работает, какие инструменты использовать и как встроить в pipeline так, чтобы ловить реальные проблемы, а не шуметь попусту.

Статический анализ vs динамический: в чём суть

Сначала надо понять, что это вообще такое и чем они друг от друга отличаются.

Статический анализ — это когда инструмент смотрит на исходный код без запуска. Ищет паттерны, нарушения стиля, очевидные ошибки типов, утечки памяти. Работает быстро, не требует настройки окружения.

# Статический анализ найдёт это сразу
def process_user(user):
    return user["name"]  # Может быть KeyError, если нет ключа

x = None
y = x + 1  # Type error, очевидно

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

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

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

Пример: у тебя есть функция, которая работает правильно для 99% пользователей, но для некоторых граничных случаев падает. Статический анализ это не увидит. Динамический анализ поймает, если запустить код с правильными тестовыми данными.

def calculate_discount(price, discount_percent):
    if discount_percent > 100:
        discount_percent = 100
    return price * (1 - discount_percent / 100)

# Статический анализ: "Выглядит нормально"
# Динамический анализ при price=None: "Runtime error! TypeError"

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

Как работает динамический анализ: техника изнутри

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

Инструментирование кода — самый распространённый подход. Анализатор добавляет в код специальные проверки перед компиляцией или во время запуска. Вроде того, как если бы ты везде вставил print() для отладки, но автоматически и умно.

# Исходный код
def divide(a, b):
    return a / b

# После инструментирования (примерно)
def divide(a, b):
    if b == 0:
        LOG_ERROR("Division by zero")
    if not isinstance(a, (int, float)):
        LOG_ERROR(f"Invalid type for a: {type(a)}")
    return a / b

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

Анализ памяти — смотрит, как программа работает с памятью. Утечки памяти, двойное освобождение, обращение к неинициализированной памяти. Особенно важно для C/C++.

Например, Valgrind для Linux отслеживает каждое обращение к памяти и находит утечки:

valgrind --leak-check=full ./my_program

Анализ потоков — если в программе несколько потоков, динамический анализ может найти race conditions, deadlocks. Запускает код в разных "расписаниях" и смотрит, где может быть конфликт.

На одном проекте в Яндексе мы использовали ThreadSanitizer, чтобы найти race condition в многопоточной системе. Статический анализ это не увидел бы никогда, потому что race condition зависит от времени выполнения.

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

Выбор инструмента зависит от того, что ты пишешь. Вот что работает:

Для Python:

python -m cProfile -s cumulative my_script.py

Для JavaScript/TypeScript:

Для Java:

Для Go:

go test -race ./...

Для C/C++:

gcc -fsanitize=address -g my_program.c -o my_program
./my_program

На практике я видел, как AddressSanitizer поймал баг в C++ коде, который гулял в продакшене полгода. Утечка памяти на 100MB в день.

Встраиваем динамический анализ в CI/CD pipeline

Просто запустить анализ — это полдела. Надо встроить его в процесс так, чтобы он работал на каждый коммит и блокировал мёртвый код.

Вот как это делается в GitHub Actions (аналогично для GitLab CI):

name: Dynamic Analysis

on: [push, pull_request]

jobs:
  test-and-analyze:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'
      
      - name: Install dependencies
        run: |
          pip install -r requirements.txt
          pip install pytest pytest-cov coverage
      
      - name: Run tests with coverage
        run: pytest --cov=src --cov-report=xml --cov-report=term
      
      - name: Check coverage threshold
        run: |
          coverage report --fail-under=80
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage.xml
      
      - name: Memory profiling
        run: python -m memory_profiler my_module.py

Ключевой момент: --fail-under=80. Если покрытие кода ниже 80%, pipeline падает. PR не мёржится. Работает.

Для Java с JaCoCo:

<!-- pom.xml -->
<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.8</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
        <execution>
            <id>jacoco-check</id>
            <goals>
                <goal>check</goal>
            </goals>
            <configuration>
                <rules>
                    <rule>
                        <element>PACKAGE</element>
                        <excludes>
                            <exclude>*Test</exclude>
                        </excludes>
                        <limits>
                            <limit>
                                <counter>LINE</counter>
                                <value>COVEREDRATIO</value>
                                <minimum>0.80</minimum>
                            </limit>
                        </limits>
                    </rule>
                </rules>
            </configuration>
        </execution>
    </executions>
</plugin>

Запускается вместе с mvn test, и если покрытие ниже порога — билд падает.

Для Go в GitLab CI:

test:
  stage: test
  script:
    - go test -race -cover ./...
    - go test -race -coverprofile=coverage.out ./...
    - go tool cover -func=coverage.out
  coverage: '/coverage: \d+\.\d+%/'

Флаг -race включает детектор race conditions. Если есть хотя бы потенциальный конфликт — тест падает.

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

Давайте на конкретных примерах.

Проблема 1: Null/None в неожиданном месте

def get_user_age(user_id):
    user = fetch_from_db(user_id)  # Может вернуть None
    return user["age"]  # Статический анализ: может быть KeyError

Статический анализ скажет "опасно". Но динамический анализ покажет, при каких именно user_id это падает. И тесты помогут это поймать.

Проблема 2: Race condition в многопоточном коде

# Несколько потоков обращаются к shared_data одновременно
shared_data = {"count": 0}

def increment():
    shared_data["count"] += 1  # Не атомарная операция!

Статический анализ не поймёт, что тут проблема. Динамический анализ с правильным инструментом (типа ThreadSanitizer) это найдёт сразу.

Проблема 3: Утечка ресурсов

func readFile(filename string) ([]byte, error) {
    file, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    // Забыли file.Close()! Файловый дескриптор не освобождается
    data, _ := io.ReadAll(file)
    return data, nil
}

Статический анализ может намекнуть (если включена проверка). Динамический анализ за несколько итераций покажет растущее количество открытых файловых дескрипторов.

Проблема 4: Производительность на больших данных

def process_data(items):
    result = []
    for item in items:
        result.append(item)  # O(n) операция в цикле, итого O(n²)
    return result

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

Статический + динамический: правильная комбинация

Я видел три подхода к организации анализа кода:

  1. Только статический — быстро, дёшево, но много false positives и пропусков реальных проблем. Обычно в стартапах на первой неделе.

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

  3. Оба вместе — правильно. Статический ловит глупые ошибки за секунды. Динамический (через тесты и профилирование) ловит то, что реально ломается.

Вот примерный workflow:

Разработчик пишет код
    ↓
Локально: линтер + форматтер (статический, моментально)
    ↓
Push на GitHub/GitLab
    ↓
CI: статический анализ (SonarQube, CodeQL и т.п.) — 30 сек
    ↓
CI: юнит-тесты + coverage (динамический) — 2-5 мин
    ↓
CI: интеграционные тесты — 5-15 мин
    ↓
Если всё зелёное — код мёржится
    ↓
Периодически: профилирование памяти, производительности на нагрузочных тестах

На практике это выглядит так. У нас в одной компании было правило: любой PR должен пройти:

Результат? За год в продакшене было примерно в 3 раза меньше багов, связанных с типами данных и утечками памяти. Конечно, пришлось потратить время на написание тестов, но оно окупилось за два месяца.

Инструменты для комплексного анализа

Если хочешь "

Попробуйте Distiq для автоматического code review

AI-бот анализирует каждый MR/PR и оставляет комментарии с замечаниями. Интеграция за 2 минуты.

Попробовать бесплатно

Похожие статьи