analysis_options.yaml лежит в корне пакета и настраивает анализатор Dart: какой набор правил наследуется, какие дополнительные линты включены, с какой строгостью сообщается каждая диагностика, какие файлы пропускаются и какая ширина строки у форматтера.
**include:**: наследует опубликованный набор правил, например package:flutter_lints/flutter.yaml
**linter: rules:**: включает дополнительные правила или выключает унаследованное через rule_name: false
**analyzer: errors:**: переводит любую диагностику в error, warning, info или ignore
**analyzer: exclude:**: пропускает сгенерированные файлы вроде **/*.g.dart
**analyzer: language:**: более строгие проверки типов: strict-casts, strict-inference, strict-raw-types
**formatter:**: page_width для dart format, общий для IDE, CLI и CI
Этот файл читают инструменты Dart (анализатор, dart fix, dart format, IDE); компилятор его игнорирует, поэтому настоящая ошибка компиляции уронит сборку, что бы ни было написано в файле.
FlutterMiddle
Линты, анализатор и ревью кода
analysis_options, наборы линтов, dart fix, dart format, custom_lint, аннотации, ревью PR
Закрыто вопросов
0 / 21
- 0Повторить
Вопросы
dart analyze сообщает о проблемах, не трогая код, dart fix применяет автоматические исправления, которые анализатор знает для этих проблем, а dart format переписывает пробелы и переносы строк в единый официальный стиль.
**dart analyze**: выводит ошибки, предупреждения, info и линты; завершается с ненулевым кодом на ошибках и предупреждениях, а на info завершается только с --fatal-infos. flutter analyze запускает тот же инструмент, но info в нём фатальны по умолчанию
**dart fix**: --dry-run показывает, что будет изменено, --apply вносит изменения, --code=<rule> ограничивает их одним правилом. Он же применяет миграции API, которые пакеты поставляют в fix_data.yaml
**dart format**: настроек почти нет, кроме ширины строки; --output=none --set-exit-if-changed превращает его в проверку для CI
Порядок: сначала fix, потом format, потом analyze: исправление может оставить строку, которую нужно переформатировать
Шесть info: нет key, конструктор мог бы быть const, вызов print, Container, который служит лишь SizedBox, Container, который ничего не делает, и цвет со слишком коротким hex. flutter analyze считает info фатальными, так что CI упадёт.
**use_key_in_widget_constructors**: добавьте super.key: const Greeting({super.key, this.name});
**prefer_const_constructors_in_immutables**: виджеты помечены @immutable, поэтому конструктор должен быть const; тогда вызывающий код сможет писать const Greeting()
**avoid_print**: print работает и в релизных сборках; используйте log из dart:developer или пакет для логирования
**sized_box_for_whitespace**: Container только с размером и дочерним виджетом заменяется на SizedBox, он легче и может быть const
**avoid_unnecessary_containers**: Container без свойств добавляет элемент впустую; используйте Text напрямую
**use_full_hex_values_for_flutter_colors**: Color(0xFF00) означает 0x0000FF00: альфа 00, полностью прозрачный зелёный, и звезду не видно. Пишите все 8 цифр: Color(0xFF00FF00)
Все три представляют собой пакеты с готовым набором правил, который подключается через include: lints содержит базовый набор от команды Dart, flutter_lints добавляет поверх него правила для Flutter и приходит с flutter create, а very_good_analysis даёт гораздо более строгий сторонний набор.
**lints**: core.yaml (около 35 правил, нарушение которых почти всегда баг) и recommended.yaml (общепринятый стиль сверху); рассчитан на чистые Dart-пакеты
**flutter_lints**: lints/recommended плюс 10 правил для Flutter: avoid_print, use_key_in_widget_constructors, use_build_context_synchronously, sized_box_for_whitespace и другие
**very_good_analysis**: около 200 правил плюс strict-casts, strict-inference и strict-raw-types; добавляет public_member_api_docs, lines_longer_than_80_chars, prefer_const_constructors, unawaited_futures
Как выбрать: flutter_lints плюс несколько правил, включённых вручную, дают распространённую золотую середину; строгий набор подходит библиотекам и командам, которые хотят, чтобы любой спор о стиле решал инструмент
Обновления: новые правила появляются в мажорных версиях, поэтому поднимайте версию пакета отдельным PR и запускайте в нём dart fix --apply
В секции analyzer: errors: любой код диагностики можно перевести в error, warning, info или ignore. Повышайте правила, нарушение которых означает баг в рантайме, невидимый ревьюеру в диффе; стилевые правила оставляйте на info.
По умолчанию: линты сообщают на уровне info; dart analyze падает на ошибках и предупреждениях, а flutter analyze падает ещё и на info. Строгость меняет код выхода и цвет в IDE, а не то, что находится
Стоит повышать: use_build_context_synchronously, unawaited_futures, unrelated_type_equality_checks, hash_and_equals: нарушение каждого из них означает краш или незаметно неверный результат
Не стоит повышать: public_member_api_docs, lines_longer_than_80_chars: стилевое правило, которое роняет сборку, глушат массовыми ignore; оставьте его на info и исправляйте постепенно
Сначала включите, потом повышайте: errors: только задаёт строгость; правило, которого нет в linter: rules:, ничего не сообщит ни на каком уровне
Обратная ошибка: deprecated_member_use: ignore держит CI зелёным ценой единственного напоминания, что миграция ещё не сделана
**cannot-ignore:**: список кодов, которые нельзя заглушить никаким комментарием // ignore
Небольшая группа: линты, которые ловят всегда ложные сравнения, сломанное равенство, future без await, BuildContext после того, как виджет уже исчез, и секреты, напечатанные в лог устройства. Почти всё остальное касается читаемости.
**unrelated_type_equality_checks, collection_methods_unrelated_type**: '7' == 7 и ids.contains('7') на List<int> компилируются и всегда дают false
**hash_and_equals**: если переопределить == без hashCode, Set или Map будут хранить два «равных» объекта
**unawaited_futures**: забытый await в async-функции: ошибка проходит мимо вашего catch, а следующая строка выполняется до того, как запись завершилась. Проверяет только async-тела; синхронные покрывает discarded_futures. Ни одного из них нет в flutter_lints
**use_build_context_synchronously**: context используется после await, когда виджет уже может быть размонтирован; сначала проверьте context.mounted
**avoid_print**: print попадает в релизную сборку, поэтому токены и тела ответов оказываются в logcat и в консоли iOS
ApiFailure представляет собой обычный enum, а не Exception, поэтому on Exception catch его не ловит. Ошибка вылетает из loadProfile: экран остаётся в состоянии загрузки, а ApiFailure.notFound всплывает как необработанная ошибка.
Почему так происходит: в Dart можно бросить через throw любой не-null объект, а on T сопоставляет только по типу. ApiFailure не реализует Exception и не наследует Error, поэтому блок его пропускает; при вызове из initState или обработчика кнопки упавший Future превращается в необработанную асинхронную ошибку
Исправление: enum ApiFailure implements Exception, а для класса пишите class ApiFailure implements Exception. Места throw и сам catch остаются как есть
Как поймать в следующий раз: включите only_throw_errors: он сообщает о каждом throw объекта, который не является Exception или Error. Он есть в very_good_analysis, но не в flutter_lints
**Не заменяйте на голый catch (e)**: он заодно глотает TypeError и StateError, а это баги, которые нужно чинить, а не состояния для показа; такой код отмечает avoid_catches_without_on_clauses
Тест: MockClient из package:http/testing.dart, который отвечает 404, и затем expect(profile.value, isA<ProfileError>())
// ignore: глушит одно правило в одной строке, // ignore_for_file: глушит его во всём файле, включая код, который напишут позже, а exclude: полностью убирает файлы из анализа вместе с ошибками. На ревью принимайте самую узкую форму с причиной рядом.
**// ignore: rule: на строке выше или в конце той же строки; ревьюер видит ровно то, что скрыто
// ignore_for_file: rule**: нормально для сгенерированного или вендоренного кода; в написанном руками коде заодно скрывает все будущие нарушения. ignore_for_file: type=lint глушит сразу все линты
**analyzer: exclude:**: подходит для *.g.dart и build/; не подходит для легаси-папки, которую вы собираетесь чистить, потому что настоящие ошибки там тоже пропадут
Ignore может скрыть ошибку: // ignore: invalid_assignment делает dart analyze зелёным, а компилятор эту строку всё равно отвергнет
Держите их честными: включите unnecessary_ignore, чтобы видеть ignore, диагностики под которыми уже нет, и перечислите правила, которые нельзя глушить, в cannot-ignore:
Это метаданные, которые читает анализатор: @override из dart:core, остальные из package:meta, который реэкспортирует foundation во Flutter. Нарушение даёт предупреждения анализатора, а не ошибки компиляции: dart analyze в CI на них падает, а flutter run и flutter build код всё равно соберут.
**@override**: помечает намеренное переопределение; если переопределять нечего (скажем, метод базового класса переименовали), появится override_on_non_overriding_member. Линт annotate_overrides делает аннотацию обязательной
**@immutable**: все поля класса и его наследников должны быть final (must_be_immutable); проверка поверхностная, так что final List всё равно можно изменить
**@protected: можно использовать только в своей библиотеке и в методах экземпляра подклассов; это не модификатор доступа, в рантайме член остаётся публичным
@visibleForTesting: можно использовать только в своей библиотеке и в тестах; из релизной сборки ничего не вырезается
@mustCallSuper**: переопределение обязано вызвать super; проверяется только наличие вызова, поэтому if (kDebugMode) super.dispose(); проходит
Чтобы это работало всерьёз: запускайте dart analyze в CI и оставьте предупреждения фатальными (так по умолчанию); пропустить их может только запуск с --no-fatal-warnings
ChatViewModel.dispose переопределяет базовый метод, не вызывая super.dispose(), поэтому подписки, созданные через watch, никогда не отменяются. Стрим репозитория держит в памяти view model каждого закрытого чата и продолжает добавлять в неё сообщения.
Почему утечка: переопределение заменяет базовый метод, и Dart не вызывает super за вас. Стрим держит подписку, подписка держит tear-off messages.add, а он держит саму view model, поэтому GC не может её собрать
Исправление: сначала освободите ресурсы самого подкласса, затем await super.dispose(), в том же порядке, что и во Flutter-овском State.dispose
Как поймать в следующий раз: пометьте базовый dispose аннотацией @mustCallSuper из package:meta (её реэкспортирует flutter/foundation.dart). Тогда переопределение без вызова super получит предупреждение must_call_super, а dart analyze роняет CI на предупреждениях. У State.dispose и ChangeNotifier.dispose эта аннотация уже есть, поэтому такую ошибку в виджетах ловят, а в собственных базовых классах нет
Тест: постройте фейковый репозиторий на StreamController, вызовите dispose у view model и проверьте expect(controller.hasListener, isFalse)
Добавьте новое имя, оставьте старое как метод-прокладку с @Deprecated('Use X instead') и удалите его только в следующей мажорной версии. Положите в пакет fix_data.yaml, чтобы dart fix --apply переписал вызовы за пользователей пакета.
Всегда пишите сообщение: @Deprecated('Use parseUser instead. Removed in 3.0.0.'); голый @deprecated не называет замену, и provide_deprecation_message на это укажет
Что видят пользователи пакета: deprecated_member_use, уровень info: роняет flutter analyze и dart analyze --fatal-infos, но никогда не компиляцию
Внутри вашего пакета: код другой, deprecated_member_use_from_same_package, поэтому можно приглушить собственную миграцию, ничего не скрыв от потребителей
Одна реализация: старый метод вызывает новый; две копии расходятся, и через устаревший путь уезжает баг
Semver: добавление нового имени означает минорный релиз, удаление старого означает мажорный
Пусть решает инструмент: все запускают dart format, CI падает на любом диффе, и никто не спорит о вёрстке кода на ревью. В Dart 3.7 появился новый «высокий» (tall) стиль, который выбирается по языковой версии пакета, поэтому поднятие ограничения SDK переформатирует весь репозиторий.
Одно место для настроек: formatter: page_width: в analysis_options.yaml, чтобы IDE, CLI и CI совпадали без всяких --line-length
Проверка в CI: dart format --output=none --set-exit-if-changed .; форматирование при сохранении в редакторе ловит остальное раньше
Что изменил новый стиль: форматтер теперь сам добавляет и удаляет завершающие запятые, поэтому запятая в конце больше не заставляет ставить по аргументу на строку; в Dart 3.8 появился trailing_commas: preserve для команд, которым нужно старое поведение
Как влить переформатирование: один коммит без логики, его хеш в .git-blame-ignore-revs (GitHub его читает; локально задайте blame.ignoreRevsFile), и вливать, когда открытых веток мало
Помимо вёрстки: имена и стиль API задаёт Effective Dart; линты кодируют большую его часть, так что комментарий на ревью может ссылаться на правило, а не на вкус
Проверкой служит CI, а хук остаётся удобством: хуки лежат в .git/hooks, который не клонируется, а git commit --no-verify пропускает их намеренно. В хук идут быстрые самоисправляющие проверки по застейдженным файлам, в CI идёт полный набор.
В хуке: dart format по застейдженным файлам, возможно dart analyze по ним же; укладывайтесь в пару секунд, иначе люди начнут хук пропускать
Добавляйте обратно то, что хук переписал: иначе в коммит уйдёт неотформатированная версия; в lefthook это делает stage_fixed: true, и его отсутствие оказывается одной из частых причин, почему неотформатированный код всё равно попадает в репозиторий
В CI: весь репозиторий, без автоисправлений, ненулевой код выхода: проверка форматирования, flutter analyze, шаг кастомных линтов, если он есть, затем тесты
Сделайте хук устанавливаемым: закоммитьте конфиг lefthook или husky либо папку .githooks/ вместе с git config core.hooksPath .githooks и запускайте установку из скрипта настройки проекта
Правило: проверка, которая может пройти только на чьём-то ноутбуке, проверкой не является
Инструмент тот же, отличаются входные данные. Смотрите на версию SDK, разрешённые версии пакетов, файлы на диске и на то, какие директории анализируются и с какими флагами.
Версия SDK: IDE использует тот SDK, на который указывает, а CI использует тот, что ставит его образ; новый SDK добавляет диагностики без единой правки в коде. Закрепите версию через FVM или flutter-version-file в subosito/flutter-action
Разрешённые зависимости: без закоммиченного pubspec.lock (обычное дело для пакетов) раннер подтягивает самые новые допустимые версии, и какой-то метод оказывается deprecated только там; flutter pub get --enforce-lockfile падает вместо тихого разрешения
Файлы на диске: .g.dart есть на вашей машине и отсутствуют в свежем клоне, а файл, который вы забыли git add, анализируется без проблем только у вас
Охват и флаги: IDE показывает info, но никогда не падает, dart analyze падает на предупреждениях, а flutter analyze падает ещё и на info; в монорепозитории каждый из них может охватывать разный набор пакетов
Как воспроизвести: склонируйте свою ветку во временную директорию и выполните там ровно те же команды, что и CI
Анализатор доказывает что-то только в пределах одной библиотеки: неиспользуемые импорты, приватные члены и мёртвые ветки. Для публичных классов, пакетов и ассетов нужны инструменты на весь проект, а всё, до чего добираются через строку, невидимо для всех них.
Уже бесплатно: unused_import, unused_local_variable, unused_field, unused_element, dead_code
Только приватные имена: о публичном классе, который никто не импортирует, анализатор не сообщит: его может импортировать другой пакет
Нужен инструмент: check-unused-code и check-unused-files из DCM для публичного кода; dependency_validator для пакетов, которые никто не импортирует
Что реально попало в сборку: flutter build apk --analyze-size выводит все пакеты и ассеты в артефакте, а DevTools открывает записанный им JSON
Чего не узнать никому: Image.asset('icons/$name.png'), таблица маршрутов, собранная из map, метод, который нативный код вызывает по имени через MethodChannel. Считайте каждый отчёт «не используется» вопросом и удаляйте отдельным коммитом, чтобы откат был в один клик
Пять проблем, которые не ловит ни один линт: контроллер не освобождается, ошибка загрузки не обрабатывается, setState может выполниться после того, как виджет исчез, репозиторий создаётся внутри виджета, а новый orderId игнорируется.
**_note не освобождается**: dispose() нет; добавьте его с вызовом _note.dispose() перед super.dispose()
Нет пути ошибки: если fetch бросит исключение, оно останется необработанным, а пользователь навсегда увидит «Loading»
**setState после await**: если пользователь уйдёт раньше, Flutter бросит «setState() called after dispose()»; проверьте if (!mounted) return; или отдайте это FutureBuilder
**OrderRepository() внутри виджета: тест не сможет его подменить; передавайте его через конструктор или ваш DI
Нет didUpdateWidget**: когда родитель перестроится с другим orderId, страница продолжит показывать старый заказ
Мелочь: 'Loading' оказывается видимым пользователю текстом вне файлов локализации
Планка та же, что для любого PR, но с прицелом на слабые места сгенерированного кода: непроверенные зависимости, код вне соглашений проекта, заглушённые ошибки и проверки безопасности, а также тесты, которые проходят при любом коде. Автор должен уметь объяснить каждую строку.
Новые пакеты: откройте на pub.dev каждый пакет, добавленный в pubspec.yaml, и проверьте издателя, дату последнего релиза и лицензию; ассистенты выдумывают правдоподобные имена, а злоумышленники их регистрируют
Актуально, но не по-нашему: устаревшие API вроде WillPopScope хотя бы дают deprecated_member_use; а Navigator.push в приложении на go_router, setState в экране на Bloc или сырые цвета вместо токенов темы не ловит ничто
Проглоченные ошибки: голый catch, который только логирует, выдаёт упавшее сохранение за успех, а ! или as, добавленные, чтобы успокоить анализатор, переносят краш в продакшен
Ослабленная безопасность: badCertificateCallback, возвращающий true, NSAllowsArbitraryLoads или usesCleartextTraffic, добавленные, чтобы «починить сеть», ключ, вставленный прямо в Dart; diff в ios/ и android/ читайте так же внимательно, как в lib/
Жизненный цикл: контроллеры без dispose, setState после await без проверки mounted; ни одно правило flutter_lints не сообщает ни о том, ни о другом
Тесты, которые ничего не доказывают: findsOneWidget на саму страницу, моки, которые возвращают ровно то, что ждёт код, голдены, перегенерированные в том же PR; откатите изменение локально и посмотрите, упадёт ли хоть один тест
Форматирование и линты отдайте инструментам, а человеческое внимание тратьте на то, чего они не видят: стейт и жизненный цикл, пути ошибок, границы слоёв и тесты. У каждого комментария должна быть строгость и причина или вопрос, а не просто вердикт.
Что искать
Владение ресурсами: каждый контроллер, подписка и notifier, созданные здесь, здесь же и освобождаются, а виджет обрабатывает новые аргументы в didUpdateWidget
Форма состояния: может ли класс быть одновременно «загрузка» и «ошибка»? Одно sealed-состояние делает невозможное состояние непредставимым
Негативные сценарии: нет сети, отказ в разрешении, пустой список, крупный масштаб текста, поворот во время запроса; запустите ветку, а не только прочитайте её
Границы: DTO или Dio, просочившиеся в виджеты, бизнес-логика в build, видимые пользователю строки вне l10n
Тесты: упадёт ли какой-нибудь тест, если откатить это изменение?
Как формулировать
Помечайте строгость: blocking:, suggestion:, nit:, и одобряйте с комментариями, когда остались только мелочи
Ссылайтесь на стандарт, а не на вкус: Effective Dart или гайд команды; комментарий, который вы пишете снова и снова, стоит превратить в линт
Спрашивайте, прежде чем предписывать: «Что увидит пользователь, если запрос упадёт?» учит больше, чем переписанный кусок кода
Ревьюйте код, а не человека: «этот метод», а не «ты»
Что автор должен ревьюеру
Маленькие PR: примерно до 400 изменённых строк, рефакторинг отдельно от изменения поведения
Контекст: что и зачем в описании, скриншоты или видео для изменений UI и собственное ревью до запроса
Напишите плагин анализатора, который обходит разрешённое AST и сообщает диагностику. С Dart 3.10 есть официальная система плагинов, которую запускает сам dart analyze; многие команды всё ещё используют более старый custom_lint, которому нужен отдельный шаг в CI.
Два способа поставки
**custom_lint**: правила в пакете на custom_lint_builder, включённые через analyzer: plugins: - custom_lint. dart analyze его не запускает, поэтому CI нужен dart run custom_lint, иначе правило существует только в чьей-то IDE
Официальные плагины (Dart 3.10+): пакет на analysis_server_plugin, указанный в секции plugins: верхнего уровня в analysis_options.yaml; его запускают IDE и dart analyze
Как писать правило
Спрашивайте про типы, а не про написание: AST разрешён, так что можно проверить, что это за тип и где он объявлен; именно этого не умеет grep
Выходите пораньше: в IDE правила работают на каждое нажатие клавиши, поэтому фильтруйте по пути до обхода узлов
Добавьте quick fix, если исправление механическое, и начинайте правило с info, повышая его в analyzer: errors:, когда кодовая база станет чистой
Ограничения
Одна библиотека за раз: правило не ответит на вопрос «используется ли это где-нибудь»
Сгенерированный код: правило не видит то, что build_runner ещё не сгенерировал
Цена: каждое правило становится кодом, который нужно поддерживать; пишите его только для ошибки, которая возвращается снова и снова и приводит к настоящим багам
Используйте метрики как сигнал в pull request, а не как жёсткий барьер: число говорит, какой код стоит прочитать, но не говорит, хорош ли он, а жёсткий порог начинают обходить.
Цикломатическая сложность плохо подходит Flutter: build на 400 строк с одним if получает 2, а switch по десяти sealed-состояниям получает 11, хотя это самый понятный код в файле
Легко обмануть: разбиение build на методы _buildHeader и _buildBody проходит любой порог и делает только хуже: такие хелперы не могут быть const и перестраиваются вместе с родителем; настоящее решение состоит в том, чтобы выносить классы виджетов
Что лучше отражает проблемы: глубина вложенности и число параметров; их трудно превысить случайно и трудно обойти
Настройте так, чтобы это выжило: исключите сгенерированные файлы и тесты, поставьте каждый порог так, чтобы он отмечал примерно худшие несколько процентов текущего кода, и выводите результат комментарием в PR (DCM это умеет), а не через код выхода
Правило: метрики выбирают, что читать; что менять, решают люди
Включайте поэтапно: сначала dart fix, затем оставьте оставшиеся правила видимыми, но неблокирующими для старого кода и блокирующими для кода, который трогает PR, и разбирайте долг по одному правилу небольшими PR.
Первый проход
**dart fix --apply**: убирает механическую часть одним коммитом, который ревьюится как «сгенерировано машиной, ручных правок нет»; --code=<rule> дробит его, если он всё ещё слишком большой
Ранжируйте остаток: dart analyze --format=machine выводит поля через вертикальную черту, так что одна строка shell считает замечания по правилам; сначала берите правила, ловящие настоящие баги, сколько бы их ни было
Остановить рост
Новый код проверяйте по полной планке: CI запускает dart analyze по всему репозиторию с нефатальными info и dart analyze --fatal-infos по файлам, изменённым в PR; info при этом все по-прежнему видят в IDE
Видимый baseline для самых шумных правил: у Dart нет baseline-файла, поэтому правила с тысячами срабатываний уходят в ignore в analyzer: errors: с числом и тикетом рядом с каждой строкой и возвращаются по одному PR за раз. Правило в ignore выключено и для нового кода, поэтому список должен быть коротким и сокращаться
Легаси-папки: вложенный analysis_options.yaml в них (действует ближайший файл) вместо exclude:, который скрыл бы и настоящие ошибки
Сохранить читаемую историю
Blame: коммиты массовых исправлений добавляются в .git-blame-ignore-revs
Время: вливайте их, когда открытых веток мало, чтобы никому не пришлось разрешать конфликты вручную