Skip to main content

Why a checker when you already have the analyzer?

dartway check looks at the things the analyzer and the lints cannot see. The analyzer reads a file; the checker reads the project: which folder is a feature, who imports whose internals, whether a screen styles itself, whether an asset path leads anywhere. All of that compiles. Some of it fails at runtime, some of it never fails and just rots.

Three examples of what nothing else catches:

  • a file spelling out assets/icons/lock.png when no such file exists โ€” the code compiles and the screen renders a blank where the image was;
  • one feature importing another's widgets/ โ€” perfectly valid Dart, and the boundary is gone;
  • Theme.of(context).textTheme.bodySmall in a screen โ€” ordinary Flutter, and the reason your app has two greys.

The checker is not a second analyzer. It exists because DartWay's structural conventions are the part of the framework that a compiler has no opinion about.

What the report looks likeโ€‹

The report is organised per feature, not as a flat list of lines. A large project should read as a list of features to fix, not as a wall to scroll past.

๐Ÿ“ Features (grade ยท files ยท findings)

app/
โœ… profile A 4 files
๐ŸŸ  booking C 9 files ยท 2 errors, 1 warning
โœ… slot_card A 2 files
learning/ โ€” group
๐ŸŸก lesson B 6 files ยท 1 warning

The tree is built from the folders themselves โ€” nothing is declared. A folder with exactly one root .dart file is a feature, and that file is its whole public surface; a folder with no root .dart file is a group that only groups features and encapsulates nothing. widgets/ and logic/ are a feature's internals, not children.

The grade of a feature comes only from what belongs to it:

GradeMeaning
โœ… ANo errors, no warnings (infos are allowed)
๐ŸŸก BNo errors, some warnings
๐ŸŸ  COne or two errors
๐Ÿ”ด DThree or more errors

Then findings by feature (the first six of each), then a count per check, then the total number of errors. Only errors fail the run. Warnings and infos print and pass โ€” a check that blocks a commit over a 210-line file is a check people disable.

The checksโ€‹

CheckLevelWhat it means
forbiddenUiUsageerrorRaw styles outside ui_kit/ โ€” the screen is styling itself
forbiddenUiKitImporterrorImporting inside ui_kit/ instead of the ui_kit.dart barrel
uiKitPartMissingerrorA kit file without part of '../ui_kit.dart'
uiKitContainsTextwarningA text constant in the kit; texts belong to features and l10n
invalidFeatureStructureerrorA feature folder with more than one root file
forbiddenFeatureImporterrorReaching into another feature's widgets/ or logic/
featureSpecMissingwarningA feature widget that declares no DwFeatureSpec
barrelFileerrorA file that only re-exports
widgetSizesItselferrorExpanded or SizedBox.expand returned straight from build
assetPathMissingerrorAn assets/... string that names no file
forbiddenAssetPathwarningA raw assets/... path outside ui_kit/
fileLonginfoOver 200 lines
fileTooLongwarningOver 350 lines

"Raw styles" means Color(, TextStyle(, BorderRadius, Theme.of(context), context.theme, context.textTheme, context.colorScheme. The long spelling is in the list on purpose: the rule used to catch context.textTheme but not Theme.of(context).textTheme.bodySmall, which reads as ordinary Flutter and means exactly the same thing โ€” a screen deciding how it looks.

"More than one root file" is a structural claim, not a style one. A feature has exactly one public file; anything a sibling also needs belongs in a feature of its own, not in a shared/ folder. featureSpecMissing is checked only when the entry point is a widget โ€” a feature whose entry point is an extension or a plain function has nothing to hang a spec on. The spec matters because error reports, Studio and the agent all read it: without one the feature exists in the code and says nothing about itself.

Filter with --type <name> or --level error|warning|info, or narrow the run to a single folder with --dir lib/app/booking. Note that --dir skips the ui_kit/ pass โ€” the kit is checked as a whole or not at all.

Scope: the feature-shaped areas app*, auth*, common*, admin*, plus ui_kit/. core/, data/ and domain/ hold infrastructure with a shape of its own. Generated files (.g.dart, .gen.dart, .freezed.dart) and the folders generated/, gen/, l10n/, zarchive/ are nobody's code and are skipped everywhere.

Why 200 and 350โ€‹

Length is the weakest signal the checker has, and a tight limit makes it lie. The thresholds started at 120 (info) and 200 (warning) and were deliberately relaxed, because the checker began flagging files that were long for a good reason: a feature's DwFeatureSpec now lives in the file of the feature it describes, and a good description costs twenty lines.

A rule that goes off when someone documents their feature properly teaches them to document less. That is the whole argument. So nothing is said below 200 lines, above it is a nudge that never fails anything, and above 350 a warning that says "worth restructuring" โ€” because at that size a file has usually collected more than one responsibility. Split by responsibility, not by line count: a meaningful 300-line file beats a pointless chop into three.

Why SizedBox(width: double.infinity) is not in widgetSizesItselfโ€‹

widgetSizesItself fires on exactly two things, and only when they are what build returns: Expanded and SizedBox.expand. Both mean the widget has decided how much room it gets. It works until someone drops it into a bottom sheet or a scroll view, and then it throws at runtime while the analyzer stays silent. Space is the parent's call โ€” let the caller wrap it.

SizedBox(width: double.infinity) was tried in this check and taken back out. Inside a bounded parent it only means "as wide as allowed" โ€” legitimate, common, and harmless. Every hit was arguable.

A check whose findings are arguable teaches people to skip the checker. Once a rule has cried wolf twice, its next finding โ€” a real one โ€” gets the same shrug, and so does every other rule in the tool. Precision is not a nicety here; it is the only thing keeping the checker worth running. The two remaining patterns are not arguable: both throw in the first parent that does not offer unbounded space.

The same principle shows up elsewhere in the checker. A string inside a doc comment in the kit is not flagged as a text constant โ€” usage examples are the most useful thing a kit widget can carry, and flagging them taught authors to write worse documentation. A part of directive is not demanded from generated files โ€” it survives exactly until the next generator run.

Why barrelFile is an errorโ€‹

A file whose whole body is export directives reads as convenience and acts as a hole in the feature boundary. Importers name the barrel, so reaching into another feature's guts through it looks legitimate โ€” and the import checks see a barrel, not the internals behind it. One such file laundered three features' internals until it was deleted. A single-line re-export counts too: same hole, smaller.

Why asset paths are checked against the file systemโ€‹

DartWay projects do not run build_runner in the edit loop โ€” it costs minutes per change and punishes whoever forgets to run it with errors about code that is perfectly fine. So asset constants are written by hand. A generated constant could not name a missing file; a hand-written one can.

assetPathMissing restores that guarantee, and forbiddenAssetPath keeps the paths in one place: a path spelled out in a screen survives a renamed file only by accident, and cannot be found by search. The screen should receive a widget, not a file name.