Skip to content

Prefer late over null-initialized debug fields and enable the lint rule - #192438

Merged
auto-submit[bot] merged 16 commits into
flutter:masterfrom
LongCatIsLooong:_debugOnly
Sep 19, 2026
Merged

auto-submit[bot] merged 16 commits into
flutter:masterfrom
LongCatIsLooong:_debugOnly

Conversation

@LongCatIsLooong

@LongCatIsLooong LongCatIsLooong commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor
  • enables no_runtimetype_in_tostring and null_initialized_debug_expensive_fields in packages/flutter/lib
  • updates null_initialized_debug_expensive_fields and the framework code to use late final instead of conditionally (based on kDebugMode) initializing debug only fields.

late final should achieve the same thing as null initializing (eliminating the initialization code). I used gemini-created toy dart programs to verify the initialization code is stripped with late final. There are some redundant fields left in the binary, according to gemini:

When HashSet is not used anywhere else in the program, the +22/+23 byte DispatchTable diff (and +12 byte Instructions diff) is not caused by the late getter selector—it is caused by 7 empty HashSet class hierarchy shells from dart:collection remaining in the VM class table, which shifts the Object selector rows (==, hashCode, toString) in the dispatch table by +7 CID slots.

Because the minimal test program is only referencing the HashSet constructor in a late final initializer. But that shouldn't be a problem for the framework since we use HashSet in production code too (and other expensive-to-construct types). Updating the toy program to introduce a non-debug field eliminated the size difference.

TODO

  • This rule no longer guarantees that the initialization only happens in debug mode. I'll add another rule to make sure members with a debug (or _debug) prefix can't be accessed outside of asserts,
  • Maybe remove the annotation and rely on the name of the field (whether it starts with debug or _debug)

@github-actions github-actions Bot added the framework flutter/packages/flutter repository. See also f: labels. label Sep 8, 2026
@LongCatIsLooong LongCatIsLooong added the CICD Run CI/CD label Sep 8, 2026
@github-actions github-actions Bot added the a: tests "flutter test", flutter_test, or one of our tests label Sep 8, 2026
@github-actions github-actions Bot removed the a: tests "flutter test", flutter_test, or one of our tests label Sep 8, 2026
@LongCatIsLooong LongCatIsLooong changed the title Test Enable no_runtimetype_in_tostring, prefer late over null-initialized debug fields Sep 8, 2026
@LongCatIsLooong LongCatIsLooong changed the title Enable no_runtimetype_in_tostring, prefer late over null-initialized debug fields Prefer late over null-initialized debug fields and enable the lint rule Sep 10, 2026
@LongCatIsLooong
LongCatIsLooong marked this pull request as ready for review September 11, 2026 17:03

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request replaces the null_initialized_debug_expensive_fields analyzer rule with lazy_initialized_debug_expensive_fields, updating the plugin implementation, tests, documentation, and framework code to require non-static @_debugOnly fields to be declared as late with an initializer. The review feedback suggests improving the analyzer rule to ensure that late fields are declared with an initializer, and adding a corresponding test case to verify this behavior.

Comment on lines +52 to 56
// A field is lazy when it's static or late.
final bool eagerlyInitialized = hasDebugOnly && !(node.isStatic || node.fields.isLate);
if (eagerlyInitialized) {
rule.reportAtNode(node);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation of lazy_initialized_debug_expensive_fields only checks if the field is static or late. However, if a late field is declared without an initializer, it is not truly lazy-initialized on first read (as there is no initializer to evaluate). Instead, it must be explicitly assigned elsewhere. If this assignment occurs in a non-assert block, it will execute in release/profile builds, defeating the purpose of the @_debugOnly annotation. To prevent this, we should also require that any @_debugOnly field has an initializer.

    // A field is lazy when it's static or late, and has an initializer.
    final bool eagerlyInitialized = hasDebugOnly &&
        (!(node.isStatic || node.fields.isLate) ||
            node.fields.variables.any((VariableDeclaration v) => v.initializer == null));
    if (eagerlyInitialized) {
      rule.reportAtNode(node);
    }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal of this rule is just to make sure nothing is eagerly initialized.

Comment on lines +22 to +44
static const String source = '''
class GoodClass {
@_debugOnly
late final int _foo = 1;
}

class GoodClassStatic {
@_debugOnly
static int _foo = 1;
}

class BadClass {
@_debugOnly
final int _foo = 1;
}

const _debugOnly = Object();
''';

// ignore: non_constant_identifier_names
Future<void> test_lazy_initialized() async {
await assertDiagnostics(source, <ExpectedDiagnostic>[lint(145, 33)]);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To ensure that late fields without an initializer are correctly flagged by the analyzer rule, we should add a test case to lazy_initialized_debug_expensive_fields_test.dart and assert that it produces the expected diagnostic.

  static const String source = '''
class GoodClass {
  @_debugOnly
  late final int _foo = 1;
}

class GoodClassStatic {
  @_debugOnly
  static int _foo = 1;
}

class BadClass {
  @_debugOnly
  final int _foo = 1;
}

class BadClass2 {
  @_debugOnly
  late final int _foo;
}

const _debugOnly = Object();
''';

  // ignore: non_constant_identifier_names
  Future<void> test_lazy_initialized() async {
    await assertDiagnostics(source, <ExpectedDiagnostic>[
      lint(145, 33),
      lint(202, 34),
    ]);
  }

bkonyi
bkonyi previously approved these changes Sep 17, 2026

@bkonyi bkonyi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

final Map<Element, Map<Element, GlobalKey>>? _debugGlobalKeyReservations = kDebugMode
? <Element, Map<Element, GlobalKey>>{}
: null;
late final Map<Element, Map<Element, GlobalKey>> _debugGlobalKeyReservations = <Element, Map<Element, GlobalKey>>{};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ubernit: is the LHS type necessary? I would assume this is considered to have an obvious type given the collection initialization on the RHS.

@LongCatIsLooong LongCatIsLooong Sep 18, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah interesting, the analyzer is ok with that change but the field above final Map<GlobalKey, Element> _globalKeyRegistry = <GlobalKey, Element>{}; is not omitting obvious types. I looked at the blame and that part was update in April, but it only changed the wording for local types:
0201778, @loic-sharma do you know what the current recommendation is for instance variables?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I think I'll merge this as-is (since existing fields have the full type specified on both lhs and rhs, and the style guide seems to indicate that's the convention). If that recommendation turns out to be outdated we would have to update all existing fields anyway.

srawlins
srawlins previously approved these changes Sep 17, 2026
@LongCatIsLooong LongCatIsLooong added the autosubmit Merge PR when tree becomes green via auto submit App label Sep 18, 2026
@auto-submit auto-submit Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Sep 18, 2026
@auto-submit

auto-submit Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

autosubmit label was removed for flutter/flutter/192438, because The base commit of the PR is older than 7 days and can not be merged. Please merge the latest changes from the main into this branch and resubmit the PR.

@LongCatIsLooong LongCatIsLooong added the autosubmit Merge PR when tree becomes green via auto submit App label Sep 18, 2026
@auto-submit auto-submit Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Sep 18, 2026
@auto-submit

auto-submit Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

autosubmit label was removed for flutter/flutter/192438, because - The status or check suite Tree_analyze has failed. Please fix the issues identified (or deflake) before re-applying this label.

@LongCatIsLooong

Copy link
Copy Markdown
Contributor Author

Sorry had to reformat.

@LongCatIsLooong LongCatIsLooong added the autosubmit Merge PR when tree becomes green via auto submit App label Sep 18, 2026
@auto-submit
auto-submit Bot added this pull request to the merge queue Sep 19, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to no response for status checks Sep 19, 2026
@flutter-dashboard flutter-dashboard Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Sep 19, 2026
@LongCatIsLooong LongCatIsLooong added the autosubmit Merge PR when tree becomes green via auto submit App label Sep 19, 2026
@auto-submit
auto-submit Bot added this pull request to the merge queue Sep 19, 2026
Merged via the queue into flutter:master with commit 6294579 Sep 19, 2026
29 checks passed
@flutter-dashboard flutter-dashboard Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Sep 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants