Repository navigation
Description
Summary
In a Dart/Flutter pub workspace (Dart 3.11+ unified workspace, multiple packages), a build_runner process for one package (app) can fail to resolve a Type literal that references a class defined in a sibling dependency package that was already fully built by a separate, prior build_runner build process. The referenced class's file is never discovered by the analyzer's in-memory filesystem (AnalysisDriverFilesystem in build_runner/lib/src/build/resolver/), even though it exists on disk with valid content. The analyzer's constant evaluator falls back to an "unknown Type constant" (TypeState with a null inner type) rather than erroring, which downstream consumers (in this case package:injectable_generator, but the mechanism is generic to any source_gen-based builder reading a Type-typed const argument) surface as a confusing runtime crash instead of a clear diagnostic.
This reproduces deterministically (100% of runs, 10+ independent real builds) in a ~36-package real-world Flutter monorepo, but I was not able to reduce it to a fully isolated minimal repro despite substantial effort (see "What I tried" below) — filing with the full evidence chain instead, since I believe it's precise enough to be actionable.
Observed symptom
[minimal_ci]: E injectable_generator:injectable_config_builder on lib/di/injector.dart:
[minimal_ci]: FormatException: Not an instance of Type.
[minimal_ci]: Failed to build with build_runner/aot in Ns; wrote 0 outputs.
The offending source (apps/minimal_ci/lib/di/injector.dart):
import 'package:app_utilities/app_utilities.dart';
import 'package:dependencies/di.dart';
import 'package:router_registry/router_registry.dart';
import 'package:minimal_ci/di/injector.config.dart';
final getIt = GetIt.instance;
@InjectableInit(
initializerName: r'$initGetIt',
asExtension: false,
externalPackageModulesBefore: [
ExternalModule(AppUtilitiesPackageModule), // <-- fails
ExternalModule(RouterRegistryPackageModule), // <-- succeeds (never reached in the failing run, but succeeds in isolation)
],
)
Future<void> initializeInjector() => $initGetIt(getIt);AppUtilitiesPackageModule is a class generated by injectable_generator's own micro-package builder (@InjectableInit.microPackage()) into app_utilities/lib/src/di/injector.module.dart, as part of app_utilities's own, separate build_runner build invocation — which completes successfully and writes the file to disk before minimal_ci's own build_runner build process even starts (sequential melos build:select → build:app pipeline; confirmed via process-level tracing that app_utilities's build_runner process fully exits before minimal_ci's starts).
Root cause, traced precisely
1. Where the exception is actually thrown
package:source_gen's ConstantReader:
// source_gen/lib/src/constants/reader.dart
T _check<T>(T? value, String expected) {
if (value == null) {
throw FormatException('Not an instance of $expected.', objectValue);
}
return value;
}
@override
DartType get typeValue => _check(objectValue.toTypeValue(), 'Type');objectValue.toTypeValue() is analyzer's own DartObjectImpl.toTypeValue():
// analyzer/lib/src/dart/constant/value.dart
TypeImpl? toTypeValue() {
var state = this.state;
if (state is TypeState) {
return state._type;
}
return null;
}I instrumented injectable_generator (the caller, _getMicroPackageModules in injectable_config_generator.dart) to print the raw DartObject before it's read:
NGMX_DEBUG: e = ExternalModule (module = Type (null); scope = Null (null))
NGMX_DEBUG: e.getField(module) = Type (null)
NGMX_DEBUG: e.getField(module)?.isNull = true
NGMX_DEBUG: e.getField(module)?.toTypeValue() = null
So the constant is recognized as "a Type value" (state is TypeState — true), but the TypeState's own inner _type is null — analyzer's own documented "unknown value" representation (see TypeState.toString() => _type?.getDisplayString() ?? '-unknown-', StringState.UNKNOWN_VALUE, etc.). This is not an injectable_generator bug — the constant genuinely evaluated to "unknown."
2. Confirming it's a real constant-evaluation failure, not just a null field
I added a print of ElementAnnotation.constantEvaluationErrors for the @InjectableInit(...) annotation:
NGMX_DEBUG: constantEvaluationErrors = [/minimal_ci/lib/di/injector.dart(356..380): Arguments of a constant creation must be constant expressions.]
Offset 356..380 in the real file is exactly the AppUtilitiesPackageModule identifier inside ExternalModule(AppUtilitiesPackageModule). So the analyzer's own constant evaluator is reporting (internally — this is not surfaced as a normal compile error / not shown by dart analyze, and does not stop the build) that this specific Type literal could not be evaluated as a constant.
3. Why: the file is never discovered by AnalysisDriverFilesystem
I instrumented build_runner's AnalysisDriverFilesystem (build_runner/lib/src/build/resolver/analysis_driver_filesystem.dart) — the class that backs ResourceProvider, FileContentCache, and UriResolver for the entire analysis session — tracing _updateContent (writes) and exists (reads) for any path containing app_utilities or injector:
NGMX_FS_DEBUG: exists(/app_utilities/lib/app_utilities.dart) -> false (never in _data)
That's the only trace line for that path in the whole build — there is no preceding _updateContent(app_utilities|lib/app_utilities.dart) call at all. The file was never read, so it was never registered in _data, so exists() reports it as absent — even though it is a completely ordinary, hand-written barrel file (export 'src/di/injector.module.dart';) sitting on disk with valid content the whole time.
For comparison, the structurally identical case for the second ExternalModule(...) entry in the same annotation, router_registry/lib/src/di/injector.module.dart, is discovered and read correctly in the same build_runner process:
NGMX_FS_DEBUG: _updateContent(router_registry|lib/src/di/injector.module.dart) called, hasContent=true
NGMX_FS_DEBUG: _updateContent(router_registry|lib/src/di/injector.module.dart) -> phase=-1
NGMX_FS_DEBUG: exists(/router_registry/lib/src/di/injector.module.dart) -> true (_phase=0, content.phase=-1)
So within the very same build, the same discovery mechanism succeeds for one cross-package dependency file and silently fails for another — no exception, no diagnostic surfaced to the user, just a TypeState(null) two layers removed from the actual failure, and a generator-specific crash for whichever generator happens to be the one that needs .toTypeValue() on that particular constant.
What I tried (all real end-to-end tests against the 36-package project, not simulated)
None of these change the outcome:
melos exec --concurrency=1(fully serialize sibling package builds before the app build) — still fails.build_runner build --force-jit— still fails.- Exact
--scopetargeting instead of a wildcard package selector for the app build — still fails. - A 5-second
sleepinserted between the dependency package's build finishing and the app's build starting — still fails (rules out a plain elapsed-time race). - Splitting the two build phases into fully independent top-level process invocations instead of one process running both sequentially — still fails.
- Patching the calling generator (
injectable_generator'sInjectableConfigGenerator) toawait buildStep.resolver.libraries.toList()beforesuper.generate()runs (i.e., beforeGeneratorForAnnotationcomputes the annotation's constant value) — still fails identically. This was the most targeted attempt: it directly tests whether forcing full library resolution earlier, from generator code, can compensate — it can't, which suggests whatever decidesapp_utilities.dart's discoverability is fixed before any generator code runs at all.
I was not able to build a minimal, from-scratch repro (tried a 2-package and later a 9-package synthetic pub workspace with the same shape: sibling packages built via separate/concurrent build_runner build processes, then an app package aggregating them via ExternalModule(...)) — those always succeeded. Whatever conditions trigger the file-discovery gap seem to need either much larger package/dependency-graph scale, Flutter (vs. pure Dart), or some other factor I didn't isolate. I'm filing with the precise evidence chain above rather than continuing to guess at a minimal repro, since I believe the trace already pinpoints the actual defect.
Versions
build_runner: 2.15.1analyzer: 10.2.0source_gen: 4.2.4 (only relevant as the immediate caller that surfaces the symptom; not itself at fault)injectable_generator: 3.1.2 (ditto)- Dart SDK: 3.12.1, real
workspace:/resolution: workspacepub workspace (Dart 3.11+ unified workspace resolution)
Related report
Milad-Akarie/injectable#483 shows the exact same FormatException: Not an instance of Type. from the exact same code path (ExternalModule → MicroPackageModule resolution), but for a same-package/same-build-cycle scenario (a micro-package module referenced from a file built in the same builder invocation that also produces it). A commenter's theory there is specific to output ordering within one build cycle, which doesn't fully explain the cross-package, already-completed-dependency case documented here — but both point at the same underlying symptom class: something that resolves a Type constant pointing at a file AnalysisDriverFilesystem hasn't (yet, or ever) registered.
Why this matters
Since AnalysisDriverFilesystem is the sole backing for the analyzer's ResourceProvider/FileContentCache/UriResolver in build_runner, this isn't specific to injectable/injectable_generator — any source_gen-based builder that reads a Type-valued constant referencing a class from an already-built sibling package's generated file is exposed to the same silent failure mode, surfacing as whatever obscure exception that particular library's null-handling produces (FormatException, an ArgumentError, etc.) rather than a clear "could not resolve X" diagnostic.
Happy to share the instrumented diffs (both injectable_generator and build_runner) if useful for reproducing on your end, and to help test a fix.
Activity
davidmorgan commentedon Aug 17, 2026
Thanks for the report--looking into it.
davidmorgan commentedon Aug 18, 2026
@ryanaidilp This might be caused by a
build.yamlfile inapp_utilitiesthat filters on sources.If so you would see a warning in the build like:
The problem
build.yamlwould be something likeSpecifying a
sourcesfilter totally hides files frombuild_runner.What was intended is probably
generate_for, which chooses what to generate files for but does not hide the rest frombuild_runner:davidmorgan commentedon Nov 2, 2026
Closing as stale; please re-open if you have more information.