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);
 }