Skip to content

Commit 4dc7d01

Browse files
committed
Show shortest paths to dependencies in Dependencies view tooltips
Fixes #3095.
1 parent 7e7f89d commit 4dc7d01

3 files changed

Lines changed: 144 additions & 23 deletions

File tree

‎src/extension/views/packages_view.ts‎

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as vs from "vscode";
44
import { DartCapabilities } from "../../shared/capabilities/dart";
55
import { DART_DEP_FILE_NODE_CONTEXT, DART_DEP_FOLDER_NODE_CONTEXT, DART_DEP_PACKAGE_NODE_CONTEXT, DART_DEP_PROJECT_NODE_CONTEXT } from "../../shared/constants";
66
import { DartWorkspaceContext, Logger } from "../../shared/interfaces";
7-
import { PubDeps } from "../../shared/pub/deps";
7+
import { PubDeps, PubDepsPackage, ShortestPaths } from "../../shared/pub/deps";
88
import { PackageMap } from "../../shared/pub/package_map";
99
import { sortBy } from "../../shared/utils/array";
1010
import { areSameFolder, fsPath } from "../../shared/utils/fs";
@@ -50,23 +50,35 @@ export class DartPackagesProvider implements vs.TreeDataProvider<PackageDep> {
5050
: nodes;
5151

5252
} else if (element instanceof PackageDepProject) {
53-
const allPackages = await this.getPackages(element);
54-
if (!this.dartCapabilities.supportsPubDepsJson)
53+
// Try getting dependencies from `pub deps`.
54+
let packages: { [key: string]: PubDepsPackage } | undefined;
55+
let shortestPaths: ShortestPaths | undefined;
56+
if (this.dartCapabilities.supportsPubDepsJson) {
57+
// Fetch dependencies with "pub deps --json".
58+
const root = await this.deps.getRootDependency(element.projectFolder);
59+
packages = root ? this.deps.getPackageMap(root) : undefined;
60+
shortestPaths = packages ? this.deps.computeShortestPaths(packages) : undefined;
61+
}
62+
63+
// Get packages from package file.
64+
const allPackages = await this.getPackages(element, shortestPaths);
65+
66+
if (!packages)
5567
return allPackages;
5668

57-
// If we support "pub deps --json" split the packages into groups.
58-
const packageKinds = await this.deps.getDependencyKinds(element.projectFolder);
59-
const directPackages = allPackages.filter((p) => packageKinds[p.packageName] === "direct");
60-
const devPackages = allPackages.filter((p) => packageKinds[p.packageName] === "dev");
61-
const transitivePackages = allPackages.filter((p) => packageKinds[p.packageName] === "transitive");
69+
// Split the packages into groups.
70+
const directPackages = allPackages.filter((p) => packages![p.packageName]?.kind === "direct");
71+
const devPackages = allPackages.filter((p) => packages![p.packageName]?.kind === "dev");
72+
const transitivePackages = allPackages.filter((p) => packages![p.packageName]?.kind === "transitive");
73+
6274

6375
const nodes: PackageDepProjectPackageGroup[] = [];
6476
if (directPackages.length)
6577
nodes.push(new PackageDepProjectPackageGroup("direct dependencies", directPackages));
6678
if (devPackages.length)
6779
nodes.push(new PackageDepProjectPackageGroup("dev dependencies", devPackages));
6880
if (transitivePackages.length)
69-
nodes.push(new PackageDepProjectPackageGroup("transitive dependencies", transitivePackages));
81+
nodes.push(new PackageDepProjectPackageGroup("transitive dependencies", transitivePackages, shortestPaths));
7082
return nodes;
7183
} else if (element instanceof PackageDepProjectPackageGroup) {
7284
// For the package groups, we've already computed the children when we split
@@ -84,7 +96,7 @@ export class DartPackagesProvider implements vs.TreeDataProvider<PackageDep> {
8496
}
8597
}
8698

87-
private async getPackages(project: PackageDepProject): Promise<PackageDepPackage[]> {
99+
private async getPackages(project: PackageDepProject, shortestPaths: ShortestPaths | undefined): Promise<PackageDepPackage[]> {
88100
const map = PackageMap.loadForProject(this.logger, project.projectFolder);
89101
const packages = map.packages;
90102
const packageNames = sortBy(Object.keys(packages), (s) => s.toLowerCase());
@@ -95,7 +107,8 @@ export class DartPackagesProvider implements vs.TreeDataProvider<PackageDep> {
95107
let packagePath = packages[name];
96108
if (path.basename(packagePath) === "lib")
97109
packagePath = path.normalize(path.join(packagePath, ".."));
98-
return new PackageDepPackage(`${name}`, vs.Uri.file(packagePath));
110+
const shortestPath = shortestPaths ? shortestPaths[name] : undefined;
111+
return new PackageDepPackage(`${name}`, vs.Uri.file(packagePath), shortestPath);
99112
});
100113

101114
return packageDepNodes;
@@ -186,6 +199,7 @@ export class PackageDepProjectPackageGroup extends PackageDep {
186199
constructor(
187200
label: string,
188201
public readonly packages: PackageDepPackage[],
202+
public readonly shortestPaths?: ShortestPaths,
189203
) {
190204
super(label, undefined, vs.TreeItemCollapsibleState.Collapsed);
191205
this.contextValue = DART_DEP_PACKAGE_NODE_CONTEXT;
@@ -196,8 +210,12 @@ export class PackageDepPackage extends PackageDep {
196210
constructor(
197211
public readonly packageName: string,
198212
resourceUri: vs.Uri,
213+
shortestPath: string[] | undefined,
199214
) {
200215
super(packageName, resourceUri, vs.TreeItemCollapsibleState.Collapsed);
201216
this.contextValue = DART_DEP_PACKAGE_NODE_CONTEXT;
217+
218+
if (shortestPath)
219+
this.tooltip = shortestPath.join(" → ");
202220
}
203221
}

‎src/shared/pub/deps.ts‎

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ export type DependencyType = "root" | "direct" | "dev" | "transitive";
1010
export class PubDeps {
1111
constructor(private readonly logger: Logger, private readonly sdks: DartSdks, private readonly dartCapabilities: DartCapabilities) { }
1212

13-
public async getDependencyKinds(projectDirectory: string): Promise<{ [key: string]: DependencyType }> {
13+
public async getRootDependency(projectDirectory: string): Promise<PubDepsJson | undefined> {
1414
if (!this.dartCapabilities.supportsPubDepsJson) {
15-
return {};
15+
return undefined;
1616
}
1717

1818
const binPath = this.sdks.flutter
@@ -22,30 +22,75 @@ export class PubDeps {
2222

2323
if (result.exitCode !== 0) {
2424
this.logger.error(`Running "pub deps --json" returned exit code ${result.exitCode}:\n${result.stdout}\n${result.stderr}`);
25-
return {};
25+
return undefined;
2626
}
2727

2828
try {
29-
const root = JSON.parse(result.stdout) as PubDepsJson;
30-
const packages: { [key: string]: DependencyType } = {};
31-
for (const p of root.packages) {
32-
packages[p.name] = p.kind;
33-
}
34-
return packages;
29+
return JSON.parse(result.stdout) as PubDepsJson;
3530
} catch (e) {
3631
this.logger.error(`"pub deps --json" returned invalid JSON ${e}:\n${result.stdout}`);
37-
return {};
32+
return undefined;
33+
}
34+
}
35+
36+
public getPackageMap(root: PubDepsJson): { [key: string]: PubDepsPackage } {
37+
const packages: { [key: string]: PubDepsPackage } = {};
38+
for (const p of root.packages) {
39+
packages[p.name] = p;
40+
}
41+
return packages;
42+
}
43+
44+
public computeShortestPaths(packageMap: { [key: string]: PubDepsPackage; }): ShortestPaths {
45+
const results: ShortestPaths = {};
46+
const rootName = Object.values(packageMap).find((p) => p.kind === "root")?.name;
47+
// Queue is a list of pairs of packages to process, and the paths to get to them.
48+
const queue: Array<[string, string[]]> = [];
49+
for (const name of Object.keys(packageMap)) {
50+
if (packageMap[name]?.kind === "direct" || packageMap[name]?.kind === "dev") {
51+
const path = rootName ? [rootName, name] : [name];
52+
results[name] = path;
53+
queue.push([name, path]);
54+
}
55+
}
56+
57+
// Traverse the tree breadth-first, so that the first time we come across any node, we know that is
58+
// the shortest path.
59+
60+
// eslint-disable-next-line @typescript-eslint/prefer-for-of
61+
for (let i = 0; i < queue.length; i++) {
62+
const [pkg, path] = queue[i];
63+
const dependencies = packageMap[pkg]?.dependencies ?? [];
64+
65+
// Loop over this packages dependencies, and if we've not previously been to them
66+
// this is (one of) the shortest paths there.
67+
for (const dep of dependencies) {
68+
if (results[dep])
69+
continue;
70+
71+
const newPath = [...path, dep];
72+
results[dep] = newPath;
73+
74+
// Also push the dependency onto the queue to process its dependencies.
75+
queue.push([dep, newPath]);
76+
}
3877
}
78+
79+
return results;
3980
}
81+
4082
}
4183

42-
interface PubDepsJson {
84+
export interface PubDepsJson {
4385
root: string;
4486
packages: PubDepsPackage[];
4587
}
4688

47-
interface PubDepsPackage {
89+
export interface PubDepsPackage {
4890
name: string;
4991
version: string;
5092
kind: DependencyType;
93+
dependencies: string[] | undefined;
5194
}
95+
96+
export interface ShortestPaths { [key: string]: string[] }

‎src/test/dart/pub/deps.test.ts‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { strict as assert } from "assert";
2+
import { DartSdks } from "../../../shared/interfaces";
3+
import { PubDeps, PubDepsJson } from "../../../shared/pub/deps";
4+
import { activate, extApi } from "../../helpers";
5+
6+
describe("pub deps", () => {
7+
beforeEach("activate", () => activate(null));
8+
9+
it("can compute shortest paths", async () => {
10+
const deps = new PubDeps(extApi.logger, extApi.workspaceContext.sdks as DartSdks, extApi.dartCapabilities);
11+
12+
const fakeRoot: PubDepsJson = {
13+
packages: [
14+
{ name: "my_package", version: "1.2.3", kind: "root", dependencies: ["direct_1", "direct_2", "direct_3", "dev_1"] },
15+
{ name: "direct_1", version: "1.2.3", kind: "direct", dependencies: [] },
16+
{ name: "direct_2", version: "1.2.3", kind: "direct", dependencies: ["transitive_1"] },
17+
{ name: "direct_3", version: "1.2.3", kind: "direct", dependencies: ["transitive_2"] },
18+
{ name: "dev_1", version: "1.2.3", kind: "dev", dependencies: [] },
19+
{ name: "transitive_1", version: "1.2.3", kind: "transitive", dependencies: ["transitive_2"] },
20+
{ name: "transitive_2", version: "1.2.3", kind: "transitive", dependencies: ["transitive_3"] },
21+
{ name: "transitive_3", version: "1.2.3", kind: "transitive", dependencies: ["transitive_1"] },
22+
],
23+
root: "my_package",
24+
};
25+
26+
const packageMap = deps.getPackageMap(fakeRoot);
27+
const shortestPaths = deps.computeShortestPaths(packageMap);
28+
assert.deepEqual(shortestPaths, {
29+
"dev_1": ["my_package", "dev_1"],
30+
"direct_1": ["my_package", "direct_1"],
31+
"direct_2": ["my_package", "direct_2"],
32+
"direct_3": ["my_package", "direct_3"],
33+
"transitive_1": ["my_package", "direct_2", "transitive_1"],
34+
"transitive_2": ["my_package", "direct_3", "transitive_2"],
35+
"transitive_3": ["my_package", "direct_3", "transitive_2", "transitive_3"],
36+
});
37+
});
38+
39+
it("does not fail on circular dependencies", async () => {
40+
const deps = new PubDeps(extApi.logger, extApi.workspaceContext.sdks as DartSdks, extApi.dartCapabilities);
41+
42+
const fakeRoot: PubDepsJson = {
43+
packages: [
44+
{ name: "my_package", version: "1.2.3", kind: "root", dependencies: ["direct_1"] },
45+
{ name: "direct_1", version: "1.2.3", kind: "direct", dependencies: ["transitive_1"] },
46+
{ name: "transitive_1", version: "1.2.3", kind: "transitive", dependencies: ["direct_1"] },
47+
],
48+
root: "my_package",
49+
};
50+
51+
const packageMap = deps.getPackageMap(fakeRoot);
52+
const shortestPaths = deps.computeShortestPaths(packageMap);
53+
assert.deepEqual(shortestPaths, {
54+
"direct_1": ["my_package", "direct_1"],
55+
"transitive_1": ["my_package", "direct_1", "transitive_1"],
56+
});
57+
});
58+
});

0 commit comments

Comments
 (0)