Repository navigation
Prefer late over null-initialized debug fields and enable the lint rule - #192438
Conversation
no_runtimetype_in_tostring, prefer late over null-initialized debug fields
no_runtimetype_in_tostring, prefer late over null-initialized debug fieldslate over null-initialized debug fields and enable the lint rule
There was a problem hiding this comment.
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.
| // A field is lazy when it's static or late. | ||
| final bool eagerlyInitialized = hasDebugOnly && !(node.isStatic || node.fields.isLate); | ||
| if (eagerlyInitialized) { | ||
| rule.reportAtNode(node); | ||
| } |
There was a problem hiding this comment.
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);
}There was a problem hiding this comment.
The goal of this rule is just to make sure nothing is eagerly initialized.
| 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)]); | ||
| } |
There was a problem hiding this comment.
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),
]);
}| 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>>{}; |
There was a problem hiding this comment.
Ubernit: is the LHS type necessary? I would assume this is considered to have an obvious type given the collection initialization on the RHS.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
|
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. |
|
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. |
d4663ff
|
Sorry had to reformat. |
enablesno_runtimetype_in_tostringandnull_initialized_debug_expensive_fieldsinpackages/flutter/libnull_initialized_debug_expensive_fieldsand the framework code to uselate finalinstead of conditionally (based onkDebugMode) initializing debug only fields.late finalshould 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:Because the minimal test program is only referencing the
HashSetconstructor in a late final initializer. But that shouldn't be a problem for the framework since we useHashSetin production code too (and other expensive-to-construct types). Updating the toy program to introduce a non-debug field eliminated the size difference.TODO
debug(or_debug) prefix can't be accessed outside of asserts,debugor_debug)