Version 2.15.0-298.0.dev

Merge commit '304330b6fde5a0c1e35000901e0445666eef6749' into 'dev'
diff --git a/pkg/analysis_server/lib/src/analysis_server_abstract.dart b/pkg/analysis_server/lib/src/analysis_server_abstract.dart
index 9e758b6..4280b0d 100644
--- a/pkg/analysis_server/lib/src/analysis_server_abstract.dart
+++ b/pkg/analysis_server/lib/src/analysis_server_abstract.dart
@@ -44,12 +44,14 @@
     show EvictingFileByteStore;
 import 'package:analyzer/src/dart/analysis/file_content_cache.dart';
 import 'package:analyzer/src/dart/analysis/performance_logger.dart';
+import 'package:analyzer/src/dart/analysis/results.dart';
 import 'package:analyzer/src/dart/ast/element_locator.dart';
 import 'package:analyzer/src/dart/ast/utilities.dart';
 import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart';
 import 'package:analyzer/src/generated/sdk.dart';
 import 'package:analyzer/src/services/available_declarations.dart';
 import 'package:analyzer/src/util/file_paths.dart' as file_paths;
+import 'package:analyzer/src/util/performance/operation_performance.dart';
 import 'package:collection/collection.dart';
 import 'package:http/http.dart' as http;
 import 'package:meta/meta.dart';
@@ -302,8 +304,14 @@
   }
 
   DartdocDirectiveInfo getDartdocDirectiveInfoFor(ResolvedUnitResult result) {
+    return getDartdocDirectiveInfoForSession(result.session);
+  }
+
+  DartdocDirectiveInfo getDartdocDirectiveInfoForSession(
+    AnalysisSession session,
+  ) {
     return declarationsTracker
-            ?.getContext(result.session.analysisContext)
+            ?.getContext(session.analysisContext)
             ?.dartdocDirectiveInfo ??
         DartdocDirectiveInfo();
   }
@@ -312,7 +320,14 @@
   /// context that produced the [result], or `null` if there is no cache for the
   /// context.
   DocumentationCache? getDocumentationCacheFor(ResolvedUnitResult result) {
-    var context = result.session.analysisContext;
+    return getDocumentationCacheForSession(result.session);
+  }
+
+  /// Return the object used to cache the documentation for elements in the
+  /// context that produced the [session], or `null` if there is no cache for
+  /// the context.
+  DocumentationCache? getDocumentationCacheForSession(AnalysisSession session) {
+    var context = session.analysisContext;
     var tracker = declarationsTracker?.getContext(context);
     if (tracker == null) {
       return null;
@@ -462,6 +477,31 @@
     contextManager.refresh();
   }
 
+  ResolvedForCompletionResultImpl? resolveForCompletion({
+    required String path,
+    required int offset,
+    required OperationPerformanceImpl performance,
+  }) {
+    if (!file_paths.isDart(resourceProvider.pathContext, path)) {
+      return null;
+    }
+
+    var driver = getAnalysisDriver(path);
+    if (driver == null) {
+      return null;
+    }
+
+    try {
+      return driver.resolveForCompletion(
+        path: path,
+        offset: offset,
+        performance: performance,
+      );
+    } catch (e, st) {
+      instrumentationService.logException(e, st);
+    }
+  }
+
   /// Sends an error notification to the user.
   void sendServerErrorNotification(
     String message,
diff --git a/pkg/analysis_server/lib/src/domain_completion.dart b/pkg/analysis_server/lib/src/domain_completion.dart
index 6bf064f..10b3a53 100644
--- a/pkg/analysis_server/lib/src/domain_completion.dart
+++ b/pkg/analysis_server/lib/src/domain_completion.dart
@@ -31,6 +31,7 @@
 import 'package:analyzer_plugin/protocol/protocol_common.dart';
 import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
 import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
+import 'package:collection/collection.dart';
 
 /// Instances of the class [CompletionDomainHandler] implement a
 /// [RequestHandler] that handles requests in the completion domain.
@@ -302,9 +303,13 @@
       'request',
       (performance) async {
         var resolvedUnit = await performance.runAsync(
-          'getResolvedUnit',
+          'resolveForCompletion',
           (performance) async {
-            return await server.getResolvedUnit(file);
+            return server.resolveForCompletion(
+              path: file,
+              offset: offset,
+              performance: performance,
+            );
           },
         );
         if (resolvedUnit == null) {
@@ -329,13 +334,21 @@
         );
         performanceList.add(completionPerformance);
 
-        var completionRequest = DartCompletionRequest.forResolvedUnit(
-          resolvedUnit: resolvedUnit,
+        var analysisSession = resolvedUnit.analysisSession;
+        var enclosingNode =
+            resolvedUnit.resolvedNodes.lastOrNull ?? resolvedUnit.parsedUnit;
+
+        var completionRequest = DartCompletionRequest(
+          analysisSession: analysisSession,
+          filePath: resolvedUnit.path,
+          fileContent: resolvedUnit.content,
+          unitElement: resolvedUnit.unitElement,
+          enclosingNode: enclosingNode,
           offset: offset,
-          dartdocDirectiveInfo: server.getDartdocDirectiveInfoFor(
-            resolvedUnit,
-          ),
-          documentationCache: server.getDocumentationCacheFor(resolvedUnit),
+          dartdocDirectiveInfo:
+              server.getDartdocDirectiveInfoForSession(analysisSession),
+          documentationCache:
+              server.getDocumentationCacheForSession(analysisSession),
         );
         setNewRequest(completionRequest);
 
@@ -391,15 +404,17 @@
           }
         }
 
-        server.sendResponse(
-          CompletionGetSuggestions2Result(
-            completionRequest.replacementOffset,
-            completionRequest.replacementLength,
-            lengthRestricted,
-            librariesToImport.keys.map((e) => '$e').toList(),
-            isIncomplete,
-          ).toResponse(request.id),
-        );
+        performance.run('sendResponse', (_) {
+          server.sendResponse(
+            CompletionGetSuggestions2Result(
+              completionRequest.replacementOffset,
+              completionRequest.replacementLength,
+              lengthRestricted,
+              librariesToImport.keys.map((e) => '$e').toList(),
+              isIncomplete,
+            ).toResponse(request.id),
+          );
+        });
       },
     );
   }
diff --git a/pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart b/pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart
index 2a8a2ae..a09c3b7 100644
--- a/pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart
+++ b/pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart
@@ -19,7 +19,6 @@
 import 'package:analyzer/file_system/file_system.dart';
 import 'package:analyzer/instrumentation/service.dart';
 import 'package:analyzer/source/error_processor.dart';
-import 'package:analyzer/source/source_range.dart';
 import 'package:analyzer/src/error/codes.dart';
 import 'package:analyzer/src/util/file_paths.dart' as file_paths;
 import 'package:analyzer_plugin/src/utilities/change_builder/change_builder_core.dart';
@@ -209,7 +208,6 @@
     final analysisOptions = unit.session.analysisContext.analysisOptions;
 
     var overrideSet = _readOverrideSet(unit);
-    var lockRanges = <String, List<SourceRange>>{};
     for (var error in errors) {
       final processor = ErrorProcessor.getProcessor(analysisOptions, error);
       // Only fix errors not filtered out in analysis options.
@@ -221,7 +219,7 @@
           error,
           (name) => [],
         );
-        await _fixSingleError(fixContext, unit, error, overrideSet, lockRanges);
+        await _fixSingleError(fixContext, unit, error, overrideSet);
       }
     }
 
@@ -238,7 +236,6 @@
       var errors = List.from(unitResult.errors, growable: false);
       errors.sort((a, b) => a.offset.compareTo(b.offset));
 
-      var lockRanges = <String, List<SourceRange>>{};
       for (var error in errors) {
         var processor = ErrorProcessor.getProcessor(analysisOptions, error);
         // Only fix errors not filtered out in analysis options.
@@ -250,8 +247,7 @@
             error,
             (name) => [],
           );
-          await _fixSingleError(
-              fixContext, unitResult, error, overrideSet, lockRanges);
+          await _fixSingleError(fixContext, unitResult, error, overrideSet);
         }
       }
     }
@@ -264,13 +260,11 @@
       DartFixContext fixContext,
       ResolvedUnitResult result,
       AnalysisError diagnostic,
-      TransformOverrideSet? overrideSet,
-      Map<String, List<SourceRange>> lockRanges) async {
+      TransformOverrideSet? overrideSet) async {
     var context = CorrectionProducerContext.create(
       applyingBulkFixes: true,
       dartFixContext: fixContext,
       diagnostic: diagnostic,
-      lockRanges: lockRanges,
       overrideSet: overrideSet,
       resolvedResult: result,
       selectionOffset: diagnostic.offset,
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/abstract_producer.dart b/pkg/analysis_server/lib/src/services/correction/dart/abstract_producer.dart
index c1d4638..96389a0 100644
--- a/pkg/analysis_server/lib/src/services/correction/dart/abstract_producer.dart
+++ b/pkg/analysis_server/lib/src/services/correction/dart/abstract_producer.dart
@@ -241,7 +241,7 @@
 
   /// A map keyed by lock names whose value is a list of the ranges for which a
   /// lock has already been acquired.
-  final Map<String, List<SourceRange>> lockRanges;
+  final Map<String, List<SourceRange>> _lockRanges = {};
 
   CorrectionProducerContext._({
     required this.resolvedResult,
@@ -253,7 +253,6 @@
     this.overrideSet,
     this.selectionOffset = -1,
     this.selectionLength = 0,
-    required this.lockRanges,
   })  : file = resolvedResult.path,
         session = resolvedResult.session,
         sessionHelper = AnalysisSessionHelper(resolvedResult.session),
@@ -277,7 +276,6 @@
     TransformOverrideSet? overrideSet,
     int selectionOffset = -1,
     int selectionLength = 0,
-    Map<String, List<SourceRange>>? lockRanges,
   }) {
     var selectionEnd = selectionOffset + selectionLength;
     var locator = NodeLocator(selectionOffset, selectionEnd);
@@ -294,7 +292,6 @@
       overrideSet: overrideSet,
       selectionOffset: selectionOffset,
       selectionLength: selectionLength,
-      lockRanges: lockRanges ?? {},
     );
   }
 }
@@ -473,7 +470,7 @@
   /// ensure this behavior by attempting to acquire a lock prior to creating any
   /// edits, and only create the edits if a lock could be acquired.
   bool acquireLockOnRange(String lockName, SourceRange range) {
-    var ranges = _context.lockRanges.putIfAbsent(lockName, () => []);
+    var ranges = _context._lockRanges.putIfAbsent(lockName, () => []);
     if (ranges.contains(range)) {
       return false;
     }
diff --git a/pkg/analysis_server/test/src/services/correction/fix/data_driven/data_driven_test_support.dart b/pkg/analysis_server/test/src/services/correction/fix/data_driven/data_driven_test_support.dart
index c464828..e26e0061 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/data_driven/data_driven_test_support.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/data_driven/data_driven_test_support.dart
@@ -2,8 +2,6 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-import 'package:analysis_server/src/services/correction/bulk_fix_processor.dart';
-import 'package:analysis_server/src/services/correction/change_workspace.dart';
 import 'package:analysis_server/src/services/correction/dart/data_driven.dart';
 import 'package:analysis_server/src/services/correction/fix.dart';
 import 'package:analysis_server/src/services/correction/fix/data_driven/code_template.dart';
@@ -11,72 +9,13 @@
 import 'package:analysis_server/src/services/correction/fix/data_driven/transform_set.dart';
 import 'package:analysis_server/src/services/correction/fix/data_driven/transform_set_manager.dart';
 import 'package:analyzer/error/error.dart';
-import 'package:analyzer/src/dart/analysis/byte_store.dart';
 import 'package:analyzer/src/dart/error/hint_codes.dart';
-import 'package:analyzer/src/services/available_declarations.dart';
-import 'package:analyzer/src/test_utilities/platform.dart';
-import 'package:analyzer_plugin/protocol/protocol_common.dart' show SourceEdit;
 import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
-import 'package:test/test.dart';
 
-import '../../../../../utils/test_instrumentation_service.dart';
 import '../fix_processor.dart';
 
 /// A base class defining support for writing fix processor tests for
 /// data-driven fixes.
-abstract class DataDrivenBulkFixProcessorTest
-    extends DataDrivenFixProcessorTest {
-  /// Return `true` if this test uses config files.
-  bool get useConfigFiles => false;
-
-  /// The workspace in which fixes contributor operates.
-  @override
-  DartChangeWorkspace get workspace {
-    return DartChangeWorkspace([session]);
-  }
-
-  @override
-  Future<void> assertHasFix(String expected,
-      {bool Function(AnalysisError)? errorFilter,
-      int? length,
-      String? target,
-      int? expectedNumberOfFixesForKind,
-      String? matchFixMessage,
-      bool allowFixAllFixes = false}) async {
-    if (useLineEndingsForPlatform) {
-      expected = normalizeNewlinesForPlatform(expected);
-    }
-    var processor = await computeFixes();
-    change = processor.builder.sourceChange;
-
-    // apply to "file"
-    var fileEdits = change.edits;
-    expect(fileEdits, hasLength(1));
-
-    var fileContent = testCode;
-    if (target != null) {
-      expect(fileEdits.first.file, convertPath(target));
-      fileContent = getFile(target).readAsStringSync();
-    }
-
-    resultCode = SourceEdit.applySequence(fileContent, change.edits[0].edits);
-    expect(resultCode, expected);
-  }
-
-  /// Computes fixes for the specified [testUnit].
-  Future<BulkFixProcessor> computeFixes() async {
-    var tracker = DeclarationsTracker(MemoryByteStore(), resourceProvider);
-    var analysisContext = contextFor(testFile);
-    tracker.addContext(analysisContext);
-    var processor = BulkFixProcessor(TestInstrumentationService(), workspace,
-        useConfigFiles: useConfigFiles);
-    await processor.fixErrors([analysisContext]);
-    return processor;
-  }
-}
-
-/// A base class defining support for writing fix processor tests for
-/// data-driven fixes.
 abstract class DataDrivenFixProcessorTest extends FixProcessorTest {
   /// Return the URI used to import the library created by [setPackageContent].
   String get importUri => 'package:p/lib.dart';
diff --git a/pkg/analysis_server/test/src/services/correction/fix/data_driven/flutter_bulk_test.dart b/pkg/analysis_server/test/src/services/correction/fix/data_driven/flutter_bulk_test.dart
deleted file mode 100644
index 6e30841..0000000
--- a/pkg/analysis_server/test/src/services/correction/fix/data_driven/flutter_bulk_test.dart
+++ /dev/null
@@ -1,178 +0,0 @@
-// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import 'package:test_reflective_loader/test_reflective_loader.dart';
-
-import 'data_driven_test_support.dart';
-
-void main() {
-  defineReflectiveSuite(() {
-    defineReflectiveTests(FlutterBulkTest);
-  });
-}
-
-@reflectiveTest
-class FlutterBulkTest extends DataDrivenBulkFixProcessorTest {
-  Future<void>
-      test_material_ThemeData_textSelectionHandleColor_deprecated() async {
-    setPackageContent('''
-class ThemeData {
-  ThemeData({
-    @deprecated Color? textSelectionHandleColor,
-    @deprecated bool useTextSelectionTheme = false,
-    TextSelectionThemeData? textSelectionTheme}) {}
-}
-class TextSelectionThemeData {
-  TextSelectionThemeData({Color selectionHandleColor}) {}
-}
-class Color {}
-class Colors {
-  static Color yellow = Color();
-}
-''');
-    addPackageDataFile('''
-version: 1
-transforms:
-  - title: "Migrate to 'TextSelectionThemeData'"
-    date: 2020-09-24
-    element:
-      uris: ['$importUri']
-      constructor: ''
-      inClass: 'ThemeData'
-    oneOf:
-      - if: "textSelectionColor != '' && cursorColor != '' && textSelectionHandleColor != ''"
-        changes:
-          - kind: 'addParameter'
-            index: 73
-            name: 'textSelectionTheme'
-            style: optional_named
-            argumentValue:
-              expression: 'TextSelectionThemeData(cursorColor: {% cursorColor %}, selectionColor: {% textSelectionColor %}, selectionHandleColor: {% textSelectionHandleColor %},)'
-              requiredIf: "textSelectionColor != '' && cursorColor != '' && textSelectionHandleColor != ''"
-          - kind: 'removeParameter'
-            name: 'textSelectionColor'
-          - kind: 'removeParameter'
-            name: 'cursorColor'
-          - kind: 'removeParameter'
-            name: 'textSelectionHandleColor'
-          - kind: 'removeParameter'
-            name: 'useTextSelectionTheme'
-      - if: "textSelectionColor == '' && cursorColor != '' && textSelectionHandleColor != ''"
-        changes:
-          - kind: 'addParameter'
-            index: 73
-            name: 'textSelectionTheme'
-            style: optional_named
-            argumentValue:
-              expression: 'TextSelectionThemeData(cursorColor: {% cursorColor %}, selectionHandleColor: {% textSelectionHandleColor %},)'
-              requiredIf: "textSelectionColor == '' && cursorColor != '' && textSelectionHandleColor != ''"
-          - kind: 'removeParameter'
-            name: 'cursorColor'
-          - kind: 'removeParameter'
-            name: 'textSelectionHandleColor'
-          - kind: 'removeParameter'
-            name: 'useTextSelectionTheme'
-      - if: "textSelectionColor != '' && cursorColor != '' && textSelectionHandleColor == ''"
-        changes:
-          - kind: 'addParameter'
-            index: 73
-            name: 'textSelectionTheme'
-            style: optional_named
-            argumentValue:
-              expression: 'TextSelectionThemeData(cursorColor: {% cursorColor %}, selectionColor: {% textSelectionColor %},)'
-              requiredIf: "textSelectionColor != '' && cursorColor != '' && textSelectionHandleColor == ''"
-          - kind: 'removeParameter'
-            name: 'textSelectionColor'
-          - kind: 'removeParameter'
-            name: 'cursorColor'
-          - kind: 'removeParameter'
-            name: 'useTextSelectionTheme'
-      - if: "textSelectionColor != '' && cursorColor == '' && textSelectionHandleColor != ''"
-        changes:
-          - kind: 'addParameter'
-            index: 73
-            name: 'textSelectionTheme'
-            style: optional_named
-            argumentValue:
-              expression: 'TextSelectionThemeData(selectionColor: {% textSelectionColor %}, selectionHandleColor: {% textSelectionHandleColor %},)'
-              requiredIf: "textSelectionColor != '' && cursorColor == '' && textSelectionHandleColor != ''"
-          - kind: 'removeParameter'
-            name: 'textSelectionColor'
-          - kind: 'removeParameter'
-            name: 'textSelectionHandleColor'
-          - kind: 'removeParameter'
-            name: 'useTextSelectionTheme'
-      - if: "textSelectionColor == '' && cursorColor != '' && textSelectionHandleColor == ''"
-        changes:
-          - kind: 'addParameter'
-            index: 73
-            name: 'textSelectionTheme'
-            style: optional_named
-            argumentValue:
-              expression: 'TextSelectionThemeData(cursorColor: {% cursorColor %})'
-              requiredIf: "textSelectionColor == '' && cursorColor != '' && textSelectionHandleColor == ''"
-          - kind: 'removeParameter'
-            name: 'cursorColor'
-          - kind: 'removeParameter'
-            name: 'useTextSelectionTheme'
-      - if: "textSelectionColor != '' && cursorColor == '' && textSelectionHandleColor == ''"
-        changes:
-          - kind: 'addParameter'
-            index: 73
-            name: 'textSelectionTheme'
-            style: optional_named
-            argumentValue:
-              expression: 'TextSelectionThemeData(selectionColor: {% textSelectionColor %})'
-              requiredIf: "textSelectionColor != '' && cursorColor == '' && textSelectionHandleColor == ''"
-          - kind: 'removeParameter'
-            name: 'textSelectionColor'
-          - kind: 'removeParameter'
-            name: 'useTextSelectionTheme'
-      - if: "textSelectionColor == '' && cursorColor == '' && textSelectionHandleColor != ''"
-        changes:
-          - kind: 'addParameter'
-            index: 73
-            name: 'textSelectionTheme'
-            style: optional_named
-            argumentValue:
-              expression: 'TextSelectionThemeData(selectionHandleColor: {% textSelectionHandleColor %})'
-              requiredIf: "textSelectionColor == '' && cursorColor == '' && textSelectionHandleColor != ''"
-          - kind: 'removeParameter'
-            name: 'textSelectionHandleColor'
-          - kind: 'removeParameter'
-            name: 'useTextSelectionTheme'
-      - if: "useTextSelectionTheme != ''"
-        changes:
-          - kind: 'removeParameter'
-            name: 'useTextSelectionTheme'
-    variables:
-      textSelectionColor:
-        kind: 'fragment'
-        value: 'arguments[textSelectionColor]'
-      cursorColor:
-        kind: 'fragment'
-        value: 'arguments[cursorColor]'
-      textSelectionHandleColor:
-        kind: 'fragment'
-        value: 'arguments[textSelectionHandleColor]'
-      useTextSelectionTheme:
-        kind: 'fragment'
-        value: 'arguments[useTextSelectionTheme]'
-''');
-    await resolveTestCode('''
-import '$importUri';
-
-void f() {
-  ThemeData(textSelectionHandleColor: Colors.yellow, useTextSelectionTheme: false);
-}
-''');
-    await assertHasFix('''
-import '$importUri';
-
-void f() {
-  ThemeData(textSelectionTheme: TextSelectionThemeData(selectionHandleColor: Colors.yellow));
-}
-''');
-  }
-}
diff --git a/pkg/analysis_server/test/src/services/correction/fix/data_driven/test_all.dart b/pkg/analysis_server/test/src/services/correction/fix/data_driven/test_all.dart
index b2ced8a..64cffa2 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/data_driven/test_all.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/data_driven/test_all.dart
@@ -11,7 +11,6 @@
 import 'diagnostics/test_all.dart' as diagnostics;
 import 'element_matcher_test.dart' as element_matcher;
 import 'end_to_end_test.dart' as end_to_end;
-import 'flutter_bulk_test.dart' as flutter_bulk;
 import 'flutter_use_case_test.dart' as flutter_use_case;
 import 'modify_parameters_test.dart' as modify_parameters;
 import 'rename_parameter_test.dart' as rename_parameter;
@@ -31,7 +30,6 @@
     diagnostics.main();
     element_matcher.main();
     end_to_end.main();
-    flutter_bulk.main();
     flutter_use_case.main();
     modify_parameters.main();
     rename_parameter.main();
diff --git a/pkg/analysis_server/test/src/services/correction/fix/remove_argument_test.dart b/pkg/analysis_server/test/src/services/correction/fix/remove_argument_test.dart
index 87759cd..736da8c 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/remove_argument_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/remove_argument_test.dart
@@ -21,7 +21,7 @@
   @override
   String get lintCode => LintNames.avoid_redundant_argument_values;
 
-  Future<void> test_independentInvocations() async {
+  Future<void> test_singleFile() async {
     await resolveTestCode('''
 void f({bool valWithDefault = true, bool val}) {}
 void f2({bool valWithDefault = true, bool val}) {}
@@ -41,23 +41,6 @@
 }
 ''');
   }
-
-  Future<void> test_multipleInSingleInvocation() async {
-    await resolveTestCode('''
-void f() {
-  g(a: 0, b: 1, c: 2);
-}
-
-void g({int a = 0, int b = 1, int c = 2}) {}
-''');
-    await assertHasFix('''
-void f() {
-  g();
-}
-
-void g({int a = 0, int b = 1, int c = 2}) {}
-''');
-  }
 }
 
 @reflectiveTest
diff --git a/pkg/analyzer/lib/src/dart/analysis/driver.dart b/pkg/analyzer/lib/src/dart/analysis/driver.dart
index f0cabd7..fee6a89 100644
--- a/pkg/analyzer/lib/src/dart/analysis/driver.dart
+++ b/pkg/analyzer/lib/src/dart/analysis/driver.dart
@@ -1328,6 +1328,7 @@
     );
 
     return ResolvedForCompletionResultImpl(
+      analysisSession: currentSession,
       path: path,
       uri: file.uri,
       exists: file.exists,
diff --git a/pkg/analyzer/lib/src/dart/analysis/results.dart b/pkg/analyzer/lib/src/dart/analysis/results.dart
index cc7a673..a8184db 100644
--- a/pkg/analyzer/lib/src/dart/analysis/results.dart
+++ b/pkg/analyzer/lib/src/dart/analysis/results.dart
@@ -168,6 +168,7 @@
 }
 
 class ResolvedForCompletionResultImpl {
+  final AnalysisSession analysisSession;
   final String path;
   final Uri uri;
   final bool exists;
@@ -196,6 +197,7 @@
   final List<AstNode> resolvedNodes;
 
   ResolvedForCompletionResultImpl({
+    required this.analysisSession,
     required this.path,
     required this.uri,
     required this.exists,
diff --git a/pkg/analyzer_plugin/lib/src/protocol/protocol_internal.dart b/pkg/analyzer_plugin/lib/src/protocol/protocol_internal.dart
index 459974c..777ccda 100644
--- a/pkg/analyzer_plugin/lib/src/protocol/protocol_internal.dart
+++ b/pkg/analyzer_plugin/lib/src/protocol/protocol_internal.dart
@@ -4,7 +4,6 @@
 
 import 'dart:collection';
 import 'dart:convert' hide JsonDecoder;
-import 'dart:math' as math;
 
 import 'package:analyzer_plugin/protocol/protocol.dart';
 import 'package:analyzer_plugin/protocol/protocol_common.dart';
@@ -29,17 +28,6 @@
 /// If the invariants can't be preserved, then a [ConflictingEditException] is
 /// thrown.
 void addEditForSource(SourceFileEdit sourceFileEdit, SourceEdit sourceEdit) {
-  /// If the [leftEdit] and the [rightEdit] can be merged, then merge them.
-  SourceEdit? _merge(SourceEdit leftEdit, SourceEdit rightEdit) {
-    assert(leftEdit.offset <= rightEdit.offset);
-    if (leftEdit.isDeletion && rightEdit.isDeletion) {
-      var offset = leftEdit.offset;
-      var end = math.max(leftEdit.end, rightEdit.end);
-      return SourceEdit(offset, end - offset, '');
-    }
-    return null;
-  }
-
   var edits = sourceFileEdit.edits;
   var length = edits.length;
   var index = 0;
@@ -51,12 +39,7 @@
     // The [previousEdit] has an offset that is strictly greater than the offset
     // of the [sourceEdit] so we only need to look at the end of the
     // [sourceEdit] to know whether they overlap.
-    if (sourceEdit.end > previousEdit.offset) {
-      var mergedEdit = _merge(sourceEdit, previousEdit);
-      if (mergedEdit != null) {
-        edits[index - 1] = mergedEdit;
-        return;
-      }
+    if (sourceEdit.offset + sourceEdit.length > previousEdit.offset) {
       throw ConflictingEditException(
           newEdit: sourceEdit, existingEdit: previousEdit);
     }
@@ -71,12 +54,7 @@
     if ((sourceEdit.offset == nextEdit.offset &&
             sourceEdit.length > 0 &&
             nextEdit.length > 0) ||
-        nextEdit.end > sourceEdit.offset) {
-      var mergedEdit = _merge(nextEdit, sourceEdit);
-      if (mergedEdit != null) {
-        edits[index] = mergedEdit;
-        return;
-      }
+        nextEdit.offset + nextEdit.length > sourceEdit.offset) {
       throw ConflictingEditException(
           newEdit: sourceEdit, existingEdit: nextEdit);
     }
@@ -490,11 +468,3 @@
   /// the given [id], where the request was received at the given [requestTime].
   Response toResponse(String id, int requestTime);
 }
-
-extension SourceEditExtensions on SourceEdit {
-  /// Return `true` if this source edit represents a deletion.
-  bool get isDeletion => replacement.isEmpty;
-
-  /// Return `true` if this source edit represents an insertion.
-  bool get isInsertion => length == 0;
-}
diff --git a/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_core_test.dart b/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_core_test.dart
index 6ed22f9..a571f04 100644
--- a/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_core_test.dart
+++ b/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_core_test.dart
@@ -17,7 +17,6 @@
   defineReflectiveSuite(() {
     defineReflectiveTests(ChangeBuilderImplTest);
     defineReflectiveTests(EditBuilderImplTest);
-    defineReflectiveTests(FileEditBuilderImpl_ConflictingTest);
     defineReflectiveTests(FileEditBuilderImplTest);
     defineReflectiveTests(LinkedEditBuilderImplTest);
   });
@@ -298,33 +297,27 @@
   }
 }
 
-/// Tests that are specifically targeted at the handling of conflicting edits.
 @reflectiveTest
-class FileEditBuilderImpl_ConflictingTest extends AbstractChangeBuilderTest {
+class FileEditBuilderImplTest extends AbstractChangeBuilderTest {
   String path = '/test.dart';
 
-  Matcher get hasConflict => throwsA(isA<ConflictingEditException>());
-
-  Future<void> test_deletion_deletion_adjacent_left() async {
-    var firstOffset = 30;
-    var firstLength = 5;
-    var secondOffset = 23;
-    var secondLength = 7;
+  Future<void> test_addDeletion() async {
+    var offset = 23;
+    var length = 7;
     await builder.addGenericFileEdit(path, (builder) {
-      builder.addDeletion(SourceRange(firstOffset, firstLength));
-      builder.addDeletion(SourceRange(secondOffset, secondLength));
+      builder.addDeletion(SourceRange(offset, length));
     });
     var edits = builder.sourceChange.edits[0].edits;
-    expect(edits, hasLength(2));
-    expect(edits[0].offset, firstOffset);
-    expect(edits[0].length, firstLength);
+    expect(edits, hasLength(1));
+    expect(edits[0].offset, offset);
+    expect(edits[0].length, length);
     expect(edits[0].replacement, isEmpty);
-    expect(edits[1].offset, secondOffset);
-    expect(edits[1].length, secondLength);
-    expect(edits[1].replacement, isEmpty);
   }
 
-  Future<void> test_deletion_deletion_adjacent_right() async {
+  Future<void> test_addDeletion_adjacent_lowerOffsetFirst() async {
+    // TODO(brianwilkerson) This should also merge the deletions, but is written
+    //  to ensure that existing uses of FileEditBuilder continue to work even
+    //  without that change.
     var firstOffset = 23;
     var firstLength = 7;
     var secondOffset = 30;
@@ -343,23 +336,31 @@
     expect(edits[1].replacement, isEmpty);
   }
 
-  Future<void> test_deletion_deletion_overlap_left() async {
-    var firstOffset = 27;
-    var firstLength = 8;
-    var secondOffset = 23;
-    var secondLength = 7;
+  Future<void> test_addDeletion_adjacent_lowerOffsetSecond() async {
+    // TODO(brianwilkerson) This should also merge the deletions, but is written
+    //  to ensure that existing uses of FileEditBuilder continue to work even
+    //  without that change.
+    var firstOffset = 23;
+    var firstLength = 7;
+    var secondOffset = 30;
+    var secondLength = 5;
     await builder.addGenericFileEdit(path, (builder) {
-      builder.addDeletion(SourceRange(firstOffset, firstLength));
       builder.addDeletion(SourceRange(secondOffset, secondLength));
+      builder.addDeletion(SourceRange(firstOffset, firstLength));
     });
     var edits = builder.sourceChange.edits[0].edits;
-    expect(edits, hasLength(1));
+    expect(edits, hasLength(2));
     expect(edits[0].offset, secondOffset);
-    expect(edits[0].length, firstOffset + firstLength - secondOffset);
+    expect(edits[0].length, secondLength);
     expect(edits[0].replacement, isEmpty);
+    expect(edits[1].offset, firstOffset);
+    expect(edits[1].length, firstLength);
+    expect(edits[1].replacement, isEmpty);
   }
 
-  Future<void> test_deletion_deletion_overlap_right() async {
+  @failingTest
+  Future<void> test_addDeletion_overlapping() async {
+    // This support is not yet implemented.
     var firstOffset = 23;
     var firstLength = 7;
     var secondOffset = 27;
@@ -375,169 +376,6 @@
     expect(edits[0].replacement, isEmpty);
   }
 
-  Future<void> test_deletion_insertion_adjacent_left() async {
-    var deletionOffset = 23;
-    var deletionLength = 7;
-    var insertionOffset = 23;
-    var insertionText = 'x';
-    await builder.addGenericFileEdit(path, (builder) {
-      builder.addDeletion(SourceRange(deletionOffset, deletionLength));
-      expect(() {
-        builder.addSimpleInsertion(insertionOffset, insertionText);
-      }, hasConflict);
-    });
-    var edits = builder.sourceChange.edits[0].edits;
-    expect(edits, hasLength(1));
-    expect(edits[0].offset, deletionOffset);
-    expect(edits[0].length, deletionLength);
-    expect(edits[0].replacement, '');
-  }
-
-  Future<void> test_deletion_insertion_adjacent_right() async {
-    var deletionOffset = 23;
-    var deletionLength = 7;
-    var insertionOffset = 30;
-    var insertionText = 'x';
-    await builder.addGenericFileEdit(path, (builder) {
-      builder.addDeletion(SourceRange(deletionOffset, deletionLength));
-      builder.addSimpleInsertion(insertionOffset, insertionText);
-    });
-    var edits = builder.sourceChange.edits[0].edits;
-    expect(edits, hasLength(2));
-    expect(edits[0].offset, insertionOffset);
-    expect(edits[0].length, 0);
-    expect(edits[0].replacement, insertionText);
-    expect(edits[1].offset, deletionOffset);
-    expect(edits[1].length, deletionLength);
-    expect(edits[1].replacement, isEmpty);
-  }
-
-  Future<void> test_deletion_insertion_overlap() async {
-    var deletionOffset = 23;
-    var deletionLength = 7;
-    var insertionOffset = 26;
-    var insertionText = 'x';
-    await builder.addGenericFileEdit(path, (builder) {
-      builder.addDeletion(SourceRange(deletionOffset, deletionLength));
-      expect(() {
-        builder.addSimpleInsertion(insertionOffset, insertionText);
-      }, hasConflict);
-    });
-    var edits = builder.sourceChange.edits[0].edits;
-    expect(edits, hasLength(1));
-    expect(edits[0].offset, deletionOffset);
-    expect(edits[0].length, deletionLength);
-    expect(edits[0].replacement, '');
-  }
-
-  Future<void> test_insertion_deletion_adjacent_left() async {
-    var deletionOffset = 23;
-    var deletionLength = 7;
-    var insertionOffset = 23;
-    var insertionText = 'x';
-    await builder.addGenericFileEdit(path, (builder) {
-      builder.addSimpleInsertion(insertionOffset, insertionText);
-      builder.addDeletion(SourceRange(deletionOffset, deletionLength));
-    });
-    var edits = builder.sourceChange.edits[0].edits;
-    expect(edits, hasLength(2));
-    expect(edits[0].offset, deletionOffset);
-    expect(edits[0].length, deletionLength);
-    expect(edits[0].replacement, isEmpty);
-    expect(edits[1].offset, insertionOffset);
-    expect(edits[1].length, 0);
-    expect(edits[1].replacement, insertionText);
-  }
-
-  Future<void> test_insertion_deletion_adjacent_right() async {
-    var deletionOffset = 23;
-    var deletionLength = 7;
-    var insertionOffset = 30;
-    var insertionText = 'x';
-    await builder.addGenericFileEdit(path, (builder) {
-      builder.addSimpleInsertion(insertionOffset, insertionText);
-      builder.addDeletion(SourceRange(deletionOffset, deletionLength));
-    });
-    var edits = builder.sourceChange.edits[0].edits;
-    expect(edits, hasLength(2));
-    expect(edits[0].offset, insertionOffset);
-    expect(edits[0].length, 0);
-    expect(edits[0].replacement, insertionText);
-    expect(edits[1].offset, deletionOffset);
-    expect(edits[1].length, deletionLength);
-    expect(edits[1].replacement, isEmpty);
-  }
-
-  Future<void> test_insertion_deletion_overlap() async {
-    var deletionOffset = 23;
-    var deletionLength = 7;
-    var insertionOffset = 26;
-    var insertionText = 'x';
-    await builder.addGenericFileEdit(path, (builder) {
-      builder.addSimpleInsertion(insertionOffset, insertionText);
-      expect(() {
-        builder.addDeletion(SourceRange(deletionOffset, deletionLength));
-      }, hasConflict);
-    });
-    var edits = builder.sourceChange.edits[0].edits;
-    expect(edits, hasLength(1));
-    expect(edits[0].offset, insertionOffset);
-    expect(edits[0].length, 0);
-    expect(edits[0].replacement, insertionText);
-  }
-
-  Future<void> test_replacement_replacement_overlap_left() async {
-    var offset = 23;
-    var length = 7;
-    var text = 'x';
-    await builder.addGenericFileEdit(path, (builder) {
-      builder.addSimpleReplacement(SourceRange(offset, length), text);
-      expect(() {
-        builder.addSimpleReplacement(SourceRange(offset - 2, length), text);
-      }, hasConflict);
-    });
-    var edits = builder.sourceChange.edits[0].edits;
-    expect(edits, hasLength(1));
-    expect(edits[0].offset, offset);
-    expect(edits[0].length, length);
-    expect(edits[0].replacement, text);
-  }
-
-  Future<void> test_replacement_replacement_overlap_right() async {
-    var offset = 23;
-    var length = 7;
-    var text = 'x';
-    await builder.addGenericFileEdit(path, (builder) {
-      builder.addSimpleReplacement(SourceRange(offset, length), text);
-      expect(() {
-        builder.addSimpleReplacement(SourceRange(offset + 2, length), text);
-      }, hasConflict);
-    });
-    var edits = builder.sourceChange.edits[0].edits;
-    expect(edits, hasLength(1));
-    expect(edits[0].offset, offset);
-    expect(edits[0].length, length);
-    expect(edits[0].replacement, text);
-  }
-}
-
-@reflectiveTest
-class FileEditBuilderImplTest extends AbstractChangeBuilderTest {
-  String path = '/test.dart';
-
-  Future<void> test_addDeletion() async {
-    var offset = 23;
-    var length = 7;
-    await builder.addGenericFileEdit(path, (builder) {
-      builder.addDeletion(SourceRange(offset, length));
-    });
-    var edits = builder.sourceChange.edits[0].edits;
-    expect(edits, hasLength(1));
-    expect(edits[0].offset, offset);
-    expect(edits[0].length, length);
-    expect(edits[0].replacement, isEmpty);
-  }
-
   Future<void> test_addInsertion() async {
     await builder.addGenericFileEdit(path, (builder) {
       builder.addInsertion(10, (builder) {
@@ -634,6 +472,40 @@
     expect(edits[1].replacement, text);
   }
 
+  Future<void> test_addSimpleReplacement_overlapsHead() async {
+    var offset = 23;
+    var length = 7;
+    var text = 'xyz';
+    await builder.addGenericFileEdit(path, (builder) {
+      builder.addSimpleReplacement(SourceRange(offset, length), text);
+      expect(() {
+        builder.addSimpleReplacement(SourceRange(offset - 2, length), text);
+      }, throwsA(isA<ConflictingEditException>()));
+    });
+    var edits = builder.sourceChange.edits[0].edits;
+    expect(edits, hasLength(1));
+    expect(edits[0].offset, offset);
+    expect(edits[0].length, length);
+    expect(edits[0].replacement, text);
+  }
+
+  Future<void> test_addSimpleReplacement_overlapsTail() async {
+    var offset = 23;
+    var length = 7;
+    var text = 'xyz';
+    await builder.addGenericFileEdit(path, (builder) {
+      builder.addSimpleReplacement(SourceRange(offset, length), text);
+      expect(() {
+        builder.addSimpleReplacement(SourceRange(offset + 2, length), text);
+      }, throwsA(isA<ConflictingEditException>()));
+    });
+    var edits = builder.sourceChange.edits[0].edits;
+    expect(edits, hasLength(1));
+    expect(edits[0].offset, offset);
+    expect(edits[0].length, length);
+    expect(edits[0].replacement, text);
+  }
+
   Future<void> test_createEditBuilder() async {
     await builder.addGenericFileEdit(path, (builder) {
       var offset = 4;
diff --git a/pkg/compiler/lib/src/deferred_load/program_split_constraints/nodes.dart b/pkg/compiler/lib/src/deferred_load/program_split_constraints/nodes.dart
index dc1ec97..92068f6 100644
--- a/pkg/compiler/lib/src/deferred_load/program_split_constraints/nodes.dart
+++ b/pkg/compiler/lib/src/deferred_load/program_split_constraints/nodes.dart
@@ -214,6 +214,13 @@
     return node;
   }
 
+  NamedNode _lookupNamedNode(String nodeName) {
+    if (!namedNodes.containsKey(nodeName)) {
+      throw 'Missing reference node for $nodeName';
+    }
+    return namedNodes[nodeName];
+  }
+
   /// Returns a [ReferenceNode] referencing [importUriAndPrefix].
   /// [ReferenceNode]s are typically created in bulk, by mapping over a list of
   /// strings of imports in the form 'uri#prefix'. In further builder calls,
@@ -228,17 +235,15 @@
   /// Creates an unnamed [RelativeOrderNode] referencing two [NamedNode]s.
   RelativeOrderNode orderNode(String predecessor, String successor) {
     return RelativeOrderNode(
-        predecessor: namedNodes[predecessor], successor: namedNodes[successor]);
+        predecessor: _lookupNamedNode(predecessor),
+        successor: _lookupNamedNode(successor));
   }
 
   /// Creates a [CombinerNode] which can be referenced by [name] in further
   /// calls to the builder.
   CombinerNode combinerNode(String name, Set<String> nodes, CombinerType type) {
     ReferenceNode _lookup(String nodeName) {
-      if (!namedNodes.containsKey(nodeName)) {
-        throw 'Missing reference node for $nodeName';
-      }
-      var node = namedNodes[nodeName];
+      var node = _lookupNamedNode(nodeName);
       if (node is! ReferenceNode) {
         // TODO(joshualitt): Implement nested combiners.
         throw '$name references node $nodeName which is not a ReferenceNode.';
diff --git a/pkg/dartdev/lib/dartdev.dart b/pkg/dartdev/lib/dartdev.dart
index a545f1a..3e75181 100644
--- a/pkg/dartdev/lib/dartdev.dart
+++ b/pkg/dartdev/lib/dartdev.dart
@@ -21,6 +21,7 @@
 import 'src/commands/compile.dart';
 import 'src/commands/create.dart';
 import 'src/commands/debug_adapter.dart';
+import 'src/commands/doc.dart';
 import 'src/commands/fix.dart';
 import 'src/commands/language_server.dart';
 import 'src/commands/migrate.dart';
@@ -114,6 +115,7 @@
     addCommand(CreateCommand(verbose: verbose));
     addCommand(DebugAdapterCommand(verbose: verbose));
     addCommand(CompileCommand(verbose: verbose));
+    addCommand(DocCommand(verbose: verbose));
     addCommand(DevToolsCommand(
       verbose: verbose,
       customDevToolsPath: sdk.devToolsBinaries,
diff --git a/pkg/dartdev/lib/src/commands/doc.dart b/pkg/dartdev/lib/src/commands/doc.dart
new file mode 100644
index 0000000..26e6abc
--- /dev/null
+++ b/pkg/dartdev/lib/src/commands/doc.dart
@@ -0,0 +1,84 @@
+// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io' as io;
+
+import 'package:dartdoc/dartdoc.dart';
+import 'package:dartdoc/options.dart';
+import 'package:path/path.dart' as path;
+
+import '../core.dart';
+
+/// A command to create a new project from a set of templates.
+class DocCommand extends DartdevCommand {
+  static const String cmdName = 'doc';
+  String outputPath;
+
+  DocCommand({bool verbose = false})
+      : super(
+          cmdName,
+          'Generate HTML API documentation from Dart documentation comments.',
+          verbose,
+        ) {
+    outputPath = path.join('.', 'doc', 'api');
+    argParser.addOption(
+      'output-dir',
+      abbr: 'o',
+      defaultsTo: outputPath,
+      help: 'Output directory',
+    );
+    argParser.addFlag(
+      'validate-links',
+      negatable: true,
+      help: 'Display context aware warnings for broken links (slow)',
+    );
+  }
+
+  @override
+  String get invocation => '${super.invocation} <input directory>';
+
+  @override
+  FutureOr<int> run() async {
+    // At least one argument, the input directory, is required.
+    if (argResults.rest.isEmpty) {
+      usageException("Error: Input directory not specified");
+    }
+
+    // Determine input directory.
+    final dir = io.Directory(argResults.rest[0]);
+    if (!dir.existsSync()) {
+      usageException("Error: Input directory doesn't exist: ${dir.path}");
+    }
+
+    // Parse options.
+    final options = [
+      '--input=${dir.path}',
+      '--output=${argResults['output-dir']}',
+    ];
+    if (argResults['validate-links']) {
+      options.add('--validate-links');
+    } else {
+      options.add('--no-validate-links');
+    }
+    final config = await parseOptions(pubPackageMetaProvider, options);
+    if (config == null) {
+      // There was an error while parsing options.
+      return 2;
+    }
+
+    // Call DartDoc.
+    if (verbose) {
+      log.stdout('Calling DartDoc with the following options: $options');
+    }
+    final packageConfigProvider = PhysicalPackageConfigProvider();
+    final packageBuilder = PubPackageBuilder(
+        config, pubPackageMetaProvider, packageConfigProvider);
+    final dartdoc = config.generateDocs
+        ? await Dartdoc.fromContext(config, packageBuilder)
+        : await Dartdoc.withEmptyGenerator(config, packageBuilder);
+    dartdoc.executeGuarded();
+    return 0;
+  }
+}
diff --git a/pkg/dartdev/pubspec.yaml b/pkg/dartdev/pubspec.yaml
index 0145fab..aa4f922 100644
--- a/pkg/dartdev/pubspec.yaml
+++ b/pkg/dartdev/pubspec.yaml
@@ -17,6 +17,7 @@
   dart2native:
     path: ../dart2native
   dart_style: any
+  dartdoc: any
   dds:
     path: ../dds
   devtools_server: any
diff --git a/pkg/dartdev/test/commands/doc_test.dart b/pkg/dartdev/test/commands/doc_test.dart
new file mode 100644
index 0000000..84e5faf
--- /dev/null
+++ b/pkg/dartdev/test/commands/doc_test.dart
@@ -0,0 +1,70 @@
+// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test/test.dart';
+
+import '../utils.dart';
+
+const int compileErrorExitCode = 64;
+
+void main() {
+  group('doc', defineCompileTests, timeout: longTimeout);
+}
+
+void defineCompileTests() {
+  test('Passing no args fails', () {
+    final p = project();
+    var result = p.runSync(['doc']);
+    expect(result.stderr, contains('Input directory not specified'));
+    expect(result.exitCode, compileErrorExitCode);
+  });
+
+  test('--help', () {
+    final p = project();
+    final result = p.runSync(['doc', '--help']);
+    expect(
+      result.stdout,
+      contains('Usage: dart doc [arguments] <input directory>'),
+    );
+
+    expect(result.exitCode, 0);
+  });
+
+  test('Document a library', () {
+    final source = '''
+/// This is Foo. It uses [Bar].
+class Foo {
+    Bar bar;
+}
+
+/// Bar is very nice.
+class Bar {
+    _i = 42;
+}
+    ''';
+
+    final p = project(mainSrc: 'void main() { print("Hello, World"); }');
+    p.file('lib/foo.dart', source);
+    final result = p.runSync(['doc', '--validate-links', p.dirPath]);
+    print(
+        'exit: ${result.exitCode}, stderr:\n${result.stderr}\nstdout:\n${result.stdout}');
+    expect(result.stdout, contains('Documenting dartdev_temp'));
+  });
+
+  test('Document a library with broken link is flagged', () {
+    final source = '''
+/// This is Foo. It uses [Baz].
+class Foo {
+  //  Bar bar;
+}
+    ''';
+
+    final p = project(mainSrc: 'void main() { print("Hello, World"); }');
+    p.file('lib/foo.dart', source);
+    final result = p.runSync(['doc', '--validate-links', p.dirPath]);
+    print(
+        'exit: ${result.exitCode}, stderr:\n${result.stderr}\nstdout:\n${result.stdout}');
+    expect(result.stdout, contains('Documenting dartdev_temp'));
+  });
+}
diff --git a/runtime/vm/dart.cc b/runtime/vm/dart.cc
index ced30ca..00a1877 100644
--- a/runtime/vm/dart.cc
+++ b/runtime/vm/dart.cc
@@ -582,6 +582,14 @@
 
 // This waits until only the VM isolate remains in the list.
 void Dart::WaitForIsolateShutdown() {
+  int64_t start_time = 0;
+  if (FLAG_trace_shutdown) {
+    start_time = UptimeMillis();
+    OS::PrintErr("[+%" Pd64
+                 "ms] SHUTDOWN: Waiting for service "
+                 "and kernel isolates to shutdown\n",
+                 start_time);
+  }
   ASSERT(!Isolate::creation_enabled_);
   MonitorLocker ml(Isolate::isolate_creation_monitor_);
   intptr_t num_attempts = 0;
@@ -592,6 +600,25 @@
       if (num_attempts > 10) {
         DumpAliveIsolates(num_attempts, /*only_application_isolates=*/false);
       }
+      if (FLAG_trace_shutdown) {
+        OS::PrintErr("[+%" Pd64 "ms] SHUTDOWN: %" Pd
+                     " time out waiting for "
+                     "service and kernel isolates to shutdown\n",
+                     UptimeMillis(), num_attempts);
+      }
+    }
+  }
+  if (FLAG_trace_shutdown) {
+    int64_t stop_time = UptimeMillis();
+    OS::PrintErr("[+%" Pd64
+                 "ms] SHUTDOWN: Done waiting for service "
+                 "and kernel isolates to shutdown\n",
+                 stop_time);
+    if ((stop_time - start_time) > 500) {
+      OS::PrintErr("[+%" Pd64
+                   "ms] SHUTDOWN: waited too long for service "
+                   "and kernel isolates to shutdown\n",
+                   (stop_time - start_time));
     }
   }
 
@@ -642,6 +669,10 @@
                    UptimeMillis());
     }
     WaitForApplicationIsolateShutdown();
+    if (FLAG_trace_shutdown) {
+      OS::PrintErr("[+%" Pd64 "ms] SHUTDOWN: Done shutting down app isolates\n",
+                   UptimeMillis());
+    }
   }
 
   // Shutdown the kernel isolate.
@@ -658,12 +689,8 @@
   }
   ServiceIsolate::Shutdown();
 
-  // Wait for the remaining isolate (service isolate) to shutdown
+  // Wait for the remaining isolate (service/kernel isolate) to shutdown
   // before shutting down the thread pool.
-  if (FLAG_trace_shutdown) {
-    OS::PrintErr("[+%" Pd64 "ms] SHUTDOWN: Waiting for isolate shutdown\n",
-                 UptimeMillis());
-  }
   WaitForIsolateShutdown();
 
 #if !defined(PRODUCT)
@@ -696,6 +723,10 @@
   thread_pool_->Shutdown();
   delete thread_pool_;
   thread_pool_ = NULL;
+  if (FLAG_trace_shutdown) {
+    OS::PrintErr("[+%" Pd64 "ms] SHUTDOWN: Done deleting thread pool\n",
+                 UptimeMillis());
+  }
 
   Api::Cleanup();
   delete predefined_handles_;
diff --git a/runtime/vm/isolate.cc b/runtime/vm/isolate.cc
index c1e6cbe..c546dba 100644
--- a/runtime/vm/isolate.cc
+++ b/runtime/vm/isolate.cc
@@ -62,6 +62,7 @@
 
 DECLARE_FLAG(bool, print_metrics);
 DECLARE_FLAG(bool, trace_service);
+DECLARE_FLAG(bool, trace_shutdown);
 DECLARE_FLAG(bool, warn_on_pause_with_no_debugger);
 
 // Reload flags.
@@ -488,6 +489,13 @@
 }
 
 void IsolateGroup::Shutdown() {
+  char* name;
+
+  if (FLAG_trace_shutdown) {
+    name = Utils::StrDup(source()->name);
+    OS::PrintErr("[+%" Pd64 "ms] SHUTDOWN: Shutdown starting for group %s\n",
+                 Dart::UptimeMillis(), name);
+  }
   // Ensure to join all threads before waiting for pending GC tasks (the thread
   // pool can trigger idle notification, which can start new GC tasks).
   //
@@ -529,11 +537,28 @@
   // After this isolate group has died we might need to notify a pending
   // `Dart_Cleanup()` call.
   {
+    if (FLAG_trace_shutdown) {
+      OS::PrintErr("[+%" Pd64
+                   "ms] SHUTDOWN: Notifying "
+                   "isolate group shutdown (%s)\n",
+                   Dart::UptimeMillis(), name);
+    }
     MonitorLocker ml(Isolate::isolate_creation_monitor_);
     if (!Isolate::creation_enabled_ &&
         !IsolateGroup::HasApplicationIsolateGroups()) {
       ml.Notify();
     }
+    if (FLAG_trace_shutdown) {
+      OS::PrintErr("[+%" Pd64
+                   "ms] SHUTDOWN: Done Notifying "
+                   "isolate group shutdown (%s)\n",
+                   Dart::UptimeMillis(), name);
+    }
+  }
+  if (FLAG_trace_shutdown) {
+    OS::PrintErr("[+%" Pd64 "ms] SHUTDOWN: Done shutdown for group %s\n",
+                 Dart::UptimeMillis(), name);
+    free(name);
   }
 }
 
@@ -2635,6 +2660,10 @@
       // The current thread is running on the isolate group's thread pool.
       // So we cannot safely delete the isolate group (and it's pool).
       // Instead we will destroy the isolate group on the VM-global pool.
+      if (FLAG_trace_shutdown) {
+        OS::PrintErr("[+%" Pd64 "ms] : Scheduling shutdown on VM pool %s\n",
+                     Dart::UptimeMillis(), isolate_group->source()->name);
+      }
       Dart::thread_pool()->Run<ShutdownGroupTask>(isolate_group);
     }
   } else {
diff --git a/tools/VERSION b/tools/VERSION
index 786d33b..bd7b815 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 15
 PATCH 0
-PRERELEASE 297
+PRERELEASE 298
 PRERELEASE_PATCH 0
\ No newline at end of file