blob: 20eefefce96e2aa8ec51f729d3072db9d7b2522e [file] [log] [blame]
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for license information.
*--------------------------------------------------------*/
import cp = require('child_process');
import path = require('path');
import vscode = require('vscode');
import { toolExecutionEnvironment } from './goEnv';
import { promptForMissingTool, promptForUpdatingTool } from './goInstallTools';
import { getBinPath, getCurrentGoPath, getGoVersion, isVendorSupported } from './util';
import { envPath, fixDriveCasingInWindows, getCurrentGoRoot, getCurrentGoWorkspaceFromGOPATH } from './utils/goPath';
type GopkgsDone = (res: Map<string, PackageInfo>) => void;
interface Cache {
entry: Map<string, PackageInfo>;
lastHit: number;
}
export interface PackageInfo {
name: string;
isStd: boolean;
}
let gopkgsNotified: boolean = false;
let cacheTimeout: number = 5000;
const gopkgsSubscriptions: Map<string, GopkgsDone[]> = new Map<string, GopkgsDone[]>();
const gopkgsRunning: Set<string> = new Set<string>();
const allPkgsCache: Map<string, Cache> = new Map<string, Cache>();
const pkgRootDirs = new Map<string, string>();
function gopkgs(workDir?: string): Promise<Map<string, PackageInfo>> {
const gopkgsBinPath = getBinPath('gopkgs');
if (!path.isAbsolute(gopkgsBinPath)) {
promptForMissingTool('gopkgs');
return Promise.resolve(new Map<string, PackageInfo>());
}
const t0 = Date.now();
return new Promise<Map<string, PackageInfo>>((resolve, reject) => {
const args = ['-format', '{{.Name}};{{.ImportPath}};{{.Dir}}'];
if (workDir) {
args.push('-workDir', workDir);
}
const cmd = cp.spawn(gopkgsBinPath, args, { env: toolExecutionEnvironment() });
const chunks: any[] = [];
const errchunks: any[] = [];
let err: any;
cmd.stdout.on('data', (d) => chunks.push(d));
cmd.stderr.on('data', (d) => errchunks.push(d));
cmd.on('error', (e) => (err = e));
cmd.on('close', () => {
const pkgs = new Map<string, PackageInfo>();
if (err && err.code === 'ENOENT') {
return promptForMissingTool('gopkgs');
}
const errorMsg = errchunks.join('').trim() || (err && err.message);
if (errorMsg) {
if (errorMsg.startsWith('flag provided but not defined: -workDir')) {
promptForUpdatingTool('gopkgs');
// fallback to gopkgs without -workDir
return gopkgs().then((result) => resolve(result));
}
console.log(
`Running gopkgs failed with "${errorMsg}"\nCheck if you can run \`gopkgs -format {{.Name}};{{.ImportPath}}\` in a terminal successfully.`
);
return resolve(pkgs);
}
const goroot = getCurrentGoRoot();
const output = chunks.join('');
if (output.indexOf(';') === -1) {
// User might be using the old gopkgs tool, prompt to update
promptForUpdatingTool('gopkgs');
output.split('\n').forEach((pkgPath) => {
if (!pkgPath || !pkgPath.trim()) {
return;
}
const index = pkgPath.lastIndexOf('/');
const pkgName = index === -1 ? pkgPath : pkgPath.substr(index + 1);
pkgs.set(pkgPath, {
name: pkgName,
isStd: !pkgPath.includes('.')
});
});
return resolve(pkgs);
}
output.split('\n').forEach((pkgDetail) => {
if (!pkgDetail || !pkgDetail.trim() || pkgDetail.indexOf(';') === -1) {
return;
}
const [pkgName, pkgPath, pkgDir] = pkgDetail.trim().split(';');
pkgs.set(pkgPath, {
name: pkgName,
isStd: goroot === null ? false : pkgDir.startsWith(goroot)
});
});
const timeTaken = Date.now() - t0;
cacheTimeout = timeTaken > 5000 ? timeTaken : 5000;
return resolve(pkgs);
});
});
}
function getAllPackagesNoCache(workDir: string): Promise<Map<string, PackageInfo>> {
return new Promise<Map<string, PackageInfo>>((resolve, reject) => {
// Use subscription style to guard costly/long running invocation
const callback = (pkgMap: Map<string, PackageInfo>) => {
resolve(pkgMap);
};
let subs = gopkgsSubscriptions.get(workDir);
if (!subs) {
subs = [];
gopkgsSubscriptions.set(workDir, subs);
}
subs.push(callback);
// Ensure only single gokpgs running
if (!gopkgsRunning.has(workDir)) {
gopkgsRunning.add(workDir);
gopkgs(workDir).then((pkgMap) => {
gopkgsRunning.delete(workDir);
gopkgsSubscriptions.delete(workDir);
subs.forEach((cb) => cb(pkgMap));
});
}
});
}
/**
* Runs gopkgs
* @argument workDir. The workspace directory of the project.
* @returns Map<string, string> mapping between package import path and package name
*/
export async function getAllPackages(workDir: string): Promise<Map<string, PackageInfo>> {
const cache = allPkgsCache.get(workDir);
const useCache = cache && new Date().getTime() - cache.lastHit < cacheTimeout;
if (useCache) {
cache.lastHit = new Date().getTime();
return Promise.resolve(cache.entry);
}
const pkgs = await getAllPackagesNoCache(workDir);
if (!pkgs || pkgs.size === 0) {
if (!gopkgsNotified) {
vscode.window.showInformationMessage(
'Could not find packages. Ensure `gopkgs -format {{.Name}};{{.ImportPath}}` runs successfully.'
);
gopkgsNotified = true;
}
}
allPkgsCache.set(workDir, {
entry: pkgs,
lastHit: new Date().getTime()
});
return pkgs;
}
/**
* Returns mapping of import path and package name for packages that can be imported
* Possible to return empty if useCache options is used.
* @param filePath. Used to determine the right relative path for vendor pkgs
* @param useCache. Force to use cache
* @returns Map<string, string> mapping between package import path and package name
*/
export function getImportablePackages(filePath: string, useCache: boolean = false): Promise<Map<string, PackageInfo>> {
filePath = fixDriveCasingInWindows(filePath);
const fileDirPath = path.dirname(filePath);
let foundPkgRootDir = pkgRootDirs.get(fileDirPath);
const workDir = foundPkgRootDir || fileDirPath;
const cache = allPkgsCache.get(workDir);
const getAllPackagesPromise: Promise<Map<string, PackageInfo>> =
useCache && cache ? Promise.race([getAllPackages(workDir), cache.entry]) : getAllPackages(workDir);
return Promise.all([isVendorSupported(), getAllPackagesPromise]).then(([vendorSupported, pkgs]) => {
const pkgMap = new Map<string, PackageInfo>();
if (!pkgs) {
return pkgMap;
}
const currentWorkspace = getCurrentGoWorkspaceFromGOPATH(getCurrentGoPath(), fileDirPath);
pkgs.forEach((info, pkgPath) => {
if (info.name === 'main') {
return;
}
if (!vendorSupported || !currentWorkspace) {
pkgMap.set(pkgPath, info);
return;
}
if (!foundPkgRootDir) {
// try to guess package root dir
const vendorIndex = pkgPath.indexOf('/vendor/');
if (vendorIndex !== -1) {
foundPkgRootDir = path.join(
currentWorkspace,
pkgPath.substring(0, vendorIndex).replace('/', path.sep)
);
pkgRootDirs.set(fileDirPath, foundPkgRootDir);
}
}
const relativePkgPath = getRelativePackagePath(fileDirPath, currentWorkspace, pkgPath);
if (!relativePkgPath) {
return;
}
const allowToImport = isAllowToImportPackage(fileDirPath, currentWorkspace, relativePkgPath);
if (allowToImport) {
pkgMap.set(relativePkgPath, info);
}
});
return pkgMap;
});
}
/**
* If given pkgPath is not vendor pkg, then the same pkgPath is returned
* Else, the import path for the vendor pkg relative to given filePath is returned.
*/
function getRelativePackagePath(currentFileDirPath: string, currentWorkspace: string, pkgPath: string): string {
let magicVendorString = '/vendor/';
let vendorIndex = pkgPath.indexOf(magicVendorString);
if (vendorIndex === -1) {
magicVendorString = 'vendor/';
if (pkgPath.startsWith(magicVendorString)) {
vendorIndex = 0;
}
}
// Check if current file and the vendor pkg belong to the same root project and not sub vendor
// If yes, then vendor pkg can be replaced with its relative path to the "vendor" folder
// If not, then the vendor pkg should not be allowed to be imported.
if (vendorIndex > -1) {
const rootProjectForVendorPkg = path.join(currentWorkspace, pkgPath.substr(0, vendorIndex));
const relativePathForVendorPkg = pkgPath.substring(vendorIndex + magicVendorString.length);
const subVendor = relativePathForVendorPkg.indexOf('/vendor/') !== -1;
if (relativePathForVendorPkg && currentFileDirPath.startsWith(rootProjectForVendorPkg) && !subVendor) {
return relativePathForVendorPkg;
}
return '';
}
return pkgPath;
}
const pkgToFolderMappingRegex = /ImportPath: (.*) FolderPath: (.*)/;
/**
* Returns mapping between import paths and folder paths for all packages under given folder (vendor will be excluded)
*/
export function getNonVendorPackages(
currentFolderPath: string, recursive: boolean = true): Promise<Map<string, string>> {
const target = recursive ? './...' : '.'; // go list ./... excludes vendor dirs since 1.9
return getImportPathToFolder([target], currentFolderPath);
}
export function getImportPathToFolder(targets: string[], cwd?: string): Promise<Map<string, string>> {
const goRuntimePath = getBinPath('go');
if (!goRuntimePath) {
console.warn(
`Failed to run "go list" to find packages as the "go" binary cannot be found in either GOROOT(${getCurrentGoRoot()}) PATH(${envPath})`
);
return;
}
return new Promise<Map<string, string>>((resolve, reject) => {
const childProcess = cp.spawn(
goRuntimePath,
['list', '-f', 'ImportPath: {{.ImportPath}} FolderPath: {{.Dir}}', ...targets],
{ cwd, env: toolExecutionEnvironment() }
);
const chunks: any[] = [];
childProcess.stdout.on('data', (stdout) => {
chunks.push(stdout);
});
childProcess.on('close', async (status) => {
const lines = chunks.join('').toString().split('\n');
const result = new Map<string, string>();
lines.forEach((line) => {
const matches = line.match(pkgToFolderMappingRegex);
if (!matches || matches.length !== 3) {
return;
}
const [_, pkgPath, folderPath] = matches;
if (!pkgPath) {
return;
}
result.set(pkgPath, folderPath);
});
resolve(result);
});
});
}
// This will check whether it's regular package or internal package
// Regular package will always allowed
// Internal package only allowed if the package doing the import is within the
// tree rooted at the parent of "internal" directory
// see: https://golang.org/doc/go1.4#internalpackages
// see: https://golang.org/s/go14internal
function isAllowToImportPackage(toDirPath: string, currentWorkspace: string, pkgPath: string) {
if (pkgPath.startsWith('internal/')) {
return false;
}
const internalPkgFound = pkgPath.match(/\/internal\/|\/internal$/);
if (internalPkgFound) {
const rootProjectForInternalPkg = path.join(currentWorkspace, pkgPath.substr(0, internalPkgFound.index));
return toDirPath.startsWith(rootProjectForInternalPkg + path.sep) || toDirPath === rootProjectForInternalPkg;
}
return true;
}