Fix handling of circular links in ContextRootImpl and ContextLocatorImpl.

Change-Id: Ib1de5a9f6bbf13e72024ddae6c83d3a73dcf24a5
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/186980
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
diff --git a/pkg/analyzer/lib/src/dart/analysis/context_locator.dart b/pkg/analyzer/lib/src/dart/analysis/context_locator.dart
index 7198e6b..f4749c5 100644
--- a/pkg/analyzer/lib/src/dart/analysis/context_locator.dart
+++ b/pkg/analyzer/lib/src/dart/analysis/context_locator.dart
@@ -101,7 +101,7 @@
       root.included.add(folder);
       root.excludedGlobs = _getExcludedGlobs(root);
       roots.add(root);
-      _createContextRootsIn(roots, folder, excludedFolders, root,
+      _createContextRootsIn(roots, {}, folder, excludedFolders, root,
           root.excludedGlobs, defaultOptionsFile, defaultPackagesFile);
     }
     Map<Folder, ContextRoot> rootMap = <Folder, ContextRoot>{};
@@ -140,6 +140,7 @@
   /// search for nested context roots.
   void _createContextRoots(
       List<ContextRoot> roots,
+      Set<String> visited,
       Folder folder,
       List<Folder> excludedFolders,
       ContextRoot containingRoot,
@@ -181,8 +182,8 @@
       excludedGlobs = _getExcludedGlobs(root);
       root.excludedGlobs = excludedGlobs;
     }
-    _createContextRootsIn(roots, folder, excludedFolders, containingRoot,
-        excludedGlobs, optionsFile, packagesFile);
+    _createContextRootsIn(roots, visited, folder, excludedFolders,
+        containingRoot, excludedGlobs, optionsFile, packagesFile);
   }
 
   /// For each directory within the given [folder] that is neither in the list
@@ -193,6 +194,7 @@
   /// file will be used even if there is a local version of the file.
   void _createContextRootsIn(
       List<ContextRoot> roots,
+      Set<String> visited,
       Folder folder,
       List<Folder> excludedFolders,
       ContextRoot containingRoot,
@@ -212,6 +214,12 @@
       return false;
     }
 
+    // Stop infinite recursion via links.
+    var canonicalFolderPath = folder.resolveSymbolicLinksSync().path;
+    if (!visited.add(canonicalFolderPath)) {
+      return;
+    }
+
     //
     // Check each of the subdirectories to see whether a context root needs to
     // be added for it.
@@ -222,8 +230,8 @@
           if (isExcluded(child)) {
             containingRoot.excluded.add(child);
           } else {
-            _createContextRoots(roots, child, excludedFolders, containingRoot,
-                excludedGlobs, optionsFile, packagesFile);
+            _createContextRoots(roots, visited, child, excludedFolders,
+                containingRoot, excludedGlobs, optionsFile, packagesFile);
           }
         }
       }
diff --git a/pkg/analyzer/lib/src/dart/analysis/context_root.dart b/pkg/analyzer/lib/src/dart/analysis/context_root.dart
index 583fd5b..830e013 100644
--- a/pkg/analyzer/lib/src/dart/analysis/context_root.dart
+++ b/pkg/analyzer/lib/src/dart/analysis/context_root.dart
@@ -56,13 +56,14 @@
 
   @override
   Iterable<String> analyzedFiles() sync* {
+    var visitedCanonicalPaths = <String>{};
     for (String path in includedPaths) {
       if (!_isExcluded(path)) {
         Resource resource = resourceProvider.getResource(path);
         if (resource is File) {
           yield path;
         } else if (resource is Folder) {
-          yield* _includedFilesInFolder(resource);
+          yield* _includedFilesInFolder(visitedCanonicalPaths, resource);
         } else {
           Type type = resource.runtimeType;
           throw StateError('Unknown resource at path "$path" ($type)');
@@ -78,14 +79,21 @@
 
   /// Return the absolute paths of all of the files that are included in the
   /// given [folder].
-  Iterable<String> _includedFilesInFolder(Folder folder) sync* {
+  Iterable<String> _includedFilesInFolder(
+    Set<String> visited,
+    Folder folder,
+  ) sync* {
     for (Resource resource in folder.getChildren()) {
       String path = resource.path;
       if (!_isExcluded(path)) {
         if (resource is File) {
           yield path;
         } else if (resource is Folder) {
-          yield* _includedFilesInFolder(resource);
+          var canonicalPath = resource.resolveSymbolicLinksSync().path;
+          if (visited.add(canonicalPath)) {
+            yield* _includedFilesInFolder(visited, resource);
+            visited.remove(canonicalPath);
+          }
         } else {
           Type type = resource.runtimeType;
           throw StateError('Unknown resource at path "$path" ($type)');
diff --git a/pkg/analyzer/lib/src/services/available_declarations.dart b/pkg/analyzer/lib/src/services/available_declarations.dart
index 06acfc4..6fba2c3 100644
--- a/pkg/analyzer/lib/src/services/available_declarations.dart
+++ b/pkg/analyzer/lib/src/services/available_declarations.dart
@@ -338,8 +338,15 @@
   void _findPackages() {
     var pathContext = _tracker._resourceProvider.pathContext;
     var pubPathPrefixToPathList = <String, List<String>>{};
+    var visitedFolderPaths = <String>{};
 
+    /// TODO(scheglov) Use analyzedFiles() ?
     void visitFolder(Folder folder) {
+      var canonicalFolderPath = folder.resolveSymbolicLinksSync().path;
+      if (!visitedFolderPaths.add(canonicalFolderPath)) {
+        return;
+      }
+
       var buildFile = folder.getChildAssumingFile('BUILD');
       var pubspecFile = folder.getChildAssumingFile('pubspec.yaml');
       if (buildFile.exists) {
diff --git a/pkg/analyzer/test/src/dart/analysis/context_locator_test.dart b/pkg/analyzer/test/src/dart/analysis/context_locator_test.dart
index e7df093..469a182 100644
--- a/pkg/analyzer/test/src/dart/analysis/context_locator_test.dart
+++ b/pkg/analyzer/test/src/dart/analysis/context_locator_test.dart
@@ -42,6 +42,136 @@
     contextLocator = ContextLocatorImpl(resourceProvider: resourceProvider);
   }
 
+  void test_locateRoots_link_file_toOutOfRoot() {
+    Folder rootFolder = newFolder('/home/test');
+    newFile('/home/test/lib/a.dart');
+    newFile('/home/b.dart');
+    resourceProvider.newLink(
+      convertPath('/home/test/lib/c.dart'),
+      convertPath('/home/b.dart'),
+    );
+
+    List<ContextRoot> roots =
+        contextLocator.locateRoots(includedPaths: [rootFolder.path]);
+    expect(roots, hasLength(1));
+
+    ContextRoot root = findRoot(roots, rootFolder);
+    expect(root.includedPaths, unorderedEquals([rootFolder.path]));
+    expect(root.excludedPaths, isEmpty);
+    expect(root.optionsFile, isNull);
+    expect(root.packagesFile, isNull);
+
+    _assertAnalyzedFiles(root, [
+      '/home/test/lib/a.dart',
+      '/home/test/lib/c.dart',
+    ]);
+  }
+
+  void test_locateRoots_link_file_toSiblingInRoot() {
+    Folder rootFolder = newFolder('/test');
+    newFile('/test/lib/a.dart');
+    resourceProvider.newLink(
+      convertPath('/test/lib/b.dart'),
+      convertPath('/test/lib/a.dart'),
+    );
+
+    List<ContextRoot> roots =
+        contextLocator.locateRoots(includedPaths: [rootFolder.path]);
+    expect(roots, hasLength(1));
+
+    ContextRoot root = findRoot(roots, rootFolder);
+    expect(root.includedPaths, unorderedEquals([rootFolder.path]));
+    expect(root.excludedPaths, isEmpty);
+    expect(root.optionsFile, isNull);
+    expect(root.packagesFile, isNull);
+
+    _assertAnalyzedFiles(root, [
+      '/test/lib/a.dart',
+      '/test/lib/b.dart',
+    ]);
+  }
+
+  void test_locateRoots_link_folder_toParentInRoot() {
+    Folder rootFolder = newFolder('/test');
+    newFile('/test/lib/a.dart');
+    resourceProvider.newLink(
+      convertPath('/test/lib/foo'),
+      convertPath('/test/lib'),
+    );
+
+    List<ContextRoot> roots =
+        contextLocator.locateRoots(includedPaths: [rootFolder.path]);
+    expect(roots, hasLength(1));
+
+    ContextRoot root = findRoot(roots, rootFolder);
+    expect(root.includedPaths, unorderedEquals([rootFolder.path]));
+    expect(root.excludedPaths, isEmpty);
+    expect(root.optionsFile, isNull);
+    expect(root.packagesFile, isNull);
+
+    _assertAnalyzedFiles(root, ['/test/lib/a.dart']);
+
+    _assertAnalyzed(root, [
+      '/test/lib/a.dart',
+      '/test/lib/foo/b.dart',
+    ]);
+  }
+
+  void test_locateRoots_link_folder_toParentOfRoot() {
+    Folder rootFolder = newFolder('/home/test');
+    newFile('/home/test/lib/a.dart');
+    newFile('/home/b.dart');
+    newFile('/home/other/c.dart');
+    resourceProvider.newLink(
+      convertPath('/home/test/lib/foo'),
+      convertPath('/home'),
+    );
+
+    List<ContextRoot> roots =
+        contextLocator.locateRoots(includedPaths: [rootFolder.path]);
+    expect(roots, hasLength(1));
+
+    ContextRoot root = findRoot(roots, rootFolder);
+    expect(root.includedPaths, unorderedEquals([rootFolder.path]));
+    expect(root.excludedPaths, isEmpty);
+    expect(root.optionsFile, isNull);
+    expect(root.packagesFile, isNull);
+
+    // The set of analyzed files includes everything in `/home`,
+    // but does not repeat `/home/test/lib/a.dart` and does not cycle.
+    _assertAnalyzedFiles(root, [
+      '/home/test/lib/a.dart',
+      '/home/test/lib/foo/b.dart',
+      '/home/test/lib/foo/other/c.dart',
+    ]);
+  }
+
+  void test_locateRoots_link_folder_toSiblingInRoot() {
+    Folder rootFolder = newFolder('/test');
+    newFile('/test/lib/a.dart');
+    newFile('/test/lib/foo/b.dart');
+    resourceProvider.newLink(
+      convertPath('/test/lib/bar'),
+      convertPath('/test/lib/foo'),
+    );
+
+    List<ContextRoot> roots =
+        contextLocator.locateRoots(includedPaths: [rootFolder.path]);
+    expect(roots, hasLength(1));
+
+    ContextRoot root = findRoot(roots, rootFolder);
+    expect(root.includedPaths, unorderedEquals([rootFolder.path]));
+    expect(root.excludedPaths, isEmpty);
+    expect(root.optionsFile, isNull);
+    expect(root.packagesFile, isNull);
+
+    _assertAnalyzedFiles(root, [
+      '/test/lib/a.dart',
+      '/test/lib/foo/b.dart',
+      '/test/lib/bar/b.dart',
+    ]);
+  }
+
   void test_locateRoots_multiple_dirAndNestedDir() {
     Folder outerRootFolder = newFolder('/test/outer');
     File outerOptionsFile = newOptionsFile('/test/outer');
@@ -789,6 +919,12 @@
     }
   }
 
+  void _assertAnalyzedFiles(ContextRoot root, List<String> posixPathList) {
+    var analyzedFiles = root.analyzedFiles().toList();
+    var pathList = posixPathList.map(convertPath).toList();
+    expect(analyzedFiles, unorderedEquals(pathList));
+  }
+
   void _assertNotAnalyzed(ContextRoot root, List<String> posixPathList) {
     for (var posixPath in posixPathList) {
       var path = convertPath(posixPath);
diff --git a/pkg/analyzer/test/src/services/available_declarations_test.dart b/pkg/analyzer/test/src/services/available_declarations_test.dart
index 019fe7b..6ba066f 100644
--- a/pkg/analyzer/test/src/services/available_declarations_test.dart
+++ b/pkg/analyzer/test/src/services/available_declarations_test.dart
@@ -211,10 +211,6 @@
     expect(library.id, id);
   }
 
-  @SkippedTest(
-    issue: 'https://github.com/dart-lang/sdk/issues/44501',
-    reason: 'Actually, with fixed ResourceProvider, this test cycles',
-  )
   test_getLibrary_exportViaRecursiveLink() async {
     resourceProvider.newLink(
       convertPath('/home/test/lib/foo'),