Version 2.13.0-211.6.beta

* Cherry-pick e8d53cff924214ea8dd20b36bb82f7d2dde7a131 to beta
* Cherry-pick ea9c6484f1412c09f19ce570eb1617017084a642 to beta
* Cherry-pick c8b3fb1e7163ba6000b89aa2bb4f30b39ae82acb to beta
* Cherry-pick ce5a1c2392debce967415d4c09359ff2555e3588 to beta
* Cherry-pick adc36a6563614a7c3ba29d9911e57fc68e7d9ac0 to beta
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3fe24ae..a0fb4f2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -82,8 +82,6 @@
 
 #### dart format
 
-*   Flatten indentation on nested chains of conditional (`?:`) operators.
-
 *   Correct constructor initializer indentation after `required` named
     parameters.
 
@@ -117,6 +115,12 @@
 
 [#44211]: https://github.com/dart-lang/sdk/issues/44211
 
+## 2.12.3 - 2021-04-12
+
+This is a patch release that fixes a vulnerability in `dart:html` related to
+DOM clobbering. Thanks again to **Vincenzo di Cicco** for finding and reporting
+this vulnerability.
+
 ## 2.12.2 - 2021-03-17
 
 This is a patch release that fixes crashes reported by Flutter 2 users (issue
diff --git a/DEPS b/DEPS
index 380ac87..cb652e2 100644
--- a/DEPS
+++ b/DEPS
@@ -98,7 +98,7 @@
   #     and land the review.
   #
   # For more details, see https://github.com/dart-lang/sdk/issues/30164
-  "dart_style_rev": "0067cfcc5bfa64cf59888c3fed34c71d1272555a",
+  "dart_style_rev": "f17c23e0eea9a870601c19d904e2a9c1a7c81470",
 
   "chromedriver_tag": "83.0.4103.39",
   "browser_launcher_rev": "12ab9f351a44ac803de9bc17bb2180bb312a9dd7",
diff --git a/pkg/analysis_server/lib/src/context_manager.dart b/pkg/analysis_server/lib/src/context_manager.dart
index 108e4c63..7fb15e0 100644
--- a/pkg/analysis_server/lib/src/context_manager.dart
+++ b/pkg/analysis_server/lib/src/context_manager.dart
@@ -254,6 +254,10 @@
 
   @override
   void setRoots(List<String> includedPaths, List<String> excludedPaths) {
+    if (_rootsAreUnchanged(includedPaths, excludedPaths)) {
+      return;
+    }
+
     this.includedPaths = includedPaths;
     this.excludedPaths = excludedPaths;
 
@@ -588,6 +592,21 @@
     return resourceProvider.getFile(path).readAsStringSync();
   }
 
+  /// Checks whether the current roots were built using the same paths as
+  /// [includedPaths]/[excludedPaths].
+  bool _rootsAreUnchanged(
+      List<String> includedPaths, List<String> excludedPaths) {
+    if (includedPaths.length != this.includedPaths.length ||
+        excludedPaths.length != this.excludedPaths.length) {
+      return false;
+    }
+    final existingIncludedSet = this.includedPaths.toSet();
+    final existingExcludedSet = this.excludedPaths.toSet();
+
+    return existingIncludedSet.containsAll(includedPaths) &&
+        existingExcludedSet.containsAll(excludedPaths);
+  }
+
   /// Listens to files generated by Bazel that were found or searched for.
   ///
   /// This is handled specially because the files are outside the package
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_change_workspace_folders.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_change_workspace_folders.dart
index 857fcb3..cf360d1 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_change_workspace_folders.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_change_workspace_folders.dart
@@ -40,7 +40,7 @@
         ?.map((wf) => Uri.parse(wf.uri).toFilePath())
         ?.toList();
 
-    server.updateAnalysisRoots(added, removed);
+    server.updateWorkspaceFolders(added, removed);
 
     return success();
   }
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_initialized.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_initialized.dart
index 462e933..98922d8 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_initialized.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_initialized.dart
@@ -33,7 +33,7 @@
     await server.fetchClientConfigurationAndPerformDynamicRegistration();
 
     if (!server.initializationOptions.onlyAnalyzeProjectsWithOpenFiles) {
-      server.updateAnalysisRoots(openWorkspacePaths, const []);
+      server.updateWorkspaceFolders(openWorkspacePaths, const []);
     }
 
     return success();
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_text_document_changes.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_text_document_changes.dart
index f703d18..c6addff 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_text_document_changes.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_text_document_changes.dart
@@ -11,28 +11,6 @@
 import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
 import 'package:analysis_server/src/lsp/mapping.dart';
 import 'package:analysis_server/src/lsp/source_edits.dart';
-import 'package:analyzer/file_system/file_system.dart';
-import 'package:analyzer/src/util/file_paths.dart' as file_paths;
-import 'package:path/path.dart' show dirname, join;
-
-/// Finds the nearest ancestor to [filePath] that contains a pubspec/.packages/build file.
-String _findProjectFolder(ResourceProvider resourceProvider, String filePath) {
-  // TODO(dantup): Is there something we can reuse for this?
-  var folder = dirname(filePath);
-  while (folder != dirname(folder)) {
-    final pubspec =
-        resourceProvider.getFile(join(folder, file_paths.pubspecYaml));
-    final packages =
-        resourceProvider.getFile(join(folder, file_paths.dotPackages));
-    final build = resourceProvider.getFile(join(folder, 'BUILD'));
-
-    if (pubspec.exists || packages.exists || build.exists) {
-      return folder;
-    }
-    folder = dirname(folder);
-  }
-  return null;
-}
 
 class TextDocumentChangeHandler
     extends MessageHandler<DidChangeTextDocumentParams, void> {
@@ -95,7 +73,6 @@
       server.removePriorityFile(path);
       server.documentVersions.remove(path);
       server.onOverlayDestroyed(path);
-      server.removeTemporaryAnalysisRoot(path);
 
       return success();
     });
@@ -127,22 +104,9 @@
       );
       server.onOverlayCreated(path, doc.text);
 
-      final driver = server.getAnalysisDriver(path);
       // If the file did not exist, and is "overlay only", it still should be
       // analyzed. Add it to driver to which it should have been added.
-
-      driver?.addFile(path);
-
-      // Figure out the best analysis root for this file and register it as a temporary
-      // analysis root. We need to register it even if we found a driver, so that if
-      // the driver existed only because of another open file, it will not be removed
-      // when that file is closed.
-      final analysisRoot = driver?.analysisContext?.contextRoot?.root?.path ??
-          _findProjectFolder(server.resourceProvider, path) ??
-          dirname(path);
-      if (analysisRoot != null) {
-        server.addTemporaryAnalysisRoot(path, analysisRoot);
-      }
+      server.contextManager.getDriverFor(path)?.addFile(path);
 
       server.addPriorityFile(path);
 
diff --git a/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart b/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
index afb577f..076172d 100644
--- a/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
+++ b/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
@@ -37,6 +37,7 @@
 import 'package:analysis_server/src/services/completion/completion_performance.dart'
     show CompletionPerformance;
 import 'package:analysis_server/src/services/refactoring/refactoring.dart';
+import 'package:analyzer/dart/analysis/context_locator.dart';
 import 'package:analyzer/error/error.dart';
 import 'package:analyzer/exception/exception.dart';
 import 'package:analyzer/file_system/file_system.dart';
@@ -50,6 +51,7 @@
 import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
 import 'package:analyzer_plugin/src/protocol/protocol_internal.dart' as plugin;
 import 'package:http/http.dart' as http;
+import 'package:meta/meta.dart';
 import 'package:watcher/watcher.dart';
 
 /// Instances of the class [LspAnalysisServer] implement an LSP-based server
@@ -104,29 +106,15 @@
 
   StreamSubscription _pluginChangeSubscription;
 
-  /// Temporary analysis roots for open files.
-  ///
-  /// When a file is opened and there is no driver available (for example no
-  /// folder was opened in the editor, so the set of analysis roots is empty)
-  /// we add temporary roots for the project (or containing) folder. When the
-  /// file is closed, it is removed from this map and if no other open file
-  /// uses that root, it will be removed from the set of analysis roots.
-  ///
-  /// key: file path of the open file
-  /// value: folder to be used as a root.
-  final _temporaryAnalysisRoots = <String, String>{};
-
-  /// The set of analysis roots explicitly added to the workspace.
-  final _explicitAnalysisRoots = <String>{};
+  /// The current workspace folders provided by the client. Used as analysis roots.
+  final _workspaceFolders = <String>{};
 
   /// A progress reporter for analysis status.
   ProgressReporter analyzingProgressReporter;
 
-  /// The last paths that were set as included analysis roots.
-  Set<String> _lastIncludedRootPaths;
-
-  /// The last paths that were set as excluded analysis roots.
-  Set<String> _lastExcludedRootPaths;
+  /// The number of times contexts have been created/recreated.
+  @visibleForTesting
+  int contextBuilds = 0;
 
   /// Initialize a newly created server to send and receive messages to the
   /// given [channel].
@@ -204,15 +192,10 @@
     assert(didAdd);
     if (didAdd) {
       _updateDriversAndPluginsPriorityFiles();
+      _refreshAnalysisRoots();
     }
   }
 
-  /// Adds a temporary analysis root for an open file.
-  void addTemporaryAnalysisRoot(String filePath, String folderPath) {
-    _temporaryAnalysisRoots[filePath] = folderPath;
-    _refreshAnalysisRoots();
-  }
-
   /// The socket from which messages are being read has been closed.
   void done() {}
 
@@ -479,15 +462,10 @@
     assert(didRemove);
     if (didRemove) {
       _updateDriversAndPluginsPriorityFiles();
+      _refreshAnalysisRoots();
     }
   }
 
-  /// Removes any temporary analysis root for a file that was closed.
-  void removeTemporaryAnalysisRoot(String filePath) {
-    _temporaryAnalysisRoots.remove(filePath);
-    _refreshAnalysisRoots();
-  }
-
   void sendErrorResponse(Message message, ResponseError error) {
     if (message is RequestMessage) {
       channel.sendResponse(ResponseMessage(
@@ -651,10 +629,11 @@
     sendServerErrorNotification('Socket error', error, stack);
   }
 
-  void updateAnalysisRoots(List<String> addedPaths, List<String> removedPaths) {
+  void updateWorkspaceFolders(
+      List<String> addedPaths, List<String> removedPaths) {
     // TODO(dantup): This is currently case-sensitive!
 
-    _explicitAnalysisRoots
+    _workspaceFolders
       ..addAll(addedPaths ?? const [])
       ..removeAll(removedPaths ?? const []);
 
@@ -671,14 +650,40 @@
     notifyFlutterWidgetDescriptions(path);
   }
 
+  /// Computes analysis roots for a set of open files.
+  ///
+  /// This is used when there are no workspace folders open directly.
+  List<String> _getRootsForOpenFiles() {
+    final openFiles = priorityFiles.toList();
+    final contextLocator = ContextLocator(resourceProvider: resourceProvider);
+    final roots = contextLocator.locateRoots(includedPaths: openFiles);
+
+    // For files in folders that don't have pubspecs, a root would be
+    // produced for the root of the drive which we do not want, so filter those out.
+    roots.removeWhere((root) => root.root.isRoot);
+
+    // Find any files that are no longer covered by roots because of the above
+    // removal.
+    final additionalFiles =
+        openFiles.where((file) => !roots.any((root) => root.isAnalyzed(file)));
+
+    return [
+      ...roots.map((root) => root.root.path),
+      ...additionalFiles,
+    ];
+  }
+
   void _onPluginsChanged() {
     capabilitiesComputer.performDynamicRegistration();
   }
 
   void _refreshAnalysisRoots() {
-    // Always include any temporary analysis roots for open files.
-    final includedPaths = _explicitAnalysisRoots.toSet()
-      ..addAll(_temporaryAnalysisRoots.values);
+    // When there are open folders, they are always the roots. If there are no
+    // open workspace folders, then we use the open (priority) files to compute
+    // roots.
+    final includedPaths = _workspaceFolders.isNotEmpty
+        ? _workspaceFolders.toSet()
+        : _getRootsForOpenFiles();
 
     final excludedPaths = clientConfiguration.analysisExcludedFolders
         .expand((excludePath) => resourceProvider.pathContext
@@ -688,25 +693,10 @@
             // TODO(dantup): Consider supporting per-workspace config by
             // calling workspace/configuration whenever workspace folders change
             // and caching the config for each one.
-            : _explicitAnalysisRoots.map(
+            : _workspaceFolders.map(
                 (root) => resourceProvider.pathContext.join(root, excludePath)))
         .toSet();
 
-    // If the roots didn't actually change from the last time they were set
-    // (this can happen a lot as temporary roots are collected for open files)
-    // we can avoid doing expensive work like discarding/re-scanning the
-    // declarations.
-    final rootsChanged =
-        includedPaths.length != _lastIncludedRootPaths?.length ||
-            !includedPaths.every(_lastIncludedRootPaths.contains) ||
-            excludedPaths.length != _lastExcludedRootPaths?.length ||
-            !excludedPaths.every(_lastExcludedRootPaths.contains);
-
-    if (!rootsChanged) return;
-
-    _lastIncludedRootPaths = includedPaths;
-    _lastExcludedRootPaths = excludedPaths;
-
     notificationManager.setAnalysisRoots(
         includedPaths.toList(), excludedPaths.toList());
     contextManager.setRoots(includedPaths.toList(), excludedPaths.toList());
@@ -780,6 +770,7 @@
 
   @override
   void afterContextsCreated() {
+    analysisServer.contextBuilds++;
     analysisServer.addContextsToDeclarationsTracker();
   }
 
diff --git a/pkg/analysis_server/test/lsp/change_workspace_folders_test.dart b/pkg/analysis_server/test/lsp/change_workspace_folders_test.dart
index 9bec4ca..6d7431c 100644
--- a/pkg/analysis_server/test/lsp/change_workspace_folders_test.dart
+++ b/pkg/analysis_server/test/lsp/change_workspace_folders_test.dart
@@ -26,9 +26,17 @@
     workspaceFolder1Path = convertPath('/workspace1');
     workspaceFolder2Path = convertPath('/workspace2');
     workspaceFolder3Path = convertPath('/workspace3');
+    newFolder(workspaceFolder1Path);
+    newFolder(workspaceFolder2Path);
+    newFolder(workspaceFolder3Path);
+
     workspaceFolder1Uri = Uri.file(workspaceFolder1Path);
     workspaceFolder2Uri = Uri.file(workspaceFolder2Path);
     workspaceFolder3Uri = Uri.file(workspaceFolder3Path);
+
+    newFile(join(workspaceFolder1Path, 'pubspec.yaml'));
+    newFile(join(workspaceFolder2Path, 'pubspec.yaml'));
+    newFile(join(workspaceFolder3Path, 'pubspec.yaml'));
   }
 
   Future<void> test_changeWorkspaceFolders_add() async {
@@ -75,23 +83,27 @@
     // Expect implicit root for the open file.
     expect(
       server.contextManager.includedPaths,
-      unorderedEquals([nestedFolderPath]),
+      unorderedEquals([workspaceFolder1Path]),
     );
 
-    // Add the real project root to the workspace (which should become an
-    // explicit root).
+    // Add the real project root to the workspace (which will become an
+    // explicit root but not change anything or rebuild contexts).
+    resetContextBuildCounter();
     await changeWorkspaceFolders(add: [workspaceFolder1Uri]);
     expect(
       server.contextManager.includedPaths,
-      unorderedEquals([workspaceFolder1Path, nestedFolderPath]),
+      unorderedEquals([workspaceFolder1Path]),
     );
+    expectNoContextBuilds();
 
-    // Closing the file should not result in the project being removed.
+    // Closing the file should not change roots nor trigger a rebuild.
+    resetContextBuildCounter();
     await closeFile(nestedFileUri);
     expect(
       server.contextManager.includedPaths,
       unorderedEquals([workspaceFolder1Path]),
     );
+    expectNoContextBuilds();
   }
 
   Future<void>
@@ -108,24 +120,28 @@
     // Expect implicit root for the open file.
     expect(
       server.contextManager.includedPaths,
-      unorderedEquals([nestedFolderPath]),
+      unorderedEquals([workspaceFolder1Path]),
     );
 
-    // Add the real project root to the workspace (which should become an
-    // explicit root).
+    // Add the real project root to the workspace (which will become an
+    // explicit root but not change anything or rebuild contexts).
+    resetContextBuildCounter();
     await changeWorkspaceFolders(add: [workspaceFolder1Uri]);
     expect(
       server.contextManager.includedPaths,
-      unorderedEquals([workspaceFolder1Path, nestedFolderPath]),
+      unorderedEquals([workspaceFolder1Path]),
     );
+    expectNoContextBuilds();
 
-    // Removing the workspace folder should result in falling back to just the
-    // nested folder.
-    await changeWorkspaceFolders(remove: [workspaceFolder1Uri]);
+    // Removing the workspace folder should not change roots nor trigger a
+    // rebuild because the root is still the implicit root for the open file.
+    resetContextBuildCounter();
+    await closeFile(nestedFileUri);
     expect(
       server.contextManager.includedPaths,
-      unorderedEquals([nestedFolderPath]),
+      unorderedEquals([workspaceFolder1Path]),
     );
+    expectNoContextBuilds();
   }
 
   Future<void>
@@ -144,20 +160,23 @@
       unorderedEquals([workspaceFolder1Path]),
     );
 
-    // Open a file, though no new root is expected as it was mapped to the existing
-    // open folder.
+    // An open file should not trigger any changes or rebuilds.
+    resetContextBuildCounter();
     await openFile(nestedFileUri, '');
     expect(
       server.contextManager.includedPaths,
       unorderedEquals([workspaceFolder1Path]),
     );
+    expectNoContextBuilds();
 
-    // Closing the file should not result in the project being removed.
+    // Closing the file should also not trigger any changes.
+    resetContextBuildCounter();
     await closeFile(nestedFileUri);
     expect(
       server.contextManager.includedPaths,
       unorderedEquals([workspaceFolder1Path]),
     );
+    expectNoContextBuilds();
   }
 
   Future<void>
@@ -176,32 +195,61 @@
       unorderedEquals([workspaceFolder1Path]),
     );
 
-    // Open a file, though no new root is expected as it was mapped to the existing
-    // open folder.
+    // Open a file, though no new root (or rebuild) is expected as it was mapped
+    // to the existing open project folder.
+    resetContextBuildCounter();
     await openFile(nestedFileUri, '');
     expect(
       server.contextManager.includedPaths,
       unorderedEquals([workspaceFolder1Path]),
     );
+    expectNoContextBuilds();
 
     // Removing the workspace folder will retain the workspace folder, as that's
-    // the folder we picked when the file was opened since there was already
-    // a root for it.
+    // the project root.
+    resetContextBuildCounter();
     await changeWorkspaceFolders(remove: [workspaceFolder1Uri]);
     expect(
       server.contextManager.includedPaths,
       unorderedEquals([workspaceFolder1Path]),
     );
+    expectNoContextBuilds();
+  }
+
+  Future<void> test_changeWorkspaceFolders_implicitFile_noProject() async {
+    final nestedFolderPath =
+        join(workspaceFolder1Path, 'nested', 'deeply', 'in', 'folders');
+    final nestedFilePath = join(nestedFolderPath, 'test.dart');
+    final nestedFileUri = Uri.file(nestedFilePath);
+    newFile(nestedFilePath);
+    deleteFile(join(
+        workspaceFolder1Path, 'pubspec.yaml')); // Ensure no pubspecs in tree.
+
+    await initialize(allowEmptyRootUri: true);
+    await openFile(nestedFileUri, '');
+
+    // Because there is no pubspec in the tree and we don't locate a root, we
+    // expect the file to be analyzed solo.
+    expect(
+      server.contextManager.includedPaths,
+      unorderedEquals([nestedFilePath]),
+    );
+
+    // Adding the parent folder will switch to using that as the root and rebuild
+    // the root.
+    resetContextBuildCounter();
+    await changeWorkspaceFolders(add: [workspaceFolder1Uri]);
+    expect(
+      server.contextManager.includedPaths,
+      unorderedEquals([workspaceFolder1Path]),
+    );
+    expectContextBuilds();
   }
 
   Future<void> test_changeWorkspaceFolders_openFileOutsideRoot() async {
     // When a file is opened that is outside of the analysis roots, the first
-    // analysis driver will be used (see [AbstractAnalysisServer.getAnalysisDriver]).
-    // This means as long as there is already an analysis root, the implicit root
-    // will be the original root and not the path of the opened file.
-    // For example, Go-to-Definition into a file in PubCache must *not* result in
-    // the pub cache folder being added as an analysis root, it should be analyzed
-    // by the existing project's driver.
+    // analysis driver will be used (see [AbstractAnalysisServer.getAnalysisDriver])
+    // and no new root will be created.
     final workspace1FilePath = join(workspaceFolder1Path, 'test.dart');
     newFile(workspace1FilePath);
     final workspace2FilePath = join(workspaceFolder2Path, 'test.dart');
@@ -216,19 +264,24 @@
       unorderedEquals([workspaceFolder1Path]),
     );
 
-    // Open a file in workspaceFolder2 (which is not in the analysis roots).
+    // Open a file in workspaceFolder2 which will reuse the existing driver for
+    // workspace1 so not change roots/trigger a rebuild.
+    resetContextBuildCounter();
     await openFile(workspace2FileUri, '');
     expect(
       server.contextManager.includedPaths,
       unorderedEquals([workspaceFolder1Path]),
     );
+    expectNoContextBuilds();
 
-    // Closing the file should not result in the project being removed.
+    // Closing the file will also not trigger any changes.
+    resetContextBuildCounter();
     await closeFile(workspace2FileUri);
     expect(
       server.contextManager.includedPaths,
       unorderedEquals([workspaceFolder1Path]),
     );
+    expectNoContextBuilds();
   }
 
   Future<void> test_changeWorkspaceFolders_remove() async {
diff --git a/pkg/analysis_server/test/lsp/configuration_test.dart b/pkg/analysis_server/test/lsp/configuration_test.dart
index 980af42..bf57ef4 100644
--- a/pkg/analysis_server/test/lsp/configuration_test.dart
+++ b/pkg/analysis_server/test/lsp/configuration_test.dart
@@ -58,7 +58,7 @@
       {}, // Empty config
     );
 
-    // Ensure the roots are as expected before we udpate the config.
+    // Ensure the roots are as expected before we update the config.
     expect(server.contextManager.includedPaths, equals([projectFolderPath]));
     expect(server.contextManager.excludedPaths, isEmpty);
 
diff --git a/pkg/analysis_server/test/lsp/diagnostic_test.dart b/pkg/analysis_server/test/lsp/diagnostic_test.dart
index 46ace8a..a380a07 100644
--- a/pkg/analysis_server/test/lsp/diagnostic_test.dart
+++ b/pkg/analysis_server/test/lsp/diagnostic_test.dart
@@ -285,6 +285,28 @@
     expect(diagnostic.range.end.character, equals(12));
   }
 
+  Future<void> test_looseFile_withoutPubpsec() async {
+    await initialize(allowEmptyRootUri: true);
+
+    // Opening the file should trigger diagnostics.
+    {
+      final diagnosticsUpdate = waitForDiagnostics(mainFileUri);
+      await openFile(mainFileUri, 'final a = Bad();');
+      final diagnostics = await diagnosticsUpdate;
+      expect(diagnostics, hasLength(1));
+      final diagnostic = diagnostics.first;
+      expect(diagnostic.message, contains("The function 'Bad' isn't defined"));
+    }
+
+    // Closing the file should remove the diagnostics.
+    {
+      final diagnosticsUpdate = waitForDiagnostics(mainFileUri);
+      await closeFile(mainFileUri);
+      final diagnostics = await diagnosticsUpdate;
+      expect(diagnostics, hasLength(0));
+    }
+  }
+
   Future<void> test_todos() async {
     // TODOs only show up if there's also some code in the file.
     const contents = '''
diff --git a/pkg/analysis_server/test/lsp/document_changes_test.dart b/pkg/analysis_server/test/lsp/document_changes_test.dart
index 0c8f181..361ee23 100644
--- a/pkg/analysis_server/test/lsp/document_changes_test.dart
+++ b/pkg/analysis_server/test/lsp/document_changes_test.dart
@@ -102,6 +102,25 @@
         equals({mainFilePath: RemoveContentOverlay()}));
   }
 
+  Future<void>
+      test_documentOpen_addsOverlayOnlyToDriver_onlyIfInsideRoots() async {
+    // Ensures that opening a file doesn't add it to the driver if it's outside
+    // of the drivers root.
+    final fileInsideRootPath = mainFilePath;
+    final fileOutsideRootPath = convertPath('/home/unrelated/main.dart');
+    await initialize();
+    await openFile(Uri.file(fileInsideRootPath), content);
+    await openFile(Uri.file(fileOutsideRootPath), content);
+
+    // Expect both files return the same driver
+    final driverForInside = server.getAnalysisDriver(fileInsideRootPath);
+    final driverForOutside = server.getAnalysisDriver(fileOutsideRootPath);
+    expect(driverForInside, equals(driverForOutside));
+    // But that only the file inside the root was added.
+    expect(driverForInside.addedFiles, contains(fileInsideRootPath));
+    expect(driverForInside.addedFiles, isNot(contains(fileOutsideRootPath)));
+  }
+
   Future<void> test_documentOpen_createsOverlay() async {
     await _initializeAndOpen();
 
diff --git a/pkg/analysis_server/test/lsp/initialization_test.dart b/pkg/analysis_server/test/lsp/initialization_test.dart
index 032df8d..fc6b1fc 100644
--- a/pkg/analysis_server/test/lsp/initialization_test.dart
+++ b/pkg/analysis_server/test/lsp/initialization_test.dart
@@ -10,7 +10,6 @@
 import 'package:analysis_server/src/lsp/json_parsing.dart';
 import 'package:analysis_server/src/lsp/server_capabilities_computer.dart';
 import 'package:analysis_server/src/plugin/plugin_manager.dart';
-import 'package:path/path.dart' as path;
 import 'package:test/test.dart';
 import 'package:test_reflective_loader/test_reflective_loader.dart';
 
@@ -299,15 +298,22 @@
 
     // Closing only one of the files should not remove the project folder
     // since there are still open files.
+    resetContextBuildCounter();
     await closeFile(file1Uri);
     expect(server.contextManager.includedPaths, equals([projectFolderPath]));
+    expectNoContextBuilds();
 
-    // Closing the last file should remove the project folder.
+    // Closing the last file should remove the project folder and remove
+    // the context.
+    resetContextBuildCounter();
     await closeFile(file2Uri);
     expect(server.contextManager.includedPaths, equals([]));
+    expect(server.contextManager.driverMap, hasLength(0));
+    expectContextBuilds();
   }
 
   Future<void> test_emptyAnalysisRoots_projectWithoutPubspec() async {
+    projectFolderPath = convertPath('/home/empty');
     final nestedFilePath = join(
         projectFolderPath, 'nested', 'deeply', 'in', 'folders', 'test.dart');
     final nestedFileUri = Uri.file(nestedFilePath);
@@ -317,10 +323,9 @@
     await initialize(allowEmptyRootUri: true);
     expect(server.contextManager.includedPaths, equals([]));
 
-    // Opening the file should trigger the immediate parent folder to be added.
+    // Opening the file will add a root for it.
     await openFile(nestedFileUri, '');
-    expect(server.contextManager.includedPaths,
-        equals([path.dirname(nestedFilePath)]));
+    expect(server.contextManager.includedPaths, equals([nestedFilePath]));
   }
 
   Future<void> test_emptyAnalysisRoots_projectWithPubspec() async {
@@ -510,18 +515,26 @@
     await openFile(file1Uri, '');
     await openFile(file2Uri, '');
     expect(server.contextManager.includedPaths, equals([projectFolderPath]));
+    expect(server.contextManager.driverMap, hasLength(1));
 
-    // Closing only one of the files should not remove the project folder
-    // since there are still open files.
+    // Closing only one of the files should not remove the root or rebuild the context.
+    resetContextBuildCounter();
     await closeFile(file1Uri);
     expect(server.contextManager.includedPaths, equals([projectFolderPath]));
+    expect(server.contextManager.driverMap, hasLength(1));
+    expectNoContextBuilds();
 
-    // Closing the last file should remove the project folder.
+    // Closing the last file should remove the project folder and remove
+    // the context.
+    resetContextBuildCounter();
     await closeFile(file2Uri);
     expect(server.contextManager.includedPaths, equals([]));
+    expect(server.contextManager.driverMap, hasLength(0));
+    expectContextBuilds();
   }
 
   Future<void> test_onlyAnalyzeProjectsWithOpenFiles_withoutPubpsec() async {
+    projectFolderPath = convertPath('/home/empty');
     final nestedFilePath = join(
         projectFolderPath, 'nested', 'deeply', 'in', 'folders', 'test.dart');
     final nestedFileUri = Uri.file(nestedFilePath);
@@ -534,10 +547,9 @@
     );
     expect(server.contextManager.includedPaths, equals([]));
 
-    // Opening the file should trigger the immediate parent folder to be added.
+    // Opening the file should trigger it to be added.
     await openFile(nestedFileUri, '');
-    expect(server.contextManager.includedPaths,
-        equals([path.dirname(nestedFilePath)]));
+    expect(server.contextManager.includedPaths, equals([nestedFilePath]));
   }
 
   Future<void> test_onlyAnalyzeProjectsWithOpenFiles_withPubpsec() async {
@@ -555,7 +567,8 @@
     );
     expect(server.contextManager.includedPaths, equals([]));
 
-    // Opening a file nested within the project should add the project folder.
+    // Opening a file nested within the project should cause the project folder
+    // to be added
     await openFile(nestedFileUri, '');
     expect(server.contextManager.includedPaths, equals([projectFolderPath]));
 
diff --git a/pkg/analysis_server/test/lsp/server_abstract.dart b/pkg/analysis_server/test/lsp/server_abstract.dart
index 663687b..a2477d5 100644
--- a/pkg/analysis_server/test/lsp/server_abstract.dart
+++ b/pkg/analysis_server/test/lsp/server_abstract.dart
@@ -55,6 +55,10 @@
   LspAnalysisServer server;
   MockHttpClient httpClient;
 
+  /// The number of context builds that had already occurred the last time
+  /// resetContextBuildCounter() was called.
+  int _previousContextBuilds = 0;
+
   AnalysisServerOptions get serverOptions => AnalysisServerOptions();
 
   @override
@@ -83,6 +87,14 @@
     return info;
   }
 
+  void expectContextBuilds() =>
+      expect(server.contextBuilds - _previousContextBuilds, greaterThan(0),
+          reason: 'Contexts should have been rebuilt');
+
+  void expectNoContextBuilds() =>
+      expect(server.contextBuilds - _previousContextBuilds, equals(0),
+          reason: 'Contexts should not have been rebuilt');
+
   /// Sends a request to the server and unwraps the result. Throws if the
   /// response was not successful or returned an error.
   @override
@@ -105,6 +117,10 @@
         orElse: () => null);
   }
 
+  void resetContextBuildCounter() {
+    _previousContextBuilds = server.contextBuilds;
+  }
+
   @override
   Future sendNotificationToServer(NotificationMessage notification) async {
     channel.sendNotificationToServer(notification);
diff --git a/pkg/analysis_server/tool/lsp_spec/README.md b/pkg/analysis_server/tool/lsp_spec/README.md
index 2f679b1..e70af29 100644
--- a/pkg/analysis_server/tool/lsp_spec/README.md
+++ b/pkg/analysis_server/tool/lsp_spec/README.md
@@ -17,9 +17,13 @@
 
 Note: In LSP the client makes the first request so there is no obvious confirmation that the server is working correctly until the client sends an `initialize` request. Unlike standard JSON RPC, [LSP requires that headers are sent](https://microsoft.github.io/language-server-protocol/specification).
 
+## Handling of "Loose" Files
+
+When there are no open workspace folders (or if the initialization option `onlyAnalyzeProjectsWithOpenFiles` is set to `true`), analysis will be performed based on project folders located by the open files. For each open file, the project root will be located, and that whole project analyzed. If the file does not have a project (eg. there is no pubspec.yaml in its ancestor folders) then the file will be analyzed in isolation.
+
 ## Initialization Options
 
-- `onlyAnalyzeProjectsWithOpenFiles`: When set to `true`, analysis will only be performed for projects that have open files rather than the root workspace folder. Defaults to `false`.
+- `onlyAnalyzeProjectsWithOpenFiles`: When set to `true`, workspace folders will be ignored and analysis will be performed based on the open files, as if no workspace was open at all. This allows opening large folders without causing them to be completely analyzed. Defaults to `false`.
 - `suggestFromUnimportedLibraries`: When set to `false`, completion will not include synbols that are not already imported into the current file. Defaults to `true`, though the client must additionally support `workspace/applyEdit` for these completions to be included.
 - `closingLabels`: When set to `true`, `dart/textDocument/publishClosingLabels` notifications will be sent with information to render editor closing labels.
 - `outline`: When set to `true`, `dart/textDocument/publishOutline` notifications will be sent with outline information for open files.
diff --git a/sdk/lib/html/dart2js/html_dart2js.dart b/sdk/lib/html/dart2js/html_dart2js.dart
index bc7bdfb..c60be00 100644
--- a/sdk/lib/html/dart2js/html_dart2js.dart
+++ b/sdk/lib/html/dart2js/html_dart2js.dart
@@ -40994,8 +40994,8 @@
 class _ValidatingTreeSanitizer implements NodeTreeSanitizer {
   NodeValidator validator;
 
-  /// Did we modify the tree by removing anything.
-  bool modifiedTree = false;
+  /// Number of tree modifications this instance has made.
+  int numTreeModifications = 0;
   _ValidatingTreeSanitizer(this.validator) {}
 
   void sanitizeTree(Node node) {
@@ -41026,12 +41026,12 @@
       }
     }
 
-    modifiedTree = false;
-    walk(node, null);
-    while (modifiedTree) {
-      modifiedTree = false;
+    // Walk the tree until no new modifications are added to the tree.
+    var previousTreeModifications;
+    do {
+      previousTreeModifications = numTreeModifications;
       walk(node, null);
-    }
+    } while (previousTreeModifications != numTreeModifications);
   }
 
   /// Aggressively try to remove node.
@@ -41039,7 +41039,7 @@
     // If we have the parent, it's presumably already passed more sanitization
     // or is the fragment, so ask it to remove the child. And if that fails
     // try to set the outer html.
-    modifiedTree = true;
+    numTreeModifications++;
     if (parent == null || parent != node.parentNode) {
       node.remove();
     } else {
diff --git a/tests/lib/html/node_validator_important_if_you_suppress_make_the_bug_critical_test.dart b/tests/lib/html/node_validator_important_if_you_suppress_make_the_bug_critical_test.dart
index 5a3d0f2..2d7205c 100644
--- a/tests/lib/html/node_validator_important_if_you_suppress_make_the_bug_critical_test.dart
+++ b/tests/lib/html/node_validator_important_if_you_suppress_make_the_bug_critical_test.dart
@@ -453,6 +453,20 @@
             "<input id='bad' onmouseover='alert(1)'>",
         "");
 
+    // Walking templates triggers a recursive sanitization call, which shouldn't
+    // invalidate the information collected from the previous visit of the later
+    // nodes in the walk.
+    testHtml(
+        'DOM clobbering with recursive sanitize call using templates',
+        validator,
+        "<form><div>"
+            "<input id=childNodes />"
+            "<template></template>"
+            "<input id=childNodes name=lastChild />"
+            "<img id=exploitImg src=0 onerror='alert(1)' />"
+            "</div></form>",
+        "");
+
     test('tagName makes containing form invalid', () {
       var fragment = document.body!.createFragment(
           "<form onmouseover='alert(2)'><input name='tagName'>",
diff --git a/tests/lib_2/html/node_validator_important_if_you_suppress_make_the_bug_critical_test.dart b/tests/lib_2/html/node_validator_important_if_you_suppress_make_the_bug_critical_test.dart
index 1b3ea60..ee31868 100644
--- a/tests/lib_2/html/node_validator_important_if_you_suppress_make_the_bug_critical_test.dart
+++ b/tests/lib_2/html/node_validator_important_if_you_suppress_make_the_bug_critical_test.dart
@@ -478,6 +478,20 @@
         "<input id='bad' onmouseover='alert(1)'>",
         "");
 
+    // Walking templates triggers a recursive sanitization call, which shouldn't
+    // invalidate the information collected from the previous visit of the later
+    // nodes in the walk.
+    testHtml(
+        'DOM clobbering with recursive sanitize call using templates',
+        validator,
+        "<form><div>"
+          "<input id=childNodes />"
+          "<template></template>"
+          "<input id=childNodes name=lastChild />"
+          "<img id=exploitImg src=0 onerror='alert(1)' />"
+          "</div></form>",
+        "");
+
     test('tagName makes containing form invalid', () {
       var fragment = document.body.createFragment(
           "<form onmouseover='alert(2)'><input name='tagName'>",
diff --git a/tools/VERSION b/tools/VERSION
index fa9f98f..8618025 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -28,4 +28,4 @@
 MINOR 13
 PATCH 0
 PRERELEASE 211
-PRERELEASE_PATCH 1
\ No newline at end of file
+PRERELEASE_PATCH 6
\ No newline at end of file
diff --git a/tools/dom/src/Validators.dart b/tools/dom/src/Validators.dart
index 0c45b8c..1fbafbd 100644
--- a/tools/dom/src/Validators.dart
+++ b/tools/dom/src/Validators.dart
@@ -158,8 +158,8 @@
 class _ValidatingTreeSanitizer implements NodeTreeSanitizer {
   NodeValidator validator;
 
-  /// Did we modify the tree by removing anything.
-  bool modifiedTree = false;
+  /// Number of tree modifications this instance has made.
+  int numTreeModifications = 0;
   _ValidatingTreeSanitizer(this.validator) {}
 
   void sanitizeTree(Node node) {
@@ -190,12 +190,12 @@
       }
     }
 
-    modifiedTree = false;
-    walk(node, null);
-    while (modifiedTree) {
-      modifiedTree = false;
+    // Walk the tree until no new modifications are added to the tree.
+    var previousTreeModifications;
+    do {
+      previousTreeModifications = numTreeModifications;
       walk(node, null);
-    }
+    } while (previousTreeModifications != numTreeModifications);
   }
 
   /// Aggressively try to remove node.
@@ -203,7 +203,7 @@
     // If we have the parent, it's presumably already passed more sanitization
     // or is the fragment, so ask it to remove the child. And if that fails
     // try to set the outer html.
-    modifiedTree = true;
+    numTreeModifications++;
     if (parent == null || parent != node.parentNode) {
       node.remove();
     } else {