/*
* Copyright 2013-2016 Sergey Ignatov, Alexander Zolotov, Florin Patan
*
* 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.
*/
package com.goide.sdk;
import com.goide.GoConstants;
import com.goide.GoEnvironmentUtil;
import com.goide.appengine.YamlFilesModificationTracker;
import com.goide.project.GoApplicationLibrariesService;
import com.goide.project.GoLibrariesService;
import com.goide.psi.GoFile;
import com.intellij.execution.configurations.PathEnvironmentVariableUtil;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.UserDataHolder;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.*;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.CachedValueProvider;
import com.intellij.psi.util.CachedValuesManager;
import com.intellij.util.Function;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.text.VersionComparatorUtil;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.intellij.util.containers.ContainerUtil.newLinkedHashSet;
public class GoSdkUtil {
private static final Pattern GO_VERSION_PATTERN = Pattern.compile("[tT]heVersion\\s*=\\s*`go([\\d.]+\\w+(\\d+)?)`");
private static final Pattern GAE_VERSION_PATTERN = Pattern.compile("[tT]heVersion\\s*=\\s*`go([\\d.]+)( \\(appengine-[\\d.]+\\))?`");
private static final Pattern GO_DEVEL_VERSION_PATTERN = Pattern.compile("[tT]heVersion\\s*=\\s*`(devel.*)`");
private static final Key<String> ZVERSION_DATA_KEY = Key.create("GO_ZVERSION_KEY");
private GoSdkUtil() {}
@Nullable
public static VirtualFile getSdkSrcDir(@NotNull Project project, @Nullable Module module) {
if (module != null) {
return CachedValuesManager.getManager(project).getCachedValue(module, () -> {
GoSdkService sdkService = GoSdkService.getInstance(module.getProject());
return CachedValueProvider.Result.create(getInnerSdkSrcDir(sdkService, module), sdkService);
});
}
return CachedValuesManager.getManager(project).getCachedValue(project, () -> {
GoSdkService sdkService = GoSdkService.getInstance(project);
return CachedValueProvider.Result.create(getInnerSdkSrcDir(sdkService, null), sdkService);
});
}
@Nullable
private static VirtualFile getInnerSdkSrcDir(@NotNull GoSdkService sdkService, @Nullable Module module) {
String sdkHomePath = sdkService.getSdkHomePath(module);
String sdkVersionString = sdkService.getSdkVersion(module);
return sdkHomePath != null && sdkVersionString != null ? getSdkSrcDir(sdkHomePath, sdkVersionString) : null;
}
@Nullable
private static VirtualFile getSdkSrcDir(@NotNull String sdkPath, @NotNull String sdkVersion) {
String srcPath = getSrcLocation(sdkVersion);
VirtualFile file = VirtualFileManager.getInstance().findFileByUrl(VfsUtilCore.pathToUrl(FileUtil.join(sdkPath, srcPath)));
return file != null && file.isDirectory() ? file : null;
}
@Nullable
public static GoFile findBuiltinFile(@NotNull PsiElement context) {
Project project = context.getProject();
// it's important to ask module on file, otherwise module won't be found for elements in libraries files [zolotov]
Module moduleFromContext = ModuleUtilCore.findModuleForPsiElement(context.getContainingFile());
if (moduleFromContext == null) {
for (Module module : ModuleManager.getInstance(project).getModules()) {
if (GoSdkService.getInstance(project).isGoModule(module)) {
moduleFromContext = module;
break;
}
}
}
Module module = moduleFromContext;
UserDataHolder holder = ObjectUtils.notNull(module, project);
VirtualFile file = CachedValuesManager.getManager(context.getProject()).getCachedValue(holder, () -> {
VirtualFile sdkSrcDir = getSdkSrcDir(project, module);
VirtualFile result = sdkSrcDir != null ? sdkSrcDir.findFileByRelativePath(GoConstants.BUILTIN_FILE_PATH) : null;
return CachedValueProvider.Result.create(result, getSdkAndLibrariesCacheDependencies(project, module, result));
});
if (file == null) return null;
PsiFile psiBuiltin = context.getManager().findFile(file);
return psiBuiltin instanceof GoFile ? (GoFile)psiBuiltin : null;
}
@Nullable
public static VirtualFile findExecutableInGoPath(@NotNull String executableName, @NotNull Project project, @Nullable Module module) {
executableName = GoEnvironmentUtil.getBinaryFileNameForPath(executableName);
Collection<VirtualFile> roots = getGoPathRoots(project, module);
for (VirtualFile file : roots) {
VirtualFile child = VfsUtil.findRelativeFile(file, "bin", executableName);
if (child != null) return child;
}
File fromPath = PathEnvironmentVariableUtil.findInPath(executableName);
return fromPath != null ? VfsUtil.findFileByIoFile(fromPath, true) : null;
}
/**
* @return concatenation of {@link this#getSdkSrcDir(Project, Module)} and {@link this#getGoPathSources(Project, Module)}
*/
@NotNull
public static LinkedHashSet<VirtualFile> getSourcesPathsToLookup(@NotNull Project project, @Nullable Module module) {
LinkedHashSet<VirtualFile> sdkAndGoPath = newLinkedHashSet();
ContainerUtil.addIfNotNull(sdkAndGoPath, getSdkSrcDir(project, module));
ContainerUtil.addAllNotNull(sdkAndGoPath, getGoPathSources(project, module));
return sdkAndGoPath;
}
@NotNull
public static LinkedHashSet<VirtualFile> getVendoringAwareSourcesPathsToLookup(@NotNull Project project,
@Nullable Module module,
@Nullable VirtualFile contextFile) {
LinkedHashSet<VirtualFile> sdkAndGoPath = getSourcesPathsToLookup(project, module);
if (contextFile != null) {
Collection<VirtualFile> vendorDirectories = collectVendorDirectories(contextFile, sdkAndGoPath);
if (!vendorDirectories.isEmpty()) {
LinkedHashSet<VirtualFile> result = newLinkedHashSet(vendorDirectories);
result.addAll(sdkAndGoPath);
return result;
}
}
return sdkAndGoPath;
}
@NotNull
private static Collection<VirtualFile> collectVendorDirectories(@Nullable VirtualFile contextDirectory,
@NotNull Set<VirtualFile> sourceRoots) {
if (contextDirectory == null) {
return Collections.emptyList();
}
Collection<VirtualFile> vendorDirectories = ContainerUtil.newArrayList();
VirtualFile directory = contextDirectory;
while (directory != null) {
VirtualFile vendorDirectory = directory.findChild(GoConstants.VENDOR);
if (vendorDirectory != null && vendorDirectory.isDirectory()) {
vendorDirectories.add(vendorDirectory);
}
if (sourceRoots.contains(directory)) {
break;
}
directory = directory.getParent();
}
return vendorDirectories;
}
@NotNull
private static Collection<VirtualFile> getGoPathRoots(@NotNull Project project, @Nullable Module module) {
Collection<VirtualFile> roots = ContainerUtil.newArrayList();
if (GoApplicationLibrariesService.getInstance().isUseGoPathFromSystemEnvironment()) {
roots.addAll(getGoPathsRootsFromEnvironment());
}
roots.addAll(module != null ? GoLibrariesService.getUserDefinedLibraries(module) : GoLibrariesService.getUserDefinedLibraries(project));
return roots;
}
@NotNull
public static Collection<VirtualFile> getGoPathSources(@NotNull Project project, @Nullable Module module) {
if (module != null) {
return CachedValuesManager.getManager(project).getCachedValue(module, () -> {
Collection<VirtualFile> result = newLinkedHashSet();
Project project1 = module.getProject();
GoSdkService sdkService = GoSdkService.getInstance(project1);
if (sdkService.isAppEngineSdk(module)) {
ContainerUtil.addAllNotNull(result, ContainerUtil.mapNotNull(YamlFilesModificationTracker.getYamlFiles(project1, module),
VirtualFile::getParent));
}
result.addAll(getInnerGoPathSources(project1, module));
return CachedValueProvider.Result
.create(result, getSdkAndLibrariesCacheDependencies(project1, module, YamlFilesModificationTracker.getInstance(project1)));
});
}
return CachedValuesManager.getManager(project).getCachedValue(project,
(CachedValueProvider<Collection<VirtualFile>>)() -> CachedValueProvider.Result
.create(getInnerGoPathSources(project, null),
getSdkAndLibrariesCacheDependencies(project, null, YamlFilesModificationTracker.getInstance(project))));
}
@NotNull
private static List<VirtualFile> getInnerGoPathSources(@NotNull Project project, @Nullable Module module) {
return ContainerUtil.mapNotNull(getGoPathRoots(project, module), new RetrieveSubDirectoryOrSelfFunction("src"));
}
@NotNull
private static Collection<VirtualFile> getGoPathBins(@NotNull Project project, @Nullable Module module) {
Collection<VirtualFile> result = newLinkedHashSet(ContainerUtil.mapNotNull(getGoPathRoots(project, module),
new RetrieveSubDirectoryOrSelfFunction("bin")));
String executableGoPath = GoSdkService.getInstance(project).getGoExecutablePath(module);
if (executableGoPath != null) {
VirtualFile executable = VirtualFileManager.getInstance().findFileByUrl(VfsUtilCore.pathToUrl(executableGoPath));
if (executable != null) ContainerUtil.addIfNotNull(result, executable.getParent());
}
return result;
}
/**
* Retrieves root directories from GOPATH env-variable.
* This method doesn't consider user defined libraries,
* for that case use {@link {@link this#getGoPathRoots(Project, Module)}
*/
@NotNull
public static Collection<VirtualFile> getGoPathsRootsFromEnvironment() {
return GoEnvironmentGoPathModificationTracker.getGoEnvironmentGoPathRoots();
}
@NotNull
public static String retrieveGoPath(@NotNull Project project, @Nullable Module module) {
return StringUtil.join(ContainerUtil.map(getGoPathRoots(project, module), VirtualFile::getPath), File.pathSeparator);
}
@NotNull
public static String retrieveEnvironmentPathForGo(@NotNull Project project, @Nullable Module module) {
return StringUtil.join(ContainerUtil.map(getGoPathBins(project, module), VirtualFile::getPath), File.pathSeparator);
}
@NotNull
private static String getSrcLocation(@NotNull String version) {
if (ApplicationManager.getApplication().isUnitTestMode()) {
return "src/pkg";
}
if (version.startsWith("devel")) {
return "src";
}
if (version.length() > 2 && StringUtil.parseDouble(version.substring(0, 3), 1.4) < 1.4) {
return "src/pkg";
}
return "src";
}
public static int compareVersions(@NotNull String lhs, @NotNull String rhs) {
return VersionComparatorUtil.compare(lhs, rhs);
}
@Nullable
@Contract("null, _ -> null")
public static String getImportPath(@Nullable PsiDirectory psiDirectory, boolean withVendoring) {
if (psiDirectory == null) {
return null;
}
return CachedValuesManager.getCachedValue(psiDirectory, withVendoring ? new CachedVendoredImportPathProvider(psiDirectory)
: new CachedImportPathProviderImpl(psiDirectory));
}
@Nullable
private static String getPathRelativeToSdkAndLibrariesAndVendor(@NotNull PsiDirectory psiDirectory, boolean withVendoring) {
VirtualFile file = psiDirectory.getVirtualFile();
Project project = psiDirectory.getProject();
Module module = ModuleUtilCore.findModuleForPsiElement(psiDirectory);
LinkedHashSet<VirtualFile> sourceRoots = withVendoring ? getVendoringAwareSourcesPathsToLookup(project, module, file)
: getSourcesPathsToLookup(project, module);
String relativePath = getRelativePathToRoots(file, sourceRoots);
if (relativePath != null) {
return relativePath;
}
String filePath = file.getPath();
int src = filePath.lastIndexOf("/src/");
if (src > -1) {
return filePath.substring(src + 5);
}
return null;
}
@Nullable
public static String getRelativePathToRoots(@NotNull VirtualFile file, @NotNull Collection<VirtualFile> sourceRoots) {
for (VirtualFile root : sourceRoots) {
String relativePath = VfsUtilCore.getRelativePath(file, root, '/');
if (StringUtil.isNotEmpty(relativePath)) {
return relativePath;
}
}
return null;
}
@Nullable
public static VirtualFile suggestSdkDirectory() {
if (SystemInfo.isWindows) {
return ObjectUtils.chooseNotNull(LocalFileSystem.getInstance().findFileByPath("C:\\Go"),
LocalFileSystem.getInstance().findFileByPath("C:\\cygwin"));
}
if (SystemInfo.isMac || SystemInfo.isLinux) {
String fromEnv = suggestSdkDirectoryPathFromEnv();
if (fromEnv != null) {
return LocalFileSystem.getInstance().findFileByPath(fromEnv);
}
VirtualFile usrLocal = LocalFileSystem.getInstance().findFileByPath("/usr/local/go");
if (usrLocal != null) return usrLocal;
}
if (SystemInfo.isMac) {
String macPorts = "/opt/local/lib/go";
String homeBrew = "/usr/local/Cellar/go";
File file = FileUtil.findFirstThatExist(macPorts, homeBrew);
if (file != null) {
return LocalFileSystem.getInstance().findFileByIoFile(file);
}
}
return null;
}
@Nullable
private static String suggestSdkDirectoryPathFromEnv() {
File fileFromPath = PathEnvironmentVariableUtil.findInPath("go");
if (fileFromPath != null) {
File canonicalFile;
try {
canonicalFile = fileFromPath.getCanonicalFile();
String path = canonicalFile.getPath();
if (path.endsWith("bin/go")) {
return StringUtil.trimEnd(path, "bin/go");
}
}
catch (IOException ignore) {
}
}
return null;
}
@Nullable
public static String parseGoVersion(@NotNull String text) {
Matcher matcher = GO_VERSION_PATTERN.matcher(text);
if (matcher.find()) {
return matcher.group(1);
}
matcher = GAE_VERSION_PATTERN.matcher(text);
if (matcher.find()) {
return matcher.group(1) + matcher.group(2);
}
matcher = GO_DEVEL_VERSION_PATTERN.matcher(text);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
@Nullable
public static String retrieveGoVersion(@NotNull String sdkPath) {
try {
VirtualFile sdkRoot = VirtualFileManager.getInstance().findFileByUrl(VfsUtilCore.pathToUrl(sdkPath));
if (sdkRoot != null) {
String cachedVersion = sdkRoot.getUserData(ZVERSION_DATA_KEY);
if (cachedVersion != null) {
return !cachedVersion.isEmpty() ? cachedVersion : null;
}
VirtualFile versionFile = sdkRoot.findFileByRelativePath("src/" + GoConstants.GO_VERSION_NEW_FILE_PATH);
if (versionFile == null) {
versionFile = sdkRoot.findFileByRelativePath("src/" + GoConstants.GO_VERSION_FILE_PATH);
}
if (versionFile == null) {
versionFile = sdkRoot.findFileByRelativePath("src/pkg/" + GoConstants.GO_VERSION_FILE_PATH);
}
if (versionFile != null) {
String text = VfsUtilCore.loadText(versionFile);
String version = parseGoVersion(text);
if (version == null) {
GoSdkService.LOG.debug("Cannot retrieve go version from zVersion file: " + text);
}
sdkRoot.putUserData(ZVERSION_DATA_KEY, StringUtil.notNullize(version));
return version;
}
else {
GoSdkService.LOG.debug("Cannot find go version file in sdk path: " + sdkPath);
}
}
}
catch (IOException e) {
GoSdkService.LOG.debug("Cannot retrieve go version from sdk path: " + sdkPath, e);
}
return null;
}
@NotNull
public static String adjustSdkPath(@NotNull String path) {
if (new File(path, GoConstants.LIB_EXEC_DIRECTORY).exists()) {
path += File.separatorChar + GoConstants.LIB_EXEC_DIRECTORY;
}
File possibleGCloudSdk = new File(path, GoConstants.GCLOUD_APP_ENGINE_DIRECTORY_PATH);
if (possibleGCloudSdk.exists() && possibleGCloudSdk.isDirectory()) {
if (isAppEngine(possibleGCloudSdk.getAbsolutePath())) {
return possibleGCloudSdk.getAbsolutePath() + GoConstants.APP_ENGINE_GO_ROOT_DIRECTORY_PATH;
}
}
return isAppEngine(path) ? path + GoConstants.APP_ENGINE_GO_ROOT_DIRECTORY_PATH : path;
}
private static boolean isAppEngine(@NotNull String path) {
return new File(path, GoConstants.APP_ENGINE_MARKER_FILE).exists();
}
@NotNull
public static Collection<VirtualFile> getSdkDirectoriesToAttach(@NotNull String sdkPath, @NotNull String versionString) {
// scr is enough at the moment, possible process binaries from pkg
return ContainerUtil.createMaybeSingletonList(getSdkSrcDir(sdkPath, versionString));
}
@NotNull
private static Collection<Object> getSdkAndLibrariesCacheDependencies(@NotNull Project project,
@Nullable Module module,
Object... extra) {
Collection<Object> dependencies = ContainerUtil.newArrayList((Object[])GoLibrariesService.getModificationTrackers(project, module));
ContainerUtil.addAllNotNull(dependencies, GoSdkService.getInstance(project));
ContainerUtil.addAllNotNull(dependencies, extra);
return dependencies;
}
@NotNull
public static Collection<Module> getGoModules(@NotNull Project project) {
if (project.isDefault()) return Collections.emptyList();
GoSdkService sdkService = GoSdkService.getInstance(project);
return ContainerUtil.filter(ModuleManager.getInstance(project).getModules(), sdkService::isGoModule);
}
public static boolean isUnreachableInternalPackage(@NotNull VirtualFile targetDirectory,
@NotNull VirtualFile referenceContextFile,
@NotNull Set<VirtualFile> sourceRoots) {
return isUnreachablePackage(GoConstants.INTERNAL, targetDirectory, referenceContextFile, sourceRoots);
}
public static boolean isUnreachableVendoredPackage(@NotNull VirtualFile targetDirectory,
@NotNull VirtualFile referenceContextFile,
@NotNull Set<VirtualFile> sourceRoots) {
return isUnreachablePackage(GoConstants.VENDOR, targetDirectory, referenceContextFile, sourceRoots);
}
@Nullable
public static VirtualFile findParentDirectory(@Nullable VirtualFile file, @NotNull Set<VirtualFile> sourceRoots, @NotNull String name) {
if (file == null) {
return null;
}
VirtualFile currentFile = file.isDirectory() ? file : file.getParent();
while (currentFile != null && !sourceRoots.contains(currentFile)) {
if (currentFile.isDirectory() && name.equals(currentFile.getName())) {
return currentFile;
}
currentFile = currentFile.getParent();
}
return null;
}
private static boolean isUnreachablePackage(@NotNull String unreachableDirectoryName,
@NotNull VirtualFile targetDirectory,
@NotNull VirtualFile referenceContextFile,
@NotNull Set<VirtualFile> sourceRoots) {
VirtualFile directory = findParentDirectory(targetDirectory, sourceRoots, unreachableDirectoryName);
VirtualFile parent = directory != null ? directory.getParent() : null;
return directory != null && !VfsUtilCore.isAncestor(parent, referenceContextFile, false);
}
private static class RetrieveSubDirectoryOrSelfFunction implements Function<VirtualFile, VirtualFile> {
@NotNull private final String mySubdirName;
public RetrieveSubDirectoryOrSelfFunction(@NotNull String subdirName) {
mySubdirName = subdirName;
}
@Override
public VirtualFile fun(VirtualFile file) {
return file == null || FileUtil.namesEqual(mySubdirName, file.getName()) ? file : file.findChild(mySubdirName);
}
}
private abstract static class CachedImportPathProvider implements CachedValueProvider<String> {
private final PsiDirectory myPsiDirectory;
private final boolean myWithVendoring;
public CachedImportPathProvider(@NotNull PsiDirectory psiDirectory, boolean vendoring) {
myPsiDirectory = psiDirectory;
myWithVendoring = vendoring;
}
@Nullable
@Override
public Result<String> compute() {
String path = getPathRelativeToSdkAndLibrariesAndVendor(myPsiDirectory, myWithVendoring);
Module module = ModuleUtilCore.findModuleForPsiElement(myPsiDirectory);
return Result.create(path, getSdkAndLibrariesCacheDependencies(myPsiDirectory.getProject(), module, myPsiDirectory));
}
}
private static class CachedImportPathProviderImpl extends CachedImportPathProvider {
public CachedImportPathProviderImpl(@NotNull PsiDirectory psiDirectory) {
super(psiDirectory, false);
}
}
private static class CachedVendoredImportPathProvider extends CachedImportPathProvider {
public CachedVendoredImportPathProvider(@NotNull PsiDirectory psiDirectory) {
super(psiDirectory, true);
}
}
}