feat(extraction): initial support for extracting openjdk11 (#3821)

* chore: rename openjdk extractor to openjdk8

* feat(extraction): initial java wrapper script for openjdk11

* feat(extraction): initial support for extracting openjdk11

There is still work to be done in the extractor and indexer for
supporting modular builds, but this will successfully produce .kzip
files for most of openjdk11.

* chore(extraction): fix lint complaints

* chore: rename openjdk extractor to openjdk8

* feat(extraction): initial java wrapper script for openjdk11

* feat(typescript_indexer): Getter/Setter entries (#3784)

* wrap plugin indexer in try/catch block (#3812)

* chore: explicity mark py_binary version (#3703)

* build: disable incompatible_list_based_execution_strategy_selection (#3817)

* feat(typescript_indexer): VName schema compliance for constructors (#3785)

* add constructor spec impl

* Ensure members declared in a ctor are scoped to the class. Add tests for this.

* Remove getter/setter code and conflicts

* Remove comment artifact, make class properties emit "childof" entries of the class.

* parameters in constructors and functions are children of those functions

* use more idiomatic type check

* docs(typescript_indexer): document separate compilation (#3806)

This describes how separate compilation works for Google's TS builds.
It's not obvious from this code that this is how things work; for
example, in non-Google TS builds, the "compilation unit" that TS works
with is generally all source code in the app all at once.

(As you might expect, that is costly; recent versions of TypeScript have
been working on adding some more incremental compilation support, but
I'm not clear on how exactly it works.)

* fix(objc): Log extractor errors (#3818)

Bazel captures logs below error. Increase our log level so we actually see the logs during a bazel build.

* feat(extraction): initial support for extracting openjdk11

There is still work to be done in the extractor and indexer for
supporting modular builds, but this will successfully produce .kzip
files for most of openjdk11.

* chore(extraction): fix lint complaints

* chore(extraction): address comments

* chore(cxx_extractor): clang format

* chore(extraction): address comments, remove moved go source

* chore(extraction): use absolute path in example

* chore(extraction): make path to JDK a flag

* chore(extraction): address comments, move most logic to extract command

* chore(extraction): address further comment
diff --git a/go.mod b/go.mod
index 7fece19..8512cab 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@
 	bitbucket.org/creachadair/stringset v0.0.3
 	github.com/DataDog/zstd v1.3.5
 	github.com/apache/beam v0.0.0-20190503190717-3cf3a4708c58
+	github.com/bazelbuild/rules_go v0.0.0-20190612215247-20ab48681cad
 	github.com/beevik/etree v1.1.0
 	github.com/dsnet/compress v0.0.1 // indirect
 	github.com/ghodss/yaml v1.0.1-0.20180820084758-c7ce16629ff4
@@ -37,6 +38,7 @@
 	golang.org/x/net v0.0.0-20190213061140-3a22650c66bd
 	golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be
 	golang.org/x/sync v0.0.0-20181108010431-42b317875d0f
+	golang.org/x/sys v0.0.0-20190130150945-aca44879d564
 	golang.org/x/text v0.3.0
 	golang.org/x/tools v0.0.0-20190228203856-589c23e65e65
 	google.golang.org/api v0.0.0-20180404000327-3097bf831ede
diff --git a/go.sum b/go.sum
index b5f9ed5..11faff0 100644
--- a/go.sum
+++ b/go.sum
@@ -8,6 +8,8 @@
 github.com/DataDog/zstd v1.3.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
 github.com/apache/beam v0.0.0-20190503190717-3cf3a4708c58 h1:J5kBjMKEdIPFrxE0zEqH/5yeV8BFThaxyIEfTF8psQY=
 github.com/apache/beam v0.0.0-20190503190717-3cf3a4708c58/go.mod h1:/8NX3Qi8vGstDLLaeaU7+lzVEu/ACaQhYjeefzQ0y1o=
+github.com/bazelbuild/rules_go v0.0.0-20190612215247-20ab48681cad h1:6kiycsdgfnwDxaORxEYkxyCpe90ah+eMsVWTJMcK70o=
+github.com/bazelbuild/rules_go v0.0.0-20190612215247-20ab48681cad/go.mod h1:MC23Dc/wkXEyk3Wpq6lCqz0ZAYOZDw2DR5y3N1q2i7M=
 github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
 github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
diff --git a/kythe/cxx/extractor/objc_extractor_bazel_main.cc b/kythe/cxx/extractor/objc_extractor_bazel_main.cc
index 55f473f..9315bd7 100644
--- a/kythe/cxx/extractor/objc_extractor_bazel_main.cc
+++ b/kythe/cxx/extractor/objc_extractor_bazel_main.cc
@@ -123,7 +123,7 @@
 
   if (ContainsUnsupportedArg(args)) {
     LOG(ERROR) << "Not extracting " << info.owner()
-              << " because it had an unsupported argument.";
+               << " because it had an unsupported argument.";
     return false;
   }
 
@@ -162,7 +162,7 @@
 
   if (ContainsUnsupportedArg(args)) {
     LOG(ERROR) << "Not extracting " << info.owner()
-              << " because it had an unsupported argument.";
+               << " because it had an unsupported argument.";
     return false;
   }
 
diff --git a/kythe/extractors/openjdk11/BUILD b/kythe/extractors/openjdk11/BUILD
new file mode 100644
index 0000000..6981cff
--- /dev/null
+++ b/kythe/extractors/openjdk11/BUILD
@@ -0,0 +1,24 @@
+load("//tools:build_rules/shims.bzl", "go_binary")
+
+go_binary(
+    name = "java_wrapper",
+    srcs = ["java_wrapper/java_wrapper.go"],
+    deps = [
+        "@org_bitbucket_creachadair_shell//:go_default_library",
+        "@org_golang_x_sys//unix:go_default_library",
+    ],
+)
+
+go_binary(
+    name = "extract",
+    srcs = ["extract/extract.go"],
+    data = [
+        "vnames.json",
+        ":java_wrapper",
+        "//kythe/java/com/google/devtools/kythe/extractors/java/standalone:javac9_extractor_deploy.jar",
+    ],
+    deps = [
+        "//kythe/go/util/flagutil",
+        "@io_bazel_rules_go//go/tools/bazel:go_default_library",
+    ],
+)
diff --git a/kythe/extractors/openjdk11/README.md b/kythe/extractors/openjdk11/README.md
new file mode 100644
index 0000000..b028d0a
--- /dev/null
+++ b/kythe/extractors/openjdk11/README.md
@@ -0,0 +1,21 @@
+# Kythe Extracting openjdk11
+
+This package describes extracting OpenJDK11 from source using Kythe
+
+## Fetch and Configure OpenJDK11
+
+Follow the instructions outline at
+http://hg.openjdk.java.net/jdk/jdk11/raw-file/tip/doc/building.html
+for configuring OpenJDK11 for your platform.
+
+## Extraction
+
+Once you have a correctly configured source tree for your platform,
+run:
+
+```
+$ bazel run //kythe/extractors/openjdk11:extract -- -jdk /absolute/path/to/openjdk11
+```
+
+Assuming the source tree was configured correctly, the result will be a
+collection of .kzip files (one per module) in ~/kythe-openjdk11-output
diff --git a/kythe/extractors/openjdk11/extract/extract.go b/kythe/extractors/openjdk11/extract/extract.go
new file mode 100644
index 0000000..43d906d
--- /dev/null
+++ b/kythe/extractors/openjdk11/extract/extract.go
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2019 The Kythe Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Binary extract extracts a pre-configured OpenJDK11 source tree to produce Kythe compilation units.
+package main
+
+import (
+	"bufio"
+	"context"
+	"errors"
+	"flag"
+	"io"
+	"log"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"strings"
+
+	"kythe.io/kythe/go/util/flagutil"
+
+	"github.com/bazelbuild/rules_go/go/tools/bazel"
+)
+
+const (
+	javaCommandVar      = "KYTHE_JAVA_COMMAND"
+	wrapperExtractorVar = "KYTHE_JAVA_EXTRACTOR_JAR"
+
+	kytheRootVar   = "KYTHE_ROOT_DIRECTORY"
+	kytheOutputVar = "KYTHE_OUTPUT_DIRECTORY"
+	kytheCorpusVar = "KYTHE_CORPUS"
+	kytheVNameVar  = "KYTHE_VNAMES"
+
+	javaMakeVar           = "JAVA_CMD"
+	runfilesWrapperPath   = "io_kythe/kythe/extractors/openjdk11/java_wrapper"
+	runfilesVNamesPath    = "io_kythe/kythe/extractors/openjdk11/vnames.json"
+	runfilesExtractorPath = "io_kythe/kythe/java/com/google/devtools/kythe/extractors/java/standalone/javac9_extractor_deploy.jar"
+)
+
+var (
+	makeDir       string
+	outputDir     string
+	vNameRules    string
+	wrapperPath   string
+	extractorPath string
+	errorPattern  = regexp.MustCompile("ERROR: extractor failure for module ([^:]*):")
+)
+
+func setupRunfiles() error {
+	if os.Getenv("RUNFILES_DIR") != "" || os.Getenv("RUNFILES_MANIFEST_FILE") != "" {
+		return nil
+	}
+	for _, base := range []string{os.Args[0] + ".runfiles", "."} {
+		root, err := filepath.Abs(base)
+		if err != nil {
+			continue
+		} else if _, err := os.Stat(root); err != nil {
+			continue
+		}
+		os.Setenv("RUNFILES_DIR", root)
+		manifest := filepath.Join(root, "MANIFEST")
+		if _, err := os.Stat(manifest); err == nil {
+			os.Setenv("RUNFILES_MANIFEST_FILE", manifest)
+		}
+		return nil
+	}
+	return errors.New("unable to setup runfiles")
+}
+
+func defaultWrapperPath() string {
+	val, _ := bazel.Runfile(runfilesWrapperPath)
+	return val
+}
+
+func defaultVNamesPath() string {
+	val, _ := bazel.Runfile(runfilesVNamesPath)
+	return val
+}
+
+func defaultExtractorPath() string {
+	val, _ := bazel.Runfile(runfilesExtractorPath)
+	return val
+}
+
+func defaultOutputDir() string {
+	val := os.Getenv(kytheOutputVar)
+	if val == "" {
+		usr, err := user.Current()
+		if err != nil {
+			log.Fatalf("ERROR: unable to determine current user: %v", err)
+		}
+		val = filepath.Join(usr.HomeDir, "kythe-openjdk11-output")
+	}
+	return val
+}
+
+func findJavaCommand() (string, error) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+	cmd := exec.CommandContext(ctx, "make", "-n", "-p")
+	cmd.Dir = makeDir
+	cmd.Stderr = os.Stderr
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		return "", err
+	}
+	if err := cmd.Start(); err != nil {
+		return "", err
+	}
+	const prefix = javaMakeVar + " := "
+	scanner := bufio.NewScanner(stdout)
+	for scanner.Scan() {
+		if strings.HasPrefix(scanner.Text(), prefix) {
+			cancel() // Safe to call repeatedly.
+			cmd.Wait()
+			return strings.TrimPrefix(scanner.Text(), prefix), nil
+		}
+	}
+	return "", cmd.Wait()
+}
+
+func mustFindJavaCommand() string {
+	java, err := findJavaCommand()
+	if err != nil {
+		log.Fatalf("unable to determine %s: %v", javaCommandVar, err)
+	}
+	return java
+}
+
+func setEnvDefaultFunc(env []string, key string, value func() string) []string {
+	if val := os.Getenv(key); val == "" {
+		env = append(env, key+"="+value())
+	}
+	return env
+}
+
+func setEnvDefault(env []string, key, value string) []string {
+	return setEnvDefaultFunc(env, key, func() string { return value })
+}
+
+func makeEnv() []string {
+	env := os.Environ()
+	env = setEnvDefaultFunc(env, javaCommandVar, mustFindJavaCommand)
+	env = setEnvDefault(env, kytheCorpusVar, "openjdk11")
+	if vNameRules != "" {
+		env = setEnvDefault(env, kytheVNameVar, vNameRules)
+	}
+	env = append(env,
+		kytheRootVar+"="+makeDir,
+		kytheOutputVar+"="+outputDir,
+		wrapperExtractorVar+"="+extractorPath)
+	return env
+}
+
+func init() {
+	setupRunfiles()
+	flag.StringVar(&makeDir, "jdk", "", "path to the OpenJDK11 source tree (required)")
+	flag.StringVar(&outputDir, "output", defaultOutputDir(), "path to which the compilations and errors should be written (optional)")
+	flag.StringVar(&vNameRules, "rules", defaultVNamesPath(), "path of vnames.json file (optional)")
+	flag.StringVar(&wrapperPath, "java_wrapper", defaultWrapperPath(), "path to the java_wrapper executable (optional)")
+	flag.StringVar(&extractorPath, "extractor_jar", defaultExtractorPath(), "path to the javac_extractor_deployt.jar (optional)")
+	flag.Usage = flagutil.SimpleUsage("Extract a configured openjdk11 source directory", "[--java_wrapper=] [path]")
+}
+
+func main() {
+	flag.Parse()
+	if makeDir == "" {
+		flagutil.UsageError("missing -jdk")
+	}
+	if wrapperPath == "" {
+		flagutil.UsageError("missing -java_wrapper")
+	}
+	if _, err := os.Stat(wrapperPath); err != nil {
+		flagutil.UsageErrorf("java_wrapper not found: %v", err)
+	}
+
+	cmd := exec.Command("make", javaMakeVar+"="+wrapperPath, "ENABLE_JAVAC_SERVER=no", "clean", "jdk")
+	cmd.Dir = makeDir
+	cmd.Env = makeEnv()
+	cmd.Stdout = nil // Quiet, you
+	stderr, err := cmd.StderrPipe()
+
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	if err := cmd.Start(); err != nil {
+		log.Fatal(err)
+	}
+
+	logCollectedErrors(io.TeeReader(stderr, os.Stderr))
+
+	if err := cmd.Wait(); err != nil {
+		log.Fatal(err)
+	}
+}
+
+func collectExtractionErrors(stderr io.Reader) ([]string, error) {
+	var result []string
+	scanner := bufio.NewScanner(stderr)
+	for scanner.Scan() {
+		if subs := errorPattern.FindSubmatch(scanner.Bytes()); subs != nil {
+			result = append(result, string(subs[1]))
+		}
+	}
+	return result, scanner.Err()
+}
+
+func logCollectedErrors(stderr io.Reader) {
+	errors, err := collectExtractionErrors(stderr)
+	if err != nil {
+		log.Println(err)
+	}
+	log.Println("Error extracting modules:\n\t", strings.Join(errors, "\n\t"))
+}
diff --git a/kythe/extractors/openjdk11/java_wrapper/java_wrapper.go b/kythe/extractors/openjdk11/java_wrapper/java_wrapper.go
new file mode 100644
index 0000000..4cdf05b
--- /dev/null
+++ b/kythe/extractors/openjdk11/java_wrapper/java_wrapper.go
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2019 The Kythe Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Binary java_wrapper wraps the real java command to invoke the standalone java extractor
+// in parallel with the genuine compilation command.
+// As it interjects itself between make and a real java command, all options are
+// provided via environment variables.  See the corresponding consts and java_extractor for details.
+package main
+
+import (
+	"fmt"
+	"log"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"regexp"
+	"strings"
+
+	"bitbucket.org/creachadair/shell"
+	"golang.org/x/sys/unix"
+)
+
+const (
+	javaCommandVar  = "KYTHE_JAVA_COMMAND"       // Path to the real java command, required.
+	extractorJarVar = "KYTHE_JAVA_EXTRACTOR_JAR" // Path to the javac_extractor jar, required.
+	kytheOutputVar  = "KYTHE_OUTPUT_DIRECTORY"   // Extraction output directory, required.
+	kytheTargetVar  = "KYTHE_ANALYSIS_TARGET"    // Extraction analysis target to set, defaults to the java module name.
+)
+
+var (
+	modulePattern = regexp.MustCompile("^@/.*/_the.(.*)_batch.tmp$")
+)
+
+func moduleName() string {
+	// Hackish way to determine the likely module being compiled.
+	return modulePattern.ReplaceAllString(os.Args[len(os.Args)-1], "$1")
+}
+
+func outputDir() string {
+	if val := os.Getenv(kytheOutputVar); val != "" {
+		return val
+	}
+	log.Fatal("ERROR: KYTHE_OUTPUT_DIRECTORY not set")
+	return ""
+}
+
+func mustGetEnvPath(key string) string {
+	if val := os.Getenv(key); val != "" {
+		if _, err := os.Stat(val); err != nil {
+			log.Fatalf("invalid %s: %v", key, err)
+		}
+		return val
+	}
+	log.Fatal(key + " not set")
+	return ""
+}
+
+func javaCommand() string {
+	return mustGetEnvPath(javaCommandVar)
+}
+
+func extractorJar() string {
+	return mustGetEnvPath(extractorJarVar)
+}
+
+func extractorArgs(args []string, jar string) []string {
+	isJavac := false
+	var result []string
+	for len(args) > 0 {
+		var a string
+		var v string
+		switch a, args = shift(args); a {
+		case "-m", "--module":
+			v, args = shift(args)
+			if !strings.HasSuffix(v, ".javac.Main") {
+				isJavac = false
+				break
+			}
+			isJavac = true
+			result = append(result,
+				"--add-exports=jdk.compiler.interim/com.sun.tools.javac.main=ALL-UNNAMED",
+				"--add-exports=jdk.compiler.interim/com.sun.tools.javac.util=ALL-UNNAMED",
+				"--add-exports=jdk.compiler.interim/com.sun.tools.javac.file=ALL-UNNAMED",
+				"--add-exports=jdk.compiler.interim/com.sun.tools.javac.api=ALL-UNNAMED",
+				"--add-exports=jdk.compiler.interim/com.sun.tools.javac.code=ALL-UNNAMED",
+				"-jar", jar, "-Xprefer:source")
+		case "--add-modules", "--limit-modules":
+			v, args = shift(args)
+			result = append(result, a, v+",java.logging,java.sql")
+		case "--doclint-format":
+			_, args = shift(args)
+		case "-Werror":
+		default:
+			switch {
+			case strings.HasPrefix(a, "-Xplugin:depend"), strings.HasPrefix(a, "-Xlint:"), strings.HasPrefix(a, "-Xdoclint"):
+			case strings.HasPrefix(a, "-Xmx"):
+				result = append(result, "-Xmx3G")
+			default:
+				result = append(result, a)
+			}
+		}
+	}
+	// As we can only do anything meaningful with Java compilations,
+	// but wrap the java binary, don't attempt to exact other invocations.
+	if !isJavac {
+		return nil
+	}
+	return result
+}
+
+func extractorEnv() []string {
+	env := os.Environ()
+	env = setEnvDefault(env, kytheTargetVar, moduleName())
+	return env
+}
+
+func setEnvDefault(env []string, key, def string) []string {
+	if val := os.Getenv(key); val == "" {
+		env = append(env, key+"="+def)
+	}
+	return env
+}
+
+func shift(args []string) (string, []string) {
+	if len(args) > 0 {
+		return args[0], args[1:]
+	}
+	return "", nil
+}
+
+func main() {
+	java := javaCommand()
+	jar := extractorJar()
+	if args := extractorArgs(os.Args[1:], jar); len(args) > 0 {
+		cmd := exec.Command(java, args...)
+		cmd.Env = extractorEnv()
+		log.Printf("*** Extracting: %s", moduleName())
+		if output, err := cmd.CombinedOutput(); err != nil {
+			w, err := os.Create(filepath.Join(outputDir(), moduleName()+".err"))
+			if err != nil {
+				log.Fatalf("Error creating error log for module %s: %v", moduleName(), err)
+			}
+			fmt.Fprintf(w, "--- %s\n", shell.Join(args))
+			w.Write(output)
+			w.Close()
+
+			// Log, but don't abort, on extraction failures.
+			log.Printf("ERROR: extractor failure for module %s: %v", moduleName(), err)
+		}
+	}
+	// Always end by running the java command directly, as "java".
+	os.Args[0] = "java"
+	log.Fatal(unix.Exec(java, os.Args, os.Environ())) // If exec returns at all, it's an error.
+}
diff --git a/kythe/extractors/openjdk11/vnames.json b/kythe/extractors/openjdk11/vnames.json
new file mode 100644
index 0000000..d190f93
--- /dev/null
+++ b/kythe/extractors/openjdk11/vnames.json
@@ -0,0 +1,17 @@
+[
+ {
+  "pattern": "(build/(?:[^/]*/)?support/gensrc)/(.*)",
+  "vname": {
+   "corpus": "openjdk11",
+   "path": "@2@",
+   "root": "@1@"
+  }
+ },
+ {
+  "pattern": "(?:srcs/)?src/(.*)",
+  "vname": {
+   "corpus": "openjdk11",
+   "path": "@1@"
+  }
+ }
+]
diff --git a/kythe/extractors/openjdk/BUILD b/kythe/extractors/openjdk8/BUILD
similarity index 100%
rename from kythe/extractors/openjdk/BUILD
rename to kythe/extractors/openjdk8/BUILD
diff --git a/kythe/extractors/openjdk/Dockerfile b/kythe/extractors/openjdk8/Dockerfile
similarity index 100%
rename from kythe/extractors/openjdk/Dockerfile
rename to kythe/extractors/openjdk8/Dockerfile
diff --git a/kythe/extractors/openjdk/extract.sh b/kythe/extractors/openjdk8/extract.sh
similarity index 100%
rename from kythe/extractors/openjdk/extract.sh
rename to kythe/extractors/openjdk8/extract.sh
diff --git a/kythe/extractors/openjdk/run.sh b/kythe/extractors/openjdk8/run.sh
similarity index 100%
rename from kythe/extractors/openjdk/run.sh
rename to kythe/extractors/openjdk8/run.sh
diff --git a/kythe/java/com/google/devtools/kythe/extractors/shared/FileVNames.java b/kythe/java/com/google/devtools/kythe/extractors/shared/FileVNames.java
index c91437e..c5cd816 100644
--- a/kythe/java/com/google/devtools/kythe/extractors/shared/FileVNames.java
+++ b/kythe/java/com/google/devtools/kythe/extractors/shared/FileVNames.java
@@ -123,6 +123,11 @@
       this.pattern = pattern;
       this.vname = vname;
     }
+
+    // GSON-required no-args constructor.
+    private BaseFileVName() {
+      this(null, null);
+    }
   }
 
   /** Subset of a {@link VName} with built-in templating '@<num>@' markers. */
@@ -137,6 +142,11 @@
       this.path = path;
     }
 
+    // GSON-required no-args constructor.
+    private VNameTemplate() {
+      this(null, null, null);
+    }
+
     /**
      * Returns a {@link VName} by filling in its corpus/root/path with regex groups in the given
      * {@link Matcher}.