Introduce --dep_roots flag to Stardoc to search alternate roots for bzl deps

This is necessary to handle generated bzl input files.

See https://github.com/bazelbuild/skydoc/issues/151 for details.

RELNOTES: None.
PiperOrigin-RevId: 233481475
diff --git a/src/main/java/com/google/devtools/build/skydoc/FilesystemFileAccessor.java b/src/main/java/com/google/devtools/build/skydoc/FilesystemFileAccessor.java
index e9ad5f3..2b1bd91 100644
--- a/src/main/java/com/google/devtools/build/skydoc/FilesystemFileAccessor.java
+++ b/src/main/java/com/google/devtools/build/skydoc/FilesystemFileAccessor.java
@@ -30,4 +30,9 @@
     byte[] content = Files.readAllBytes(Paths.get(pathString));
     return ParserInputSource.create(content, PathFragment.create(pathString));
   }
+
+  @Override
+  public boolean fileExists(String pathString) {
+    return Files.exists(Paths.get(pathString));
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/skydoc/SkydocMain.java b/src/main/java/com/google/devtools/build/skydoc/SkydocMain.java
index fd256a7..20d4f52 100644
--- a/src/main/java/com/google/devtools/build/skydoc/SkydocMain.java
+++ b/src/main/java/com/google/devtools/build/skydoc/SkydocMain.java
@@ -142,9 +142,17 @@
   private final LinkedHashSet<Path> pending = new LinkedHashSet<>();
   private final Map<Path, Environment> loaded = new HashMap<>();
   private final SkylarkFileAccessor fileAccessor;
+  private final List<String> depRoots;
 
-  public SkydocMain(SkylarkFileAccessor fileAccessor) {
+  public SkydocMain(SkylarkFileAccessor fileAccessor, List<String> depRoots) {
     this.fileAccessor = fileAccessor;
+    if (depRoots.isEmpty()) {
+      // For backwards compatibility, if no dep_roots are specified, use the current
+      // directory as the only root.
+      this.depRoots = ImmutableList.of(".");
+    } else {
+      this.depRoots = depRoots;
+    }
   }
 
   public static void main(String[] args)
@@ -158,6 +166,7 @@
     String targetFileLabelString;
     String outputPath;
     ImmutableSet<String> symbolNames;
+    ImmutableList<String> depRoots;
 
     // TODO(cparsons): Remove optional positional arg parsing.
     List<String> residualArgs = parser.getResidue();
@@ -172,10 +181,12 @@
       targetFileLabelString = residualArgs.get(0);
       outputPath = residualArgs.get(1);
       symbolNames = getSymbolNames(residualArgs);
+      depRoots = ImmutableList.of();
     } else {
       targetFileLabelString = skydocOptions.targetFileLabel;
       outputPath = skydocOptions.outputFilePath;
       symbolNames = ImmutableSet.copyOf(skydocOptions.symbolNames);
+      depRoots = ImmutableList.copyOf(skydocOptions.depRoots);
     }
 
     Label targetFileLabel =
@@ -186,7 +197,7 @@
     ImmutableList.Builder<RuleInfo> unknownNamedRules = ImmutableList.builder();
     ImmutableMap.Builder<String, UserDefinedFunction> userDefinedFunctions = ImmutableMap.builder();
 
-    new SkydocMain(new FilesystemFileAccessor())
+    new SkydocMain(new FilesystemFileAccessor(), depRoots)
         .eval(
             semanticsOptions.toSkylarkSemantics(),
             targetFileLabel,
@@ -387,7 +398,7 @@
     }
     pending.add(path);
 
-    ParserInputSource parserInputSource = fileAccessor.inputSource(path.toString());
+    ParserInputSource parserInputSource = getInputSource(path.toString());
     BuildFileAST buildFileAST = BuildFileAST.parseSkylarkFile(parserInputSource, eventHandler);
 
     Map<String, Extension> imports = new HashMap<>();
@@ -403,8 +414,9 @@
         imports.put(anImport.getImportString(), new Extension(importEnv));
       } catch (NoSuchFileException noSuchFileException) {
         throw new IllegalStateException(
-            String.format("File %s imported '%s', yet %s was not found.",
-                path, anImport.getImportString(), pathOfLabel(relativeLabel)));
+            String.format(
+                "File %s imported '%s', yet %s was not found, even at roots %s.",
+                path, anImport.getImportString(), pathOfLabel(relativeLabel), depRoots));
       }
     }
 
@@ -425,6 +437,17 @@
     return Paths.get(workspacePrefix + label.toPathFragment());
   }
 
+  private ParserInputSource getInputSource(String bzlWorkspacePath) throws IOException {
+    for (String rootPath : depRoots) {
+      if (fileAccessor.fileExists(rootPath + "/" + bzlWorkspacePath)) {
+        return fileAccessor.inputSource(rootPath + "/" + bzlWorkspacePath);
+      }
+    }
+
+    // All depRoots attempted and no valid file was found.
+    throw new NoSuchFileException(bzlWorkspacePath);
+  }
+
   /** Evaluates the AST from a single skylark file, given the already-resolved imports. */
   private Environment evalSkylarkBody(
       SkylarkSemantics semantics,
diff --git a/src/main/java/com/google/devtools/build/skydoc/SkydocOptions.java b/src/main/java/com/google/devtools/build/skydoc/SkydocOptions.java
index aa561f5..1a45913 100644
--- a/src/main/java/com/google/devtools/build/skydoc/SkydocOptions.java
+++ b/src/main/java/com/google/devtools/build/skydoc/SkydocOptions.java
@@ -47,4 +47,13 @@
       effectTags = OptionEffectTag.UNKNOWN,
       help = "The path of the file to output documentation into")
   public List<String> symbolNames;
+
+  @Option(
+      name = "dep_roots",
+      allowMultiple = true,
+      defaultValue = "",
+      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+      effectTags = OptionEffectTag.UNKNOWN,
+      help = "File path roots to search when resolving transitive bzl dependencies")
+  public List<String> depRoots;
 }
diff --git a/src/main/java/com/google/devtools/build/skydoc/SkylarkFileAccessor.java b/src/main/java/com/google/devtools/build/skydoc/SkylarkFileAccessor.java
index 9abe532..d4039c4 100644
--- a/src/main/java/com/google/devtools/build/skydoc/SkylarkFileAccessor.java
+++ b/src/main/java/com/google/devtools/build/skydoc/SkylarkFileAccessor.java
@@ -28,4 +28,7 @@
    * string.
    */
   ParserInputSource inputSource(String pathString) throws IOException;
+
+  /** Returns true if a file exists at the current path. */
+  boolean fileExists(String pathString);
 }
diff --git a/src/test/java/com/google/devtools/build/skydoc/SkydocTest.java b/src/test/java/com/google/devtools/build/skydoc/SkydocTest.java
index 5c51834..5ab2830 100644
--- a/src/test/java/com/google/devtools/build/skydoc/SkydocTest.java
+++ b/src/test/java/com/google/devtools/build/skydoc/SkydocTest.java
@@ -50,15 +50,23 @@
 
   @Before
   public void setUp() {
-    skydocMain = new SkydocMain(new SkylarkFileAccessor() {
+    skydocMain =
+        new SkydocMain(
+            new SkylarkFileAccessor() {
 
-      @Override
-      public ParserInputSource inputSource(String pathString) throws IOException {
-        Path path = fileSystem.getPath("/" + pathString);
-        byte[] bytes = FileSystemUtils.asByteSource(path).read();
-        return ParserInputSource.create(bytes, path.asFragment());
-      }
-    });
+              @Override
+              public ParserInputSource inputSource(String pathString) throws IOException {
+                Path path = fileSystem.getPath("/" + pathString);
+                byte[] bytes = FileSystemUtils.asByteSource(path).read();
+                return ParserInputSource.create(bytes, path.asFragment());
+              }
+
+              @Override
+              public boolean fileExists(String pathString) {
+                return fileSystem.exists(fileSystem.getPath("/" + pathString));
+              }
+            },
+            ImmutableList.of("/other_root", "."));
   }
 
   @Test
@@ -169,14 +177,12 @@
         "def rule_impl(ctx):",
         "  return struct()");
 
-    scratch.file(
-        "/deps/foo/docstring.bzl",
-        "doc_string = 'Dep rule'");
+    scratch.file("/other_root/deps/foo/other_root.bzl", "doc_string = 'Dep rule'");
 
     scratch.file(
         "/deps/foo/dep_rule.bzl",
         "load('//lib:rule_impl.bzl', 'rule_impl')",
-        "load(':docstring.bzl', 'doc_string')",
+        "load(':other_root.bzl', 'doc_string')",
         "",
         "_hidden_rule = rule(",
         "    doc = doc_string,",