Merge pull request #2619 from asger-semmle/ts-monorepo-deps

Approved by erik-krogh, max-schaefer
This commit is contained in:
semmle-qlci
2020-02-05 10:57:55 +00:00
committed by GitHub
9 changed files with 637 additions and 90 deletions

View File

@@ -1,31 +1,11 @@
package com.semmle.js.extractor;
import com.semmle.js.extractor.ExtractorConfig.SourceType;
import com.semmle.js.extractor.FileExtractor.FileType;
import com.semmle.js.extractor.trapcache.DefaultTrapCache;
import com.semmle.js.extractor.trapcache.DummyTrapCache;
import com.semmle.js.extractor.trapcache.ITrapCache;
import com.semmle.js.parser.ParsedProject;
import com.semmle.js.parser.TypeScriptParser;
import com.semmle.ts.extractor.TypeExtractor;
import com.semmle.ts.extractor.TypeTable;
import com.semmle.util.data.StringUtil;
import com.semmle.util.exception.CatastrophicError;
import com.semmle.util.exception.Exceptions;
import com.semmle.util.exception.ResourceError;
import com.semmle.util.exception.UserError;
import com.semmle.util.extraction.ExtractorOutputConfig;
import com.semmle.util.files.FileUtil;
import com.semmle.util.io.csv.CSVReader;
import com.semmle.util.language.LegacyLanguage;
import com.semmle.util.process.Env;
import com.semmle.util.projectstructure.ProjectLayout;
import com.semmle.util.trap.TrapWriter;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Writer;
import java.lang.ProcessBuilder.Redirect;
import java.net.URI;
import java.net.URISyntaxException;
@@ -50,6 +30,35 @@ import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.semmle.js.extractor.ExtractorConfig.SourceType;
import com.semmle.js.extractor.FileExtractor.FileType;
import com.semmle.js.extractor.trapcache.DefaultTrapCache;
import com.semmle.js.extractor.trapcache.DummyTrapCache;
import com.semmle.js.extractor.trapcache.ITrapCache;
import com.semmle.js.parser.ParsedProject;
import com.semmle.js.parser.TypeScriptParser;
import com.semmle.ts.extractor.TypeExtractor;
import com.semmle.ts.extractor.TypeTable;
import com.semmle.util.data.StringUtil;
import com.semmle.util.exception.CatastrophicError;
import com.semmle.util.exception.Exceptions;
import com.semmle.util.exception.ResourceError;
import com.semmle.util.exception.UserError;
import com.semmle.util.extraction.ExtractorOutputConfig;
import com.semmle.util.files.FileUtil;
import com.semmle.util.io.WholeIO;
import com.semmle.util.io.csv.CSVReader;
import com.semmle.util.language.LegacyLanguage;
import com.semmle.util.process.Env;
import com.semmle.util.projectstructure.ProjectLayout;
import com.semmle.util.trap.TrapWriter;
/**
* An alternative entry point to the JavaScript extractor.
*
@@ -388,9 +397,10 @@ public class AutoBuild {
for (FileType filetype : defaultExtract)
for (String extension : filetype.getExtensions()) patterns.add("**/*" + extension);
// include .eslintrc files and package.json files
// include .eslintrc files, package.json files, and tsconfig.json files
patterns.add("**/.eslintrc*");
patterns.add("**/package.json");
patterns.add("**/tsconfig.json");
// include any explicitly specified extensions
for (String extension : fileTypes.keySet()) patterns.add("**/*" + extension);
@@ -545,12 +555,15 @@ public class AutoBuild {
List<Path> tsconfigFiles = new ArrayList<>();
findFilesToExtract(defaultExtractor, filesToExtract, tsconfigFiles);
DependencyInstallationResult dependencyInstallationResult = DependencyInstallationResult.empty;
if (!tsconfigFiles.isEmpty() && this.installDependencies) {
this.installDependencies(filesToExtract);
dependencyInstallationResult = this.installDependencies(filesToExtract);
}
// extract TypeScript projects and files
Set<Path> extractedFiles = extractTypeScript(defaultExtractor, filesToExtract, tsconfigFiles);
Set<Path> extractedFiles =
extractTypeScript(
defaultExtractor, filesToExtract, tsconfigFiles, dependencyInstallationResult);
// extract remaining files
for (Path f : filesToExtract) {
@@ -587,36 +600,255 @@ public class AutoBuild {
}
}
protected void installDependencies(Set<Path> filesToExtract) {
if (!verifyYarnInstallation()) {
return;
/**
* Returns an existing file named <code>dir/stem.ext</code> where <code>.ext</code> is any
* of the given extensions, or <code>null</code> if no such file exists.
*/
private static Path tryResolveWithExtensions(Path dir, String stem, Iterable<String> extensions) {
for (String ext : extensions) {
Path path = dir.resolve(stem + ext);
if (Files.exists(dir.resolve(path))) {
return path;
}
}
return null;
}
/**
* Returns an existing file named <code>dir/stem.ext</code> where <code>ext</code> is any TypeScript or JavaScript extension,
* or <code>null</code> if no such file exists.
*/
private static Path tryResolveTypeScriptOrJavaScriptFile(Path dir, String stem) {
Path resolved = tryResolveWithExtensions(dir, stem, FileType.TYPESCRIPT.getExtensions());
if (resolved != null) return resolved;
return tryResolveWithExtensions(dir, stem, FileType.JS.getExtensions());
}
/**
* Gets a child of a JSON object as a string, or <code>null</code>.
*/
private String getChildAsString(JsonObject obj, String name) {
JsonElement child = obj.get(name);
if (child instanceof JsonPrimitive && ((JsonPrimitive)child).isString()) {
return child.getAsString();
}
return null;
}
/**
* Installs dependencies for use by the TypeScript type checker.
* <p>
* Some packages must be downloaded while others exist within the same repo ("monorepos")
* but are not in a location where TypeScript would look for it.
* <p>
* Downloaded packages are intalled under <tt>SCRATCH_DIR</tt>, in a mirrored directory hierarchy
* we call the "virtual source root".
* Each <tt>package.json</tt> file is rewritten and copied to the virtual source root,
* where <tt>yarn install</tt> is invoked.
* <p>
* Packages that exists within the repo are stripped from the dependencies
* before installation, so they are not downloaded. Since they are part of the main source tree,
* these packages are not mirrored under the virtual source root.
* Instead, an explicit package location mapping is passed to the TypeScript parser wrapper.
* <p>
* The TypeScript parser wrapper then overrides module resolution so packages can be found
* under the virtual source root and via that package location mapping.
*/
protected DependencyInstallationResult installDependencies(Set<Path> filesToExtract) {
if (!verifyYarnInstallation()) {
return DependencyInstallationResult.empty;
}
final Path sourceRoot = Paths.get(".").toAbsolutePath();
final Path virtualSourceRoot = Paths.get(EnvironmentVariables.getScratchDir()).toAbsolutePath();
// Read all package.json files and index them by name.
Map<Path, JsonObject> packageJsonFiles = new LinkedHashMap<>();
Map<String, Path> packagesInRepo = new LinkedHashMap<>();
Map<String, Path> packageMainFile = new LinkedHashMap<>();
for (Path file : filesToExtract) {
if (file.getFileName().toString().equals("package.json")) {
System.out.println("Installing dependencies from " + file);
ProcessBuilder pb =
new ProcessBuilder(
Arrays.asList(
"yarn",
"install",
"--non-interactive",
"--ignore-scripts",
"--ignore-platform",
"--ignore-engines",
"--ignore-optional",
"--no-default-rc",
"--no-bin-links",
"--pure-lockfile"));
pb.directory(file.getParent().toFile());
pb.redirectOutput(Redirect.INHERIT);
pb.redirectError(Redirect.INHERIT);
try {
pb.start().waitFor(this.installDependenciesTimeout, TimeUnit.MILLISECONDS);
} catch (IOException | InterruptedException ex) {
throw new ResourceError("Could not install dependencies from " + file, ex);
String text = new WholeIO().read(file);
JsonElement json = new JsonParser().parse(text);
if (!(json instanceof JsonObject)) continue;
JsonObject jsonObject = (JsonObject) json;
file = file.toAbsolutePath();
packageJsonFiles.put(file, jsonObject);
String name = getChildAsString(jsonObject, "name");
if (name != null) {
packagesInRepo.put(name, file);
}
} catch (JsonParseException e) {
System.err.println("Could not parse JSON file: " + file);
System.err.println(e);
// Continue without the malformed package.json file
}
}
}
// Process all package.json files now that we know the names of all local packages.
// - remove dependencies on local packages
// - guess the main file for each package
// Note that we ignore optional dependencies during installation, so "optionalDependencies"
// is ignored here as well.
final List<String> dependencyFields =
Arrays.asList("dependencies", "devDependencies", "peerDependencies");
packageJsonFiles.forEach(
(path, packageJson) -> {
Path relativePath = sourceRoot.relativize(path);
for (String dependencyField : dependencyFields) {
JsonElement dependencyElm = packageJson.get(dependencyField);
if (!(dependencyElm instanceof JsonObject)) continue;
JsonObject dependencyObj = (JsonObject) dependencyElm;
List<String> propsToRemove = new ArrayList<>();
for (String packageName : dependencyObj.keySet()) {
if (packagesInRepo.containsKey(packageName)) {
// Remove dependency on local package
propsToRemove.add(packageName);
} else {
// Remove file dependency on a package that doesn't exist in the checkout.
String dependency = getChildAsString(dependencyObj, packageName);
if (dependency != null && (dependency.startsWith("file:") || dependency.startsWith("./") || dependency.startsWith("../"))) {
if (dependency.startsWith("file:")) {
dependency = dependency.substring("file:".length());
}
Path resolvedPackage = path.getParent().resolve(dependency + "/package.json");
if (!Files.exists(resolvedPackage)) {
propsToRemove.add(packageName);
}
}
}
}
for (String prop : propsToRemove) {
dependencyObj.remove(prop);
}
}
// For named packages, find the main file.
String name = getChildAsString(packageJson, "name");
if (name != null) {
Path entryPoint = guessPackageMainFile(path, packageJson, FileType.TYPESCRIPT.getExtensions());
if (entryPoint == null) {
// Try a TypeScript-recognized JS extension instead
entryPoint = guessPackageMainFile(path, packageJson, Arrays.asList(".js", ".jsx"));
}
if (entryPoint != null) {
System.out.println(relativePath + ": Main file set to " + sourceRoot.relativize(entryPoint));
packageMainFile.put(name, entryPoint);
} else {
System.out.println(relativePath + ": Main file not found");
}
}
});
// Write the new package.json files to disk
for (Path file : packageJsonFiles.keySet()) {
Path relativePath = sourceRoot.relativize(file);
Path virtualFile = virtualSourceRoot.resolve(relativePath);
try {
Files.createDirectories(virtualFile.getParent());
try (Writer writer = Files.newBufferedWriter(virtualFile)) {
new Gson().toJson(packageJsonFiles.get(file), writer);
}
} catch (IOException e) {
throw new ResourceError("Could not rewrite package.json file: " + virtualFile, e);
}
}
// Install dependencies
for (Path file : packageJsonFiles.keySet()) {
Path virtualFile = virtualSourceRoot.resolve(sourceRoot.relativize(file));
System.out.println("Installing dependencies from " + virtualFile);
ProcessBuilder pb =
new ProcessBuilder(
Arrays.asList(
"yarn",
"install",
"--non-interactive",
"--ignore-scripts",
"--ignore-platform",
"--ignore-engines",
"--ignore-optional",
"--no-default-rc",
"--no-bin-links",
"--pure-lockfile"));
pb.directory(virtualFile.getParent().toFile());
pb.redirectOutput(Redirect.INHERIT);
pb.redirectError(Redirect.INHERIT);
try {
pb.start().waitFor(this.installDependenciesTimeout, TimeUnit.MILLISECONDS);
} catch (IOException | InterruptedException ex) {
throw new ResourceError("Could not install dependencies from " + file, ex);
}
}
return new DependencyInstallationResult(virtualSourceRoot, packageMainFile, packagesInRepo);
}
/**
* Attempts to find a TypeScript file that acts as the main entry point to the
* given package - that is, the file you get when importing the package by name
* without any path suffix.
*/
private Path guessPackageMainFile(Path packageJsonFile, JsonObject packageJson, Iterable<String> extensions) {
Path packageDir = packageJsonFile.getParent();
// Try <package_dir>/index.ts.
Path resolved = tryResolveWithExtensions(packageDir, "index", extensions);
if (resolved != null) {
return resolved;
}
// Get the "main" property from the package.json
// This usually refers to the compiled output, such as `./out/foo.js` but may hint as to
// the name of main file ("foo" in this case).
String mainStr = getChildAsString(packageJson, "main");
// Look for source files `./src` if it exists
Path sourceDir = packageDir.resolve("src");
if (Files.isDirectory(sourceDir)) {
// Try `src/index.ts`
resolved = tryResolveTypeScriptOrJavaScriptFile(sourceDir, "index");
if (resolved != null) {
return resolved;
}
// If "main" was defined, try to map it to a file in `src`.
// For example `out/dist/foo.bundle.js` might be mapped back to `src/foo.ts`.
if (mainStr != null) {
Path candidatePath = Paths.get(mainStr);
// Strip off prefix directories that don't exist under `src/`, such as `out` and `dist`.
while (candidatePath.getNameCount() > 1 && !Files.isDirectory(sourceDir.resolve(candidatePath.getParent()))) {
candidatePath = candidatePath.subpath(1, candidatePath.getNameCount());
}
// Strip off extensions until a file can be found
while (true) {
resolved = tryResolveWithExtensions(sourceDir, candidatePath.toString(), extensions);
if (resolved != null) {
return resolved;
}
Path withoutExt = candidatePath.resolveSibling(FileUtil.stripExtension(candidatePath.getFileName().toString()));
if (withoutExt.equals(candidatePath)) break; // No more extensions to strip
candidatePath = withoutExt;
}
}
}
// Try to resolve main as a sibling of the package.json file, such as "./main.js" -> "./main.ts".
if (mainStr != null) {
Path mainPath = Paths.get(mainStr);
String withoutExt = FileUtil.stripExtension(mainPath.getFileName().toString());
resolved = tryResolveWithExtensions(packageDir, withoutExt, extensions);
if (resolved != null) {
return resolved;
}
}
return null;
}
private ExtractorConfig mkExtractorConfig() {
@@ -628,7 +860,10 @@ public class AutoBuild {
}
private Set<Path> extractTypeScript(
FileExtractor extractor, Set<Path> files, List<Path> tsconfig) {
FileExtractor extractor,
Set<Path> files,
List<Path> tsconfig,
DependencyInstallationResult deps) {
Set<Path> extractedFiles = new LinkedHashSet<>();
if (hasTypeScriptFiles(files) || !tsconfig.isEmpty()) {
@@ -640,7 +875,7 @@ public class AutoBuild {
for (Path projectPath : tsconfig) {
File projectFile = projectPath.toFile();
long start = logBeginProcess("Opening project " + projectFile);
ParsedProject project = tsParser.openProject(projectFile);
ParsedProject project = tsParser.openProject(projectFile, deps);
logEndProcess(start, "Done opening project " + projectFile);
// Extract all files belonging to this project which are also matched
// by our include/exclude filters.
@@ -729,7 +964,8 @@ public class AutoBuild {
// extract TypeScript projects from 'tsconfig.json'
if (typeScriptMode == TypeScriptMode.FULL
&& file.getFileName().endsWith("tsconfig.json")
&& !excludes.contains(file)) {
&& !excludes.contains(file)
&& isFileIncluded(file)) {
tsconfigFiles.add(file);
}

View File

@@ -0,0 +1,48 @@
package com.semmle.js.extractor;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Map;
/** Contains the results of installing dependencies. */
public class DependencyInstallationResult {
private Path virtualSourceRoot;
private Map<String, Path> packageEntryPoints;
private Map<String, Path> packageJsonFiles;
public static final DependencyInstallationResult empty =
new DependencyInstallationResult(null, Collections.emptyMap(), Collections.emptyMap());
public DependencyInstallationResult(
Path virtualSourceRoot,
Map<String, Path> packageEntryPoints,
Map<String, Path> packageJsonFiles) {
this.packageEntryPoints = packageEntryPoints;
this.packageJsonFiles = packageJsonFiles;
}
/**
* Returns the virtual source root or <code>null</code> if no virtual source root exists.
*
* The virtual source root is a directory hierarchy that mirrors the real source
* root, where dependencies are installed.
*/
public Path getVirtualSourceRoot() {
return virtualSourceRoot;
}
/**
* Returns the mapping from package names to the TypeScript file that should
* act as its main entry point.
*/
public Map<String, Path> getPackageEntryPoints() {
return packageEntryPoints;
}
/**
* Returns the mapping from package name to corresponding package.json.
*/
public Map<String, Path> getPackageJsonFiles() {
return packageJsonFiles;
}
}

View File

@@ -7,16 +7,22 @@ import com.semmle.util.process.Env.Var;
public class EnvironmentVariables {
public static final String CODEQL_EXTRACTOR_JAVASCRIPT_ROOT_ENV_VAR =
"CODEQL_EXTRACTOR_JAVASCRIPT_ROOT";
public static final String CODEQL_EXTRACTOR_JAVASCRIPT_SCRATCH_DIR_ENV_VAR =
"CODEQL_EXTRACTOR_JAVASCRIPT_SCRATCH_DIR";
public static final String LGTM_WORKSPACE_ENV_VAR =
"LGTM_WORKSPACE";
/**
* Gets the extractor root based on the <code>CODEQL_EXTRACTOR_JAVASCRIPT_ROOT</code> or <code>
* SEMMLE_DIST</code> or environment variable, or <code>null</code> if neither is set.
*/
public static String tryGetExtractorRoot() {
String env = Env.systemEnv().get(CODEQL_EXTRACTOR_JAVASCRIPT_ROOT_ENV_VAR);
if (env != null && !env.isEmpty()) return env;
env = Env.systemEnv().get(Var.SEMMLE_DIST);
if (env != null && !env.isEmpty()) return env;
String env = Env.systemEnv().getNonEmpty(CODEQL_EXTRACTOR_JAVASCRIPT_ROOT_ENV_VAR);
if (env != null) return env;
env = Env.systemEnv().getNonEmpty(Var.SEMMLE_DIST);
if (env != null) return env;
return null;
}
@@ -31,4 +37,16 @@ public class EnvironmentVariables {
}
return env;
}
/**
* Gets the scratch directory from the appropriate environment variable.
*/
public static String getScratchDir() {
String env = Env.systemEnv().getNonEmpty(CODEQL_EXTRACTOR_JAVASCRIPT_SCRATCH_DIR_ENV_VAR);
if (env != null) return env;
env = Env.systemEnv().getNonEmpty(LGTM_WORKSPACE_ENV_VAR);
if (env != null) return env;
throw new UserError(CODEQL_EXTRACTOR_JAVASCRIPT_SCRATCH_DIR_ENV_VAR + " or " + LGTM_WORKSPACE_ENV_VAR + " must be set");
}
}

View File

@@ -140,7 +140,7 @@ public class Main {
for (File projectFile : projectFiles) {
long start = verboseLogStartTimer(ap, "Opening project " + projectFile);
ParsedProject project = tsParser.openProject(projectFile);
ParsedProject project = tsParser.openProject(projectFile, DependencyInstallationResult.empty);
verboseLogEndTimer(ap, start);
// Extract all files belonging to this project which are also matched
// by our include/exclude filters.

View File

@@ -1,14 +1,5 @@
package com.semmle.js.extractor.test;
import com.semmle.js.extractor.AutoBuild;
import com.semmle.js.extractor.ExtractorState;
import com.semmle.js.extractor.FileExtractor;
import com.semmle.js.extractor.FileExtractor.FileType;
import com.semmle.util.data.StringUtil;
import com.semmle.util.exception.UserError;
import com.semmle.util.files.FileUtil;
import com.semmle.util.files.FileUtil8;
import com.semmle.util.process.Env;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -25,12 +16,24 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.After;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import com.semmle.js.extractor.AutoBuild;
import com.semmle.js.extractor.DependencyInstallationResult;
import com.semmle.js.extractor.ExtractorState;
import com.semmle.js.extractor.FileExtractor;
import com.semmle.js.extractor.FileExtractor.FileType;
import com.semmle.util.data.StringUtil;
import com.semmle.util.exception.UserError;
import com.semmle.util.files.FileUtil;
import com.semmle.util.files.FileUtil8;
import com.semmle.util.process.Env;
public class AutoBuildTests {
private Path SEMMLE_DIST, LGTM_SRC;
private Set<String> expected;
@@ -129,8 +132,9 @@ public class AutoBuildTests {
}
@Override
protected void installDependencies(Set<Path> filesToExtract) {
protected DependencyInstallationResult installDependencies(Set<Path> filesToExtract) {
// never install dependencies during testing
return DependencyInstallationResult.empty;
}
@Override

View File

@@ -1,12 +1,31 @@
package com.semmle.js.parser;
import ch.qos.logback.classic.Level;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.ProcessBuilder.Redirect;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.semmle.js.extractor.DependencyInstallationResult;
import com.semmle.js.extractor.EnvironmentVariables;
import com.semmle.js.extractor.ExtractionMetrics;
import com.semmle.js.parser.JSParser.Result;
@@ -23,21 +42,8 @@ import com.semmle.util.logging.LogbackUtils;
import com.semmle.util.process.AbstractProcessBuilder;
import com.semmle.util.process.Builder;
import com.semmle.util.process.Env;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.ProcessBuilder.Redirect;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import ch.qos.logback.classic.Level;
/**
* The Java half of our wrapper for invoking the TypeScript parser.
@@ -401,6 +407,21 @@ public class TypeScriptParser {
checkResponseType(response, "ok");
}
/**
* Converts a map to an array of [key, value] pairs.
*/
private JsonArray mapToArray(Map<String, Path> map) {
JsonArray result = new JsonArray();
map.forEach(
(key, path) -> {
JsonArray entry = new JsonArray();
entry.add(key);
entry.add(path.toString());
result.add(entry);
});
return result;
}
/**
* Opens a new project based on a tsconfig.json file. The compiler will analyze all files in the
* project.
@@ -409,10 +430,15 @@ public class TypeScriptParser {
*
* <p>Only one project should be opened at once.
*/
public ParsedProject openProject(File tsConfigFile) {
public ParsedProject openProject(File tsConfigFile, DependencyInstallationResult deps) {
JsonObject request = new JsonObject();
request.add("command", new JsonPrimitive("open-project"));
request.add("tsConfig", new JsonPrimitive(tsConfigFile.getPath()));
request.add("packageEntryPoints", mapToArray(deps.getPackageEntryPoints()));
request.add("packageJsonFiles", mapToArray(deps.getPackageJsonFiles()));
request.add("virtualSourceRoot", deps.getVirtualSourceRoot() == null
? JsonNull.INSTANCE
: new JsonPrimitive(deps.getVirtualSourceRoot().toString()));
JsonObject response = talkToParserWrapper(request);
try {
checkResponseType(response, "project-opened");