Качество кода

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

В качестве бонусной темы мы также разберём регулярные выражения – сквозную тему, которая находит применение и в вопросах качества кода (например, для запуска подмножества тестов, соответствующих шаблону), и в других областях вроде IDE (например, для поиска и замены).

Многие из этих инструментов привязаны к конкретному языку (например, линтер/форматтер Ruff для Python). Некоторые инструменты поддерживают сразу несколько языков (например, форматтер кода Prettier). Однако сами концепции почти универсальны – форматтеры кода, линтеры, библиотеки для тестирования и так далее найдутся для любого языка программирования.

Форматирование

Автоформаттеры кода автоматически приводят в порядок внешний вид синтаксиса. Благодаря этому вы можете сосредоточиться на более глубоких и сложных задачах, а рутинные мелочи автоформаттер возьмёт на себя: единообразие ' и " в синтаксисе строк, пробелы вокруг бинарных операторов (x + y вместо x+y), отсортированный порядок инструкций import, отсутствие слишком длинных строк. Одно из главных достоинств форматтеров кода в том, что они задают единый стиль кода для всех разработчиков, работающих над кодовой базой.

Некоторые инструменты, такие как Prettier, гибко настраиваются; файл конфигурации для вашего проекта стоит добавить в контроль версий. Другие, такие как Black и gofmt, настраиваются слабо или не настраиваются вовсе – чтобы было меньше споров о мелочах (bikeshedding).

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

Линтинг

Линтеры выполняют статический анализ (анализируют ваш код, не запуская его), чтобы найти в нём антипаттерны и потенциальные проблемы. Эти инструменты копают глубже автоформаттеров и не ограничиваются внешним видом синтаксиса. Глубина анализа зависит от конкретного инструмента.

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

У хороших линтеров есть встроенная справка или документация, объясняющая каждое правило – что именно оно ищет, чем это плохо и чем такой паттерн кода лучше заменить. Посмотрите, например, документацию правила SIM102 в Ruff, которое ловит излишне вложенные операторы if в коде на Python.

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

Помимо линтеров для конкретного языка, может пригодиться и semgrep – инструмент «семантического grep», который работает на уровне AST (а не на уровне символов, как grep) и поддерживает множество языков. С помощью semgrep легко писать собственные правила линтера для своих проектов. Например, если вы хотите запретить опасный subprocess.Popen(..., shell=True) в Python, найти этот паттерн кода можно так:

semgrep -l python -e "subprocess.Popen(..., shell=True, ...)"

Тестирование

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

Тесты можно писать для фрагментов кода на разных уровнях детализации: модульные (юнит-)тесты для отдельных функций, интеграционные тесты для взаимодействия между модулями или сервисами и функциональные тесты для сквозных (end-to-end) сценариев. Можно практиковать разработку через тестирование (test-driven development), при которой сначала пишут тесты и лишь потом – код реализации. Когда вы находите в коде баги, можно писать регрессионные тесты – так вы заметите, если эта функциональность когда-нибудь сломается в будущем. Можно писать тесты на основе свойств (property-based tests) – подход, впервые появившийся в QuickCheck для Haskell и реализованный во многих библиотеках, например в Hypothesis для Python. Какой подход к тестированию правильный, зависит от вашего проекта; скорее всего, вы будете использовать какую-то комбинацию.

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

Покрытие кода

Покрытие кода (code coverage) – это метрика, с помощью которой можно оценить, насколько хороши ваши тесты. При подсчёте покрытия учитывается, какие строки вашего кода выполняются при прогоне тестов, – так вы можете убедиться, что охватываете все пути выполнения кода. Инструменты покрытия кода умеют показывать покрытие построчно и тем самым направлять вас при написании тестов. Сервисы вроде Codecov предоставляют веб-интерфейсы для отслеживания и просмотра покрытия кода на протяжении всей истории проекта.

Как и любая метрика, покрытие кода не идеально; не зацикливайтесь на покрытии – сосредоточьтесь на написании качественных тестов.

Pre-commit-хуки

Pre-commit-хуки Git, работу с которыми упрощает фреймворк pre-commit, автоматически запускают заданный пользователем код перед каждым коммитом Git. Проекты обычно используют pre-commit-хуки, чтобы автоматически запускать перед каждым коммитом форматтеры и линтеры, а иногда и тесты, – так гарантируется, что закоммиченный код соответствует стилю кода проекта и не содержит определённых проблем.

Непрерывная интеграция

Сервисы непрерывной интеграции (CI), такие как GitHub Actions, могут запускать скрипты за вас каждый раз, когда вы пушите код (или на каждый пул-реквест, или по расписанию). Разработчики обычно используют CI-сервисы для запуска инструментов качества кода, включая форматтеры, линтеры и тесты. Для компилируемых языков можно проверять, что код компилируется; для статически типизированных – что он проходит проверку типов. Запуск CI на каждый пуш новых коммитов помогает ловить ошибки, попавшие в основную версию кода; запуск на пул-реквестах – проблемы в присланных контрибьюторами изменениях; запуск по расписанию – проблемы с внешними зависимостями (например, когда разработчик случайно выпускает ломающее изменение как semver-совместимое).

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

Как правило, скрипт, работающий в CI, не вносит изменения в ваш код напрямую: он запускает инструменты в режиме «только проверка», а не «исправление», поэтому, например, автоформаттер выдаст ошибку, если код не соответствует формату.

Репозитории часто включают в README бейджи статуса, показывающие статус CI и другую информацию, например покрытие кода. Вот, к примеру, текущий статус сборки Пропущенного семестра.

Статус сборки Статус ссылок

Наш проверяльщик ссылок, использующий GitHub Action proof-html, часто падает – обычно из-за неполадок на сторонних сайтах. И всё же он помог нам найти и исправить множество битых ссылок (иногда возникших из-за опечаток, но чаще всего из-за того, что сайты перемещают контент, не настраивая редиректы, или исчезают совсем).

Хороший способ разобраться в тонкостях CI-сервисов, форматтеров, линтеров и библиотек для тестирования – учиться на примерах. Найдите качественные open-source-проекты на GitHub – чем ближе они к вашему проекту по языку программирования, предметной области, размеру, охвату и так далее, тем лучше – и изучите их pyproject.toml, .github/workflows/, DEVELOPMENT.md и другие подходящие файлы.

Непрерывное развёртывание

Непрерывное развёртывание использует инфраструктуру CI, чтобы действительно развёртывать изменения. Например, репозиторий Пропущенного семестра использует непрерывное развёртывание на GitHub Pages: каждый раз, когда мы делаем git push с обновлёнными конспектами лекций, сайт автоматически собирается и развёртывается. В CI можно собирать и другие виды артефактов, например бинарники приложений или Docker-образы сервисов.

Раннеры команд

Раннеры команд (command runners) вроде just упрощают запуск команд в контексте проекта. Выстраивая инфраструктуру качества кода для своего проекта, вы вряд ли захотите заставлять разработчиков запоминать команды вроде uv run ruff check --fix. С раннером команд это превращается в just lint, и по аналогии можно завести just format, just typecheck и т. д. – для всех инструментов, которые разработчику может понадобиться запускать в вашем проекте.

У некоторых менеджеров проектов или пакетов для конкретного языка такая функциональность встроена – тогда инструмент вроде just, не привязанный к конкретному языку, не нужен. Например, её поддерживают секция scripts в package.json у npm (Node.js) и секции tool.hatch.envs.*.scripts в pyproject.toml у Hatch (Python).

Регулярные выражения

Регулярные выражения, которые часто сокращают до «regex», – это язык для представления множеств строк. Regex-шаблоны широко применяются для поиска по шаблону в самых разных контекстах, от инструментов командной строки до IDE. Например, ag поддерживает regex-шаблоны для поиска по всей кодовой базе (скажем, ag "import .* as .*" найдёт все переименованные импорты в Python), а go test поддерживает опцию -run [regexp] для выбора подмножества тестов. Кроме того, в языках программирования есть встроенная поддержка или сторонние библиотеки для сопоставления с регулярными выражениями, так что regex можно использовать для таких задач, как поиск по шаблону, валидация и парсинг.

Чтобы помочь вам развить интуицию, ниже мы приводим несколько примеров regex-шаблонов. В этой лекции мы используем синтаксис регулярных выражений Python. Существует много диалектов regex с небольшими различиями, особенно в более сложной функциональности. Для разработки и отладки регулярных выражений можно пользоваться онлайн-тестером вроде regex101.

Синтаксис regex

Полное руководство по синтаксису regex можно найти в этой документации (или в одном из множества других ресурсов в интернете). Вот некоторые базовые строительные блоки:

Группы захвата и ссылки на них

Если вы используете regex-группы (...), к отдельным частям совпадения можно обращаться – чтобы извлечь их или выполнить поиск с заменой. Например, чтобы вытащить из даты в формате YYYY-MM-DD только месяц, можно воспользоваться таким кодом на Python:

>>> import re
>>> re.match(r"\d{4}-(\d{2})-\d{2}", "2026-01-14").group(1)
'01'

В текстовом редакторе можно ссылаться на группы захвата (capture groups) в шаблонах замены. В разных IDE синтаксис может различаться. Например, в VS Code для ссылок на группы используются переменные вроде $1, $2 и т. д., а в Vim – \1, \2 и т. д.

Ограничения

Регулярные языки мощны, но ограничены: существуют классы строк, которые нельзя выразить стандартным regex (например, невозможно написать регулярное выражение, соответствующее множеству строк {a^n b^n | n ≥ 0} – множеству строк из некоторого числа «a», за которыми следует столько же «b»; если ближе к практике, то языки вроде HTML не являются регулярными). На деле современные regex-движки поддерживают такие возможности, как опережающие проверки (lookahead) и обратные ссылки (backreferences), выходящие за пределы регулярных языков, и на практике они чрезвычайно полезны, но важно понимать, что их выразительная мощность всё равно ограничена. Для более сложных языков может понадобиться более мощный класс парсеров (один из примеров – pyparsing, PEG-парсер).

Изучение regex

Мы рекомендуем освоить основы (то, что мы разобрали в этой лекции), а дальше заглядывать в справочники по regex по мере необходимости, а не заучивать весь язык целиком.

Диалоговые ИИ-инструменты могут неплохо помогать с генерацией regex-шаблонов. Например, попробуйте отправить своей любимой LLM такой промпт:

Write a Python-style regex pattern that matches the requested path from log lines from Nginx. Here is an example log line:

169.254.1.1 - - [09/Jan/2026:21:28:51 +0000] "GET /feed.xml HTTP/2.0" 200 2995 "-" "python-requests/2.32.3"

Упражнения

  1. Настройте форматтер, линтер и pre-commit-хуки для проекта, над которым вы работаете. Если ошибок много – с ошибками форматирования должно справиться автоформатирование, а вот ошибки линтера попробуйте исправить с помощью ИИ-агента. Убедитесь, что ИИ-агент может запускать линтер и видеть результаты, – тогда он сможет работать в итеративном цикле, пока не исправит все проблемы. Внимательно проверьте результаты и убедитесь, что ИИ не сломал ваш код!
  2. Изучите библиотеку для тестирования на языке, который вы знаете, и напишите модульный тест для проекта, над которым работаете. Запустите инструмент покрытия кода, сгенерируйте отчёт о покрытии в формате HTML и изучите результаты. Сможете ли вы найти покрытые строки? Скорее всего, покрытие вашего кода окажется очень низким. Попробуйте вручную написать несколько тестов, чтобы его улучшить. Попробуйте улучшить покрытие с помощью ИИ-агента; убедитесь, что агент для программирования может запускать тесты с измерением покрытия и получать построчный отчёт о покрытии – так он будет знать, на чём сосредоточиться. Действительно ли хороши тесты, сгенерированные ИИ?
  3. Настройте для проекта, над которым вы работаете, непрерывную интеграцию, запускающуюся при каждом пуше. Пусть CI выполняет форматирование, линтинг и тесты. Намеренно сломайте свой код (например, добавьте нарушение правила линтера) и убедитесь, что CI это отлавливает.
  4. Попробуйте написать regex-шаблон и с помощью инструмента командной строки grep найти вхождения subprocess.Popen(..., shell=True) в своём коде. Теперь попробуйте «сломать» этот regex-шаблон. Продолжает ли semgrep успешно находить опасный код, на котором спотыкается ваш вызов grep?
  5. Потренируйтесь в regex-поиске с заменой в своей IDE или текстовом редакторе: замените маркеры пунктов списка Markdown - на маркеры * в этом конспекте лекции. Обратите внимание: просто заменить в файле все символы «-» было бы неправильно – этот символ много где используется не в роли маркера пункта списка.
  6. Напишите регулярное выражение, которое извлекает из JSON-структур вида {"name": "Alyssa P. Hacker", "college": "MIT"} имя (в этом примере – Alyssa P. Hacker). Подсказка: с первой попытки у вас, скорее всего, получится regex, извлекающий Alyssa P. Hacker", "college": "MIT; почитайте про жадные квантификаторы в документации по regex в Python, чтобы разобраться, как это исправить.
    1. Сделайте так, чтобы regex-шаблон работал даже тогда, когда в имени встречается символ " (двойные кавычки в JSON можно экранировать как \").
    2. На практике мы не рекомендуем использовать регулярные выражения для сложных задач парсинга. Разберитесь, как решить эту задачу с помощью JSON-парсера вашего языка программирования. Напишите программу командной строки, которая принимает на вход через stdin JSON-структуру описанного выше вида и выводит на stdout имя. Для этого должно хватить пары строк кода. На Python это легко делается в одну строку кода, не считая import json.

Редактировать страницу.

Лицензия CC BY-NC-SA.