PunchNobel Peace Prize jury treads carefully after Trump campaignCNN TürkApple iPad Mini serisine OLED dokunuşuThe Jerusalem PostHerzog says he will recommend Israelis who aided in flydubai attack for medal for civilian heroismRTP DesportoPrimeiro semestre de 2026 com maior volume de apostas de sempre em PortugalInquirerGenZennial Tour brings nat’l issues, vote discussions to GenSan youthThe South AfricanBafana Bafana vs Egypt: Salah, rotations, and what’s really at stakeUOLQuando uma eleição presidencial vai para o segundo turno? EntendaХабрКак Splash ускоряет локальные LLM на MacBBC NewsWho is the 'hero' Indian pilot who was stabbed on Israel-bound flight?Sportstar13-year-old S. Ishaan becomes youngest S14 para swimmer to double-cross Palk StraitIl Fatto QuotidianoLavoro nero e sfruttamento, 102 lavoratori irregolari: sequestrate due aziende tessili tra Napoli e SalernoSeeking AlphaEmerging Markets Bond ETF declares monthly distribution of $0.1110
The Daily Newsstand · Free, Always
Thursday, October 1, 2026

Продвинутый статический анализ в TypeScript-проектах: выходим за рамки tsc и ESLint

Translate

👋 Привет! Меня зовут Александр, я работаю фронтенд-разработчиком в компании «МегаФон». Сегодня хочу поговорить о статическом анализе TypeScript-проектов за пределами привычной связки tsc и ESLint.

tsc прежде всего отвечает за корректность типов, а ESLint находит широкий круг ошибок и нарушений принятых в проекте правил. Вместе они закрывают большинство повседневных задач, но не все проблемы удобно выражать через типы и правила линтера. Например, в проекте может потребоваться запретить зависимости между отдельными архитектурными слоями, найти экспортируемый код, который больше нигде не используется, или проследить, как непроверенные данные из HTTP-запроса доходят до потенциально опасной операции. Это уже разные задачи статического анализа, и для каждой из них есть свой подход.

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

1. Почему tsc и ESLint недостаточно?

Чтобы понять, зачем нужны продвинутые инструменты, посмотрим на ограничения tsc и ESLint.

Ограничения tsc

При проверке TypeScript создаёт внутреннее представление всего проекта — объект Program. В нём собраны исходные файлы, импорты и информация о типах. На основе Program компилятор разрешает импорты и сопоставляет типы между файлами. Поэтому tsc нельзя считать инструментом только локальной проверки. Его задача проверять правила языка и системы типов, а не любые свойства проекта. За рамками остаются:

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

  • Неиспользуемые экспорты и файлы: проверки noUnusedLocals и noUnusedParameters не заменяют поиск экспортируемых функций, типов и классов, которые больше нигде не используются.

  • Передача потенциально опасных данных: хоть tsc и умеет сужать типы внутри функций, он не отслеживает, как непроверенные пользовательские данные проходят через цепочки вызовов между модулями.

Ограничения ESLint

ESLint анализирует абстрактное синтаксическое дерево (AST) каждого проверяемого файла. Это дерево описывает структуру кода: объявления, вызовы, условия и другие элементы. При этом правилам typescript-eslint можно предоставить информацию о типах из typescript. Такая настройка позволяет выявлять не только стилистические проблемы, но и ошибки, для поиска которых нужны типы. Например, правила no-floating-promises и no-misused-promises проверяют корректную работу с Promise, а семейство no-unsafe-* находит опасные операции с any. Правило ESLint вызывается для конкретного файла, даже если ему доступны типы и символы из typescript.

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

Ниже представлена таблица сравнения уровней статического анализа:

Уровень анализа

Инструмент

Что видит

Что упускает

Абстрактное синтаксическое дерево (AST)

ESLint

Структура текущего файла, стилистика, синтаксические конструкции.

Без дополнительной настройки не использует информацию о типах.

Проверка типов

tsc

Проверка типов между файлами, выведение типов, сигнатуры.

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

ESLint с информацией о типах

ESLint + typescript-eslint

Типы и символы проекта, ошибки работы с Promise и any.

Граф зависимостей всего проекта и сложные цепочки передачи данных.

Граф зависимостей и архитектура

dependency-cruiser, knip

Граф импортов/экспортов, границы слоёв, неиспользуемые модули.

Поток значений внутри функций.

Отслеживание потока данных

Semgrep

Движение значений от места ввода до потенциально опасной операции.

Анализ между функциями и файлами в Community Edition.

2. Архитектурный контроль и границы модулей

При развитии проекта границы между слоями часто размываются, особенно когда внедряют Feature-Sliced Design (FSD), Clean Architecture или DDD (Domain-Driven Design). В спешке разработчик может импортировать React‑компонент в бизнес‑сервис или использовать инфраструктурный клиент прямо внутри доменной модели.

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

Контроль связей с помощью dependency-cruiser

Начнём с dependency-cruiser. Это локальный CLI-инструмент. Он анализирует import, require и export, строит граф зависимостей проекта и сверяет его с правилами из конфигурации.

Установим инструмент локально:

npm install --save-dev dependency-cruiser

Создадим конфигурационный файл .dependency-cruiser.cjs. Рассмотрим пример жёсткой настройки правил для Clean Architecture. Аналогичный подход можно использовать и в FSD, адаптировав правила под её слои:

/** @type {import('dependency-cruiser').IConfiguration} */
module.exports = {
  forbidden: [
    {
      name: 'no-circular',
      severity: 'error',
      comment:
        'Циклические зависимости запрещены, так как они могут приводить к проблемам с инициализацией модулей и усложняют архитектуру.',
      from: {},
      to: {
        circular: true
      }
    },
    {
      name: 'domain-isolation',
      severity: 'error',
      comment:
        'Слой Domain не должен зависеть от Infrastructure или Presentation!',
      from: {
        path: '^src/domain(?:/|$)'
      },
      to: {
        path: [
          '^src/infrastructure(?:/|$)',
          '^src/presentation(?:/|$)',
          '^src/ui(?:/|$)'
        ]
      }
    },
    {
      name: 'no-orphans',
      severity: 'warn',
      comment:
        'Обнаружен неиспользуемый файл без входящих и исходящих зависимостей.',
      from: {
        orphan: true,
        path: '^src(?:/|$)',
        pathNot: [
          '\\.d\\.ts$',
          '(^|/)index\\.(?:[cm]?ts|tsx|[cm]?js|jsx)$'
        ]
      },
      to: {}
    },
    {
      name: 'restrict-node-builtins-in-ui',
      severity: 'error',
      comment:
        'UI-компоненты не должны импортировать системные модули Node.js (fs, path и т.д.)',
      from: {
        path: '^src/(?:ui|presentation)(?:/|$)'
      },
      to: {
        dependencyTypes: ['core']
      }
    }
  ],
  options: {
    tsPreCompilationDeps: true,
    tsConfig: {
      fileName: 'tsconfig.json'
    },
    enhancedResolveOptions: {
      exportsFields: ['exports'],
      conditionNames: ['import', 'require', 'node', 'default']
    }
  }
};

Запустить проверку можно так:

npx depcruise --config .dependency-cruiser.cjs src

Если кто-то добавит в src/domain/user.ts импорт вида import { HttpClient } from '../infrastructure/http', dependency-cruiser завершит работу с ненулевым кодом выхода и выведет нарушение правила domain-isolation.

Защита инкапсуляции модулей через good-fences

Если нужно разделить монолит на логически изолированные модули и статически контролировать их публичный API без выделения каждого модуля в отдельный пакет, можно использовать good-fences.

Установим пакет:

npm install --save-dev @good-fences/api

Пакет регистрирует CLI-команду good-fences, поэтому его можно запускать через npx или npm-скрипт.

С помощью good-fences можно создать в директориях проекта файлы fence.json, которые описывают границы модулей. Поле exports определяет, какие файлы модуля доступны для импорта извне, а imports модули с какими тегами разрешено импортировать изнутри текущего модуля.

Например, модуль common можно пометить соответствующим тегом в src/modules/common/fence.json:

{ 
  "tags": ["common"]
}

А модуль auth в src/modules/auth/fence.json:

{
  "tags": ["auth"]
}

Тогда src/modules/billing/fence.json может выглядеть так:

{
 "tags": ["billing"],
 "exports": [ "index", "types" ],
 "imports": [ "common", "auth" ]
}

Такая конфигурация разрешает коду внутри billing импортировать модули, помеченные тегами common и auth, а снаружи billing доступны только его публичные модули index и types.

Запускается проверка командой:

npx good-fences src

Если разработчик попытается напрямую импортировать внутренний файл src/modules/billing/internal/calculator.ts из другого модуля, такой импорт нарушит правило exports, и good-fences завершит проверку с ошибкой.

3. Неиспользуемый код и распространение any

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

Поиск неиспользуемого кода с Knip

Правило ESLint no-unused-vars анализирует использование переменных, функций и параметров внутри отдельного файла. Оно не строит полный граф импортов проекта. Поэтому экспортируемая функция обычно не считается неиспользуемой, даже если её никто не импортирует.

Для поиска неиспользуемого кода удобно использовать Knip. Он строит граф экспортов, импортов и зависимостей проекта, включая типы, и находит неиспользуемые элементы с учётом конфигураций популярных инструментов и фреймворков (Next.js, Vite и т. д.).

Установка:

npm install -D knip typescript @types/node

Или через интерактивную команду:

npm init @knip/config

Конфигурация knip.json:

{
  "$schema": "https://unpkg.com/knip@6/schema.json",
  "entry": ["src/index.ts", "src/bin/*.ts"],
  "project": ["src/**/*.{ts,tsx}"]
}

В поле entry указаны точки входа — файлы, с которых Knip начинает обход графа проекта. От их корректного выбора зависит точность результата.

Команда для запуска:

npx knip

Что может найти Knip, чего не видит ESLint:

  1. Неиспользуемые файлы (Unused files): файлы, которые не входят в граф импортов от заданных точек входа.

  2. Неиспользуемые экспорты (Unused exports): Экспортируемые функции, классы, константы и интерфейсы, которые никто не импортирует.

  3. Неиспользуемые экспортируемые типы (Unused export types): Типы и интерфейсы, отдаваемые наружу, но не востребованные другими модулями.

  4. Неиспользуемые зависимости (Unused dependencies / devDependencies): Пакеты из package.json, использование которых Knip не обнаружил в коде, конфигурациях, скриптах и других поддерживаемых точках входа.

  5. Дублирующиеся экспорты (Duplicate exports): один и тот же элемент экспортируется более одного раза, например одновременно как именованный и экспорт по умолчанию или через повторяющиеся цепочки реэкспортов.

Измерение и контроль строгости типов с type-coverage

Проект может компилироваться без ошибок, но при этом содержать сотни явных any, а при отключённом noImplicitAny и неявных. Они появляются, например, из-за внешних библиотек без типов или злоупотребления as any.

Инструмент type-coverage получает информацию о типах из TypeScript-проекта. Он считает, какая доля идентификаторов имеет тип, отличный от any, и выводит процент покрытия типами.

Установка и запуск:

npm install -D type-coverage
npx type-coverage --at-least 95 --detail --strict

Флаги:

  • --at-least 95 — завершает команду с ошибкой, если покрытие типами ниже 95%. Благодаря этому проверку можно использовать в CI.

  • --detail — выводит файлы, строки и идентификаторы, которые инструмент считает непокрытыми типами.

  • --strict — расширяет проверку: учитывает вложенные any, например Promise<any> и any[], а также потенциально небезопасные утверждения типов, утверждения о том, что значение не равно null или undefined (!), и слишком широкие типы Object и {}.

Использование type-coverage позволяет зафиксировать минимально допустимый уровень покрытия типами при работе с унаследованным кодом и не допустить его снижения.

4. Поиск по структуре кода и отслеживание потенциально опасных данных

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

Анализ, который отслеживает движение значений по программе, называется анализом потока данных (Data Flow Analysis, DFA). Один из его вариантов — Taint Analysis, или анализ потенциально опасных данных: он прослеживает, как непроверенные значения доходят до операций, в которых могут стать опасными.

Использование Semgrep для поиска антипаттернов

Semgrep — мультиязычный инструмент статического анализа с пользовательскими правилами на YAML, синтаксис которых похож на проверяемый код. Эти правила можно запускать локально через CLI и описывать с их помощью анализ потенциально опасных данных.

В таких правилах source — место, откуда приходят потенциально опасные данные; sink — операция, в которой они могут стать опасными; sanitizer — проверка или преобразование, после которого данные считаются безопасными.

Рекомендуемый способ установки:

pipx install semgrep

На macOS также доступен Homebrew:

brew install semgrep

Пример 1: Поиск пустых блоков catch

Пустой catch перехватывает исключение, но никак его не обрабатывает:

async function loadData() {
	try {
		await requestData();
	} catch (error) {
		// Ошибка не обработана
	}
}

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

Создадим правило в файле .semgrep/rules/no-empty-catch.yaml:

rules:
  - id: javascript-no-empty-catch
    languages:
      - javascript
      - typescript
    message: >-
      Пустой блок catch подавляет исключение.
      Обработайте ошибку, залогируйте её или явно передайте дальше.
    severity: MEDIUM
    pattern-either:
      - pattern: |
          try {
            ...
          } catch ($ERR) {}
      - pattern: |
          try {
            ...
          } catch {}

Правило проверяет пустые блоки catch как в синхронном, так и в асинхронном коде. Блоки, содержащие только комментарии, также считаются пустыми, поскольку комментарий не обрабатывает исключение.

Запуск проверки:

semgrep scan --config .semgrep/rules/no-empty-catch.yaml .

Для поиска необработанных Promise, например, результатов вызовов async-функций без await, return или явной обработки результата в TypeScript-проектах лучше использовать типизированное правило ESLint:

'@typescript-eslint/no-floating-promises': 'error'

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

Пример 2: Taint Analysis — отслеживание потенциально опасных данных

Создадим правило для одного типа опасной операции: передачи пользовательских данных в unsafeExec(...) без предварительной валидации.

.semgrep/rules/taint-user-input.yaml:

rules:
  - id: taint-user-input-to-command
    languages: [typescript, javascript]
    severity: ERROR
    message: >-
      Непроверенные пользовательские данные передаются в опасную операцию.
    mode: taint
    pattern-sources:
      - pattern: req.body
      - pattern: req.query
      - pattern: req.params
    pattern-sanitizers:
      - pattern: validateCommandArgument(...)
    pattern-sinks:
      - pattern: unsafeExec(...)

Запуск проверки Semgrep локально:

semgrep --config=.semgrep/rules/

Если в коде появится цепочка:

function handleRequest(req, res) {
    const rawData = req.body.data;
    // ... промежуточный код ...
    const formatted = `${rawData}`;
    // ...
    unsafeExec(formatted);
}

Semgrep отследит распространение значения между промежуточными переменными внутри функции и укажет на вызов unsafeExec(formatted).

Способ проверки или очистки всегда зависит от конкретной опасной операции: HTML, SQL и аргументы командной оболочки требуют разных стратегий.

5. Создание собственных анализаторов через TypeScript Compiler API и ts-morph

Готовых правил линтеров иногда не хватает. Например, в компании могут быть свои архитектурные соглашения: каждый класс с декоратором @Injectable() обязан иметь явно заданный логгер в конструкторе, а все классы для передачи данных (Data Transfer Object, DTO) из папки src/dto должны состоять только из readonly полей и заканчиваться на Dto.

Решать такие задачи регулярными выражениями ненадёжно, а писать плагин для ESLint только на уровне AST громоздко. Здесь подойдёт TypeScript Compiler API — программный интерфейс к тем же синтаксическим деревьям, символам и типам, с которыми работает компилятор. Для удобства можно использовать его обёртку ts-morph.

Практическая реализация собственного анализатора на ts-morph

ts-morph упрощает обход AST — последовательную работу с узлами синтаксического дерева, а также работу с символами, типами и файлами проекта.

Установка:

npm install --save-dev ts-morph ts-node

Создадим скрипт scripts/analyze-architecture.ts. Он будет проверять два проектных правила:

  1. Имена всех DTO-классов должны иметь суффикс Dto, а все их свойства должны быть readonly.

  2. Ни один метод сервиса (*Service.ts) не должен возвращать any или неявный Promise<any>.

В этом демонстрационном примере getProperties() проверяет свойства, объявленные непосредственно в классе. Свойства, объявленные через параметры конструктора, и унаследованные свойства не учитываются. Для простоты Promise<any> ниже определяется через строковое представление типа. В анализаторе для реального проекта лучше проверять структуру типа и его аргументы через TypeScript Compiler API.

import { Project, ClassDeclaration, PropertyDeclaration } from 'ts-morph';
import * as path from 'path';

// Инициализируем проект с привязкой к tsconfig.json
const project = new Project({
  tsConfigFilePath: path.join(__dirname, '../tsconfig.json'),
});

let hasErrors = false;

function logError(filePath: string, line: number, message: string) {
  console.error(`\x1b[31m[ARCH ERROR]\x1b[0m ${filePath}:${line} - ${message}`);
  hasErrors = true;
}

console.log('Запуск анализа архитектурных правил...');

const sourceFiles = project.getSourceFiles();

sourceFiles.forEach((sourceFile) => {
  const filePath = sourceFile.getFilePath();

  // -------------------------------------------------------------------
  // Правило 1: Проверка DTO в директории src/dto
  // -------------------------------------------------------------------
  if (filePath.includes('/dto/')) {
    const classes = sourceFile.getClasses();

    classes.forEach((cls: ClassDeclaration) => {
      const className = cls.getName();

      if (!className || !className.endsWith('Dto')) {
        const line = cls.getStartLineNumber();
        logError(filePath, line, `Имя класса DTO "${className || 'Anonymous'}" должно оканчиваться на "Dto".`);
      }

      // Проверяем свойства класса
      cls.getProperties().forEach((prop: PropertyDeclaration) => {
        if (!prop.isReadonly()) {
          const line = prop.getStartLineNumber();
          logError(
            filePath,
            line,
            `Свойство "${prop.getName()}" в DTO "${className}" должно иметь модификатор readonly!`
          );
        }
      });
    });
  }

  // -------------------------------------------------------------------
  // Правило 2: Проверка методов сервисов (*Service.ts) на возвращаемый `any`
  // -------------------------------------------------------------------
  if (filePath.endsWith('Service.ts')) {
    const classes = sourceFile.getClasses();

    classes.forEach((cls) => {
      cls.getMethods().forEach((method) => {
        const returnType = method.getReturnType();
        const returnTypeString = returnType.getText();
        const methodName = method.getName();
        const line = method.getStartLineNumber();
        
        if (returnType.isAny()) {
          logError( filePath, line, `Метод "${methodName}" сервиса "${cls.getName()}" возвращает значение типа "any". Укажите точный тип!` );
          }

        if (returnTypeString === 'Promise<any>') {
          logError(
            filePath,
            line,
            `Метод "${methodName}" сервиса "${cls.getName()}" возвращает "${returnTypeString}" с вложенным "any". Укажите точный тип!`
          );
        }
      });
    });
  }
});

if (hasErrors) {
  console.error('Проверка завершена с ошибками архитектуры.');
  process.exit(1);
} else {
  console.log('Архитектурный анализ успешно пройден!');
  process.exit(0);
}

Для запуска анализатора достаточно добавить команду в package.json:

{
  "scripts": {
    "lint:arch": "ts-node scripts/analyze-architecture.ts"
  }
}

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

6. За пределами статического анализа: мутационное тестирование с StrykerJS

StrykerJS формально не относится к статическому анализу, но хорошо дополняет описанные выше автоматические проверки. С его помощью можно оценить, действительно ли тесты обнаруживают изменения поведения программы. Обычное покрытие кода (Code Coverage) показывает только факт выполнения строк во время тестов, но не гарантирует качества самих тестов.

Мутационное тестирование прежде всего оценивает качество тестов. Инструмент вносит намеренные изменения (мутации) в исходный код и запускает тесты. Если хотя бы один тест упал, мутант получает статус Killed: тесты обнаружили изменение. Если все тесты прошли, мутант получает статус Survived, что обычно указывает на пробел в тестах.

Настройка StrykerJS локально

Установим StrykerJS и пакет интеграции с Vitest:

npm install --save-dev @stryker-mutator/core @stryker-mutator/vitest-runner

Создадим stryker.config.json:

{
  "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",
  "mutate": [
    "src/**/*.ts",
    "!src/**/*.spec.ts",
    "!src/**/*.dto.ts"
  ],
  "testRunner": "vitest",
  "reporters": ["html", "clear-text", "progress"],
  "coverageAnalysis": "perTest"
}

При замене условий (например, if (a > b) на if (a >= b) или return true на return false) Stryker покажет, какие изменения в логике не обнаруживаются существующими тестами.

7. Настройка локального окружения для разработки и интеграция проверок в CI/CD

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

Поэтому проверки удобно разделить на несколько уровней в зависимости от стоимости их выполнения:

  1. Перед коммитом — быстрые локальные проверки: ESLint для файлов, добавленных в коммит, и инкрементальная проверка tsc.

  2. Перед отправкой изменений и в CI для Pull Request — проектные проверки: Knip, dependency-cruiser, Semgrep, ts-morph, type-coverage.

  3. По расписанию — ресурсоёмкие проверки: StrykerJS и более тяжёлые проверки безопасности.

Настройка Git-хуков через Husky и lint-staged

Git-хук — команда, которую Git автоматически запускает перед определённым событием. Здесь с помощью Husky и lint-staged подключим быстрые локальные проверки к этапу коммита:

1 - Установка Husky и lint-staged:

npm install -D husky lint-staged
npx husky init

2 - Настроим .husky/pre-commit:

npx lint-staged

3 - Создадим lint-staged.config.mjs:

export default {
  '*.{ts,tsx}': [
    'eslint --fix',
    () => 'tsc --noEmit'
  ]
};

ESLint будет запускаться только для TypeScript-файлов, добавленных в коммит. tsc --noEmit здесь проверяет весь TypeScript-проект, а не только эти файлы, но запускается, только если в коммит попали файлы .ts или .tsx. На большом проекте проверку типов может быть разумнее перенести на этап перед отправкой изменений или в CI.

Запуск проектных проверок в CI

Следующий уровень — проверки, которые выполняются при создании или обновлении Pull Request.

Например, в package.json можно собрать их в отдельные команды:

{
  "scripts": {
    "lint:arch": "depcruise --config .dependency-cruiser.cjs src && ts-node scripts/analyze-architecture.ts",
    "lint:semgrep": "semgrep scan --config .semgrep/rules/ .",
    "lint:knip": "knip",
    "type-coverage": "type-coverage --at-least 95 --detail --strict",
    "check:deep": "npm run lint:arch && npm run lint:semgrep && npm run lint:knip && npm run type-coverage"
  }
}

В обычном проекте CI может выполнять:

npm ci
npm run check:deep

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

Тяжёлые проверки по расписанию

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

8. Заключительная сводная матрица инструментов

Сведём все разобранные решения в единую матрицу выбора:

Инструмент

Какую проблему решает

На каком этапе применять

Скорость работы

tsc

Проверка типов между файлами

Перед коммитом / в IDE

⚡⚡⚡ Высокая (при incremental)

ESLint + @typescript-eslint

Правила по синтаксическому дереву и проверки с информацией о типах

Перед коммитом / в IDE

⚡⚡⚡ Высокая

dependency-cruiser

Контроль архитектурных слоёв и циклических зависимостей

Перед отправкой / в CI для PR

⚡⚡ Средняя

good-fences

Контроль публичных границ и импортов между модулями

Перед отправкой / в CI для PR

⚡⚡ Средняя

Knip

Поиск неиспользуемого кода, экспортов и лишних пакетов

Перед отправкой / в CI для PR

⚡⚡ Средняя

type-coverage

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

В CI для PR

⚡⚡ Средняя

Semgrep (CLI)

Поиск антипаттернов и отслеживание потенциально опасных данных

В CI для PR

⚡⚡ Средняя

ts-morph (собственные правила)

Проверка узкоспециализированных бизнес- и архитектурных правил

В CI для PR

⚡⚡ Средняя

StrykerJS

Динамическая оценка эффективности тестов методом мутаций

По расписанию

🐢 Низкая

Скорость указана относительно и может существенно зависеть от размера проекта, конфигурации анализатора и использования кэша.

Заключение

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

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

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.