Skip to content

build_runner: analyzer resolver silently fails to discover a sibling package's file, causing Type-literal constants to resolve to null (surfaces as source_gen FormatException, injectable#483-adjacent) #5075

Closed

Description

@ryanaidilp

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 --scope targeting instead of a wildcard package selector for the app build — still fails.
  • A 5-second sleep inserted 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's InjectableConfigGenerator) to await buildStep.resolver.libraries.toList() before super.generate() runs (i.e., before GeneratorForAnnotation computes 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 decides app_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.1
  • analyzer: 10.2.0
  • source_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: workspace pub 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

  1. self-assigned this
    on Aug 17, 2026
  2. davidmorgan commented on Aug 17, 2026

    @davidmorgan
    Contributor

    Thanks for the report--looking into it.

  3. davidmorgan commented on Aug 18, 2026

    @davidmorgan
    Contributor

    @ryanaidilp This might be caused by a build.yaml file in app_utilities that filters on sources.

    If so you would see a warning in the build like:

        The package `app_utilities` does not include some required sources in any of its targets (see their build.yaml file).
          The missing sources are:
            - lib/$lib$
    

    The problem build.yaml would be something like

        targets:
          $default:
            sources:
              - "lib/src/**"
    

    Specifying a sources filter totally hides files from build_runner.

    What was intended is probably generate_for, which chooses what to generate files for but does not hide the rest from build_runner:

        targets:
          $default:
            builders:
              injectable_generator:injectable_builder:
                generate_for:
                  - "lib/src/**"
    
  4. davidmorgan commented on Nov 2, 2026

    @davidmorgan
    Contributor

    Closing as stale; please re-open if you have more information.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions