Skip to content

Returning null from SpellCheckService.fetchSpellCheckSuggestions can break spellcheck and put EditableText into bad state #152272

@Adam-Langley

Description

@Adam-Langley

Steps to reproduce

  1. Write a custom SpellCheckService, which in some cases returns genuin TextRanges, and in other cases, returns null
  2. Apply this new SpellCheckService to a TextField
  3. Fill the TextField with words, such that some of them are highlighted as incorrectly spelled
  4. From the end of the TextField, hold down backspace to repeatedly remove characters until they are all gone

Expected results

All characters should be removed one by one, while spell check runs for each change, without error.

The SpellCheckService 'fetchSpellCheckSuggestions' method definition allows for the returning of null, and the documentation does not state when it can and cannot be null.
It appears that null is intended to represent the fact that spell checking could not run (i.e, language not supported) - let's call that State 1, and an empty result set ([]) is intended to represent the spellcheck successfully running, but no errors found in the text - State 2.

The subsystem does not like the SpellCheckService transitioning from State 2 to State 1 for a singular EditableText instance.
EditableText should be more resilient to this.

Actual results

At some point, an error is generated by the spellcheck infrastrcuture, putting it into a bad state, then errors are repeatedly generated while attempting to render the text caret (caret will no longer render).

Code sample

Sample SpellCheckService which will exhibits the issue.

import 'dart:async';
import 'dart:math';
import 'dart:ui';
import 'package:flutter/services.dart';

class TestCaseSpellCheckService implements SpellCheckService {

  @override
  Future<List<SuggestionSpan>?> fetchSpellCheckSuggestions(Locale locale, String text) async {
    // generate a boolean that is true 50% of the time
    final bool shouldFail = Random().nextDouble() < 0.50;
    if (shouldFail) {
      return null;
    }

    return _processSpellCheck(text);
  }

  Future<List<SuggestionSpan>?> _processSpellCheck(String text) async
  {
    // make every word an error.
    // 1. use regex to split the text into words
    // 2. for each word, create a SuggestionSpan with the word as the text and a list of suggestions
    // 3. return the list of SuggestionSpan, which is comprised of a TextRange and a list of suggestions
    final re = RegExp(r'(\w+)');
    final words = re.allMatches(text);
    final suggestions = words.map((word) => SuggestionSpan(
      TextRange(start: word.start, end: word.end),
      ['suggestion1', 'suggestion2']
    )).toList();
    return suggestions;    
  }
}

Screenshots or Video

Video of repeated backspace causing error in TextField (caret then disappears as bad state prevents it from rendering)

Screen.Recording.2024-07-25.at.10.18.39.AM.mov

Screenshot of moment error occurs
image

Logs

Stack trace

flutter: [E] TIME: 2024-07-25T10:19:48.809862 Unhandled Exception: RangeError (start): Invalid value: Not in inclusive range 0..176: 177  ERROR: RangeError (start): Invalid value: Not in inclusive range 0..176: 177  STACKTRACE: #0      RangeError.checkValidRange (dart:core/errors.dart:360:7)
#1      _StringBase.substring (dart:core-patch/string_patch.dart:418:27)
#2      _correctSpellCheckResults (package:flutter/src/widgets/spell_check.dart:136:36)
#3      buildTextSpanWithSpellCheckSuggestions (package:flutter/src/widgets/spell_check.dart:202:30)
#4      EditableTextState.buildTextSpan (package:flutter/src/widgets/editable_text.dart:5360:14)
#5      EditableTextState.build.<anonymous closure> (package:flutter/src/widgets/editable_text.dart:5255:45)
#6      ScrollableState.build (package:flutter/src/widgets/scrollable.dart:981:44)
#7      StatefulElement.build (package:flutter/src/widgets/framework.dart:5599:27)
#8      ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5487:15)
#9      StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5650:11)
#10     Element.rebuild (package:flutter/src/widgets/framework.dart:5203:7)
#11     StatefulElement.update (package:flutter/src/widgets/framework.dart:5673:5)
#12     Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#13     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5512:16)
#14     Element.rebuild (package:flutter/src/widgets/framework.dart:5203:7)
#15     ProxyElement.update (package:flutter/src/widgets/framework.dart:5816:5)
#16     Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#17     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5512:16)
#18     Element.rebuild (package:flutter/src/widgets/framework.dart:5203:7)
#19     ProxyElement.update (package:flutter/src/widgets/framework.dart:5816:5)
#20     _InheritedNotifierElement.update (package:flutter/src/widgets/inherited_notifier.dart:105:11)
#21     Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#22     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5512:16)
#23     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5650:11)
#24     Element.rebuild (package:flutter/src/widgets/framework.dart:5203:7)
#25     StatefulElement.update (package:flutter/src/widgets/framework.dart:5673:5)
#26     Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#27     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5512:16)
#28     Element.rebuild (package:flutter/src/widgets/framework.dart:5203:7)
#29     ProxyElement.update (package:flutter/src/widgets/framework.dart:5816:5)
#30     Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#31     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5512:16)
#32     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5650:11)
#33     Element.rebuild (package:flutter/src/widgets/framework.dart:5203:7)
#34     StatefulElement.update (package:flutter/src/widgets/framework.dart:5673:5)
#35     Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#36     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5512:16)
#37     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5650:11)
#38     Element.rebuild (package:flutter/src/widgets/framework.dart:5203:7)
#39     StatefulElement.update (package:flutter/src/widgets/framework.dart:5673:5)
#40     Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#41     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5512:16)
#42     Element.rebuild (package:flutter/src/widgets/framework.dart:5203:7)
#43     ProxyElement.update (package:flutter/src/widgets/framework.dart:5816:5)
#44     Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#45     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5512:16)
#46     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5650:11)
#47     Element.rebuild (package:flutter/src/widgets/framework.dart:5203:7)
#48     StatefulElement.update (package:flutter/src/widgets/framework.dart:5673:5)
#49     Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#50     SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6776:14)
#51     Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#52     SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6776:14)
#53     Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#54     SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6776:14)
#55     Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#56     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5512:16)
#57     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5650:11)
#58     Element.rebuild (package:flutter/src/widgets/framework.dart:5203:7)
#59     StatefulElement.update (package:flutter/src/widgets/framework.dart:5673:5)
#60     Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#61     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5512:16)
#62     Element.rebuild (package:flutter/src/widgets/framework.dart:5203:7)
#63     ProxyElement.update (package:flutter/src/widgets/framework.dart:5816:5)
#64     Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#65     SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6776:14)
#66     Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#67     SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6776:14)
#68     Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#69     SlottedRenderObjectElement._updateChildren (package:flutter/src/widgets/slotted_render_object_widget.dart:295:33)
#70     SlottedRenderObjectElement.update (package:flutter/src/widgets/slotted_render_object_widget.dart:256:5)
#71     Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#72     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5512:16)
#73     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5650:11)
#74     Element.rebuild (package:flutter/src/widgets/framework.dart:5203:7)
#75     StatefulElement.update (package:flutter/src/widgets/framework.dart:5673:5)
#76     Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#77     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5512:16)
#78     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5650:11)
#79     Element.rebuild (package:flutter/src/widgets/framework.dart:5203:7)
#80     StatefulElement.update (package:flutter/src/widgets/framework.dart:5673:5)
#81     Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#82     SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6776:14)
#83     Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#84     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5512:16)
#85     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5650:11)
#86     Element.rebuild (package:flutter/src/widgets/framework.dart:5203:7)
#87     StatefulElement.update (package:flutter/src/widgets/framework.dart:5673:5)
#88     Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#89     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5512:16)
#90     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5650:11)
#91     Element.rebuild (package:flutter/src/widgets/framework.dart:5203:7)
#92     StatefulElement.update (package:flutter/src/widgets/framework.dart:5673:5)
#93     Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#94     SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6776:14)
#95     Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#96     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5512:16)
#97     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5650:11)
#98     Element.rebuild (package:flutter/src/widgets/framework.dart:5203:7)
#99     StatefulElement.update (package:flutter/src/widgets/framework.dart:5673:5)
#100    Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#101    SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6776:14)
#102    Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#103    SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6776:14)
#104    Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#105    SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6776:14)
#106    Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#107    ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5512:16)
#108    StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5650:11)
#109    Element.rebuild (package:flutter/src/widgets/framework.dart:5203:7)
#110    StatefulElement.update (package:flutter/src/widgets/framework.dart:5673:5)
#111    Element.updateChild (package:flutter/src/widgets/framework.dart:3827:15)
#112    ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5512:16)
#113    StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5650:11)
#114    Element.rebuild (package:flutter/src/widgets/framework.dart:5203:7)
#115    BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2905:19)
#116    WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:1136:21)
#117    RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:443:5)
#118    SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1392:15)
#119    SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1313:9)
#120    SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:1171:5)
#121    _invoke (dart:ui/hooks.dart:312:13)
#122    PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:419:5)
#123    _drawFrame (dart:ui/hooks.dart:283:31)

flutter: [E] TIME: 2024-07-25T10:19:48.845998 Unhandled Exception: Bad state: TextPainter.text must be set to a non-null value before using the TextPainter.  ERROR: Bad state: TextPainter.text must be set to a non-null value before using the TextPainter.  STACKTRACE: #0      TextPainter.layout (package:flutter/src/painting/text_painter.dart:1168:7)
#1      RenderEditable._computeTextMetricsIfNeeded (package:flutter/src/rendering/editable.dart:2256:18)
#2      RenderEditable.getLocalRectForCaret (package:flutter/src/rendering/editable.dart:1788:5)
#3      EditableTextState._updateComposingRectIfNeeded (package:flutter/src/widgets/editable_text.dart:4487:38)
#4      EditableTextState._schedulePeriodicPostFrameCallbacks (package:flutter/src/widgets/editable_text.dart:4393:5)
#5      SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1392:15)
#6      SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1326:11)
#7      SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:1171:5)
#8      _invoke (dart:ui/hooks.dart:312:13)
#9      PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:419:5)
#10     _drawFrame (dart:ui/hooks.dart:283:31)

Flutter Doctor output

Issue produces on both Windows and MacOS. Not tested on mobile, but expect behavior to be the same.

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.22.3, on macOS 14.0 23A344 darwin-x64, locale en-NZ)
[✓] Android toolchain - develop for Android devices (Android SDK version 32.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 15.3)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2023.2)
[✓] VS Code (version 1.91.1)
[✓] Connected device (3 available)
[✓] Network resources

• No issues found!

Metadata

Metadata

Assignees

Labels

P2Important issues not at the top of the work lista: error messageError messages from the Flutter frameworka: text inputEntering text in a text field or keyboard related problemsc: crashStack traces logged to the consolefound in release: 3.22Found to occur in 3.22found in release: 3.24Found to occur in 3.24frameworkflutter/packages/flutter repository. See also f: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onr: fixedIssue is closed as already fixed in a newer versionteam-text-inputOwned by Text Input team

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions