Skip to content

Commit 4fe981d

Browse files
authored
Get the direct dependencies while checking for promotion (#952)
* get the direct dependencies * fix comments * Put the projectype checking in the getDirectDependencies && fix tslint errors * Move getDirectDeps into upgradeManager and just check the dependencies in the assessmentManager * update package version * Defer the upgrade scan * fix comments * Fix tslint errors
1 parent 6a9c60a commit 4fe981d

File tree

5 files changed

+257
-72
lines changed

5 files changed

+257
-72
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "vscode-java-dependency",
33
"displayName": "Project Manager for Java",
44
"description": "%description%",
5-
"version": "0.26.5",
5+
"version": "0.27.0",
66
"publisher": "vscjava",
77
"preview": false,
88
"aiKey": "5c642b22-e845-4400-badb-3f8509a70777",

src/syncHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class SyncHandler implements Disposable {
4747

4848
this.disposables.push(workspace.onDidChangeWorkspaceFolders(() => {
4949
this.refresh();
50-
upgradeManager.scan();
50+
setImmediate(() => upgradeManager.scan()); // Deferred
5151
}));
5252

5353
try {

src/upgrade/assessmentManager.ts

Lines changed: 206 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33

4+
import * as fs from 'fs';
45
import * as semver from 'semver';
6+
import { globby } from 'globby';
7+
8+
import { Uri } from 'vscode';
59
import { Jdtls } from "../java/jdtls";
610
import { NodeKind, type INodeData } from "../java/nodeData";
711
import { type DependencyCheckItem, type UpgradeIssue, type PackageDescription, UpgradeReason } from "./type";
@@ -11,6 +15,7 @@ import { buildPackageId } from './utility';
1115
import metadataManager from './metadataManager';
1216
import { sendInfo } from 'vscode-extension-telemetry-wrapper';
1317
import { batchGetCVEIssues } from './cve';
18+
import { ContainerPath } from '../views/containerNode';
1419

1520
function packageNodeToDescription(node: INodeData): PackageDescription | null {
1621
const version = node.metaData?.["maven.version"];
@@ -143,62 +148,238 @@ async function getDependencyIssues(dependencies: PackageDescription[]): Promise<
143148
return issues;
144149
}
145150

146-
async function getProjectIssues(projectNode: INodeData): Promise<UpgradeIssue[]> {
147-
const issues: UpgradeIssue[] = [];
148-
const dependencies = await getAllDependencies(projectNode);
149-
issues.push(...await getCVEIssues(dependencies));
150-
issues.push(...getJavaIssues(projectNode));
151-
issues.push(...await getDependencyIssues(dependencies));
151+
async function getWorkspaceIssues(projectDeps: {projectNode: INodeData, dependencies: PackageDescription[]}[]): Promise<UpgradeIssue[]> {
152152

153+
const issues: UpgradeIssue[] = [];
154+
const dependencyMap: Map<string, PackageDescription> = new Map();
155+
for (const { projectNode, dependencies } of projectDeps) {
156+
issues.push(...getJavaIssues(projectNode));
157+
for (const dep of dependencies) {
158+
const key = `${dep.groupId}:${dep.artifactId}:${dep.version ?? ""}`;
159+
if (!dependencyMap.has(key)) {
160+
dependencyMap.set(key, dep);
161+
}
162+
}
163+
}
164+
const uniqueDependencies = Array.from(dependencyMap.values());
165+
issues.push(...await getCVEIssues(uniqueDependencies));
166+
issues.push(...await getDependencyIssues(uniqueDependencies));
153167
return issues;
168+
}
154169

170+
/**
171+
* Find all pom.xml files in a directory using glob
172+
*/
173+
async function findAllPomFiles(dir: string): Promise<string[]> {
174+
try {
175+
return await globby('**/pom.xml', {
176+
cwd: dir,
177+
absolute: true,
178+
ignore: ['**/node_modules/**', '**/target/**', '**/.git/**', '**/.idea/**', '**/.vscode/**']
179+
});
180+
} catch {
181+
return [];
182+
}
155183
}
156184

157-
async function getWorkspaceIssues(workspaceFolderUri: string): Promise<UpgradeIssue[]> {
158-
const projects = await Jdtls.getProjects(workspaceFolderUri);
159-
const projectsIssues = await Promise.allSettled(projects.map(async (projectNode) => {
160-
const issues = await getProjectIssues(projectNode);
161-
return issues;
162-
}));
185+
/**
186+
* Parse dependencies from a single pom.xml file
187+
*/
188+
function parseDependenciesFromSinglePom(pomPath: string): Set<string> {
189+
// TODO : Use a proper XML parser if needed
190+
const directDeps = new Set<string>();
191+
try {
192+
const pomContent = fs.readFileSync(pomPath, 'utf-8');
193+
194+
// Extract dependencies from <dependencies> section (not inside <dependencyManagement>)
195+
// First, remove dependencyManagement sections to avoid including managed deps
196+
const withoutDepMgmt = pomContent.replace(/<dependencyManagement>[\s\S]*?<\/dependencyManagement>/g, '');
163197

164-
const workspaceIssues = projectsIssues.map(x => {
165-
if (x.status === "fulfilled") {
166-
return x.value;
198+
// Match <dependency> blocks and extract groupId and artifactId
199+
const dependencyRegex = /<dependency>\s*<groupId>([^<]+)<\/groupId>\s*<artifactId>([^<]+)<\/artifactId>/g;
200+
let match = dependencyRegex.exec(withoutDepMgmt);
201+
while (match !== null) {
202+
const groupId = match[1].trim();
203+
const artifactId = match[2].trim();
204+
// Skip property references like ${project.groupId}
205+
if (!groupId.includes('${') && !artifactId.includes('${')) {
206+
directDeps.add(`${groupId}:${artifactId}`);
207+
}
208+
match = dependencyRegex.exec(withoutDepMgmt);
167209
}
210+
} catch {
211+
// If we can't read the pom, return empty set
212+
}
213+
return directDeps;
214+
}
168215

169-
sendInfo("", {
170-
operationName: "java.dependency.assessmentManager.getWorkspaceIssues",
216+
/**
217+
* Parse direct dependencies from all pom.xml files in the project.
218+
* Finds all pom.xml files starting from the project root and parses them to collect dependencies.
219+
*/
220+
async function parseDirectDependenciesFromPom(projectPath: string): Promise<Set<string>> {
221+
const directDeps = new Set<string>();
222+
223+
// Find all pom.xml files in the project starting from the project root
224+
const allPomFiles = await findAllPomFiles(projectPath);
225+
226+
// Parse each pom.xml and collect dependencies
227+
for (const pom of allPomFiles) {
228+
const deps = parseDependenciesFromSinglePom(pom);
229+
deps.forEach(dep => directDeps.add(dep));
230+
}
231+
232+
return directDeps;
233+
}
234+
235+
/**
236+
* Find all Gradle build files in a directory using glob
237+
*/
238+
async function findAllGradleFiles(dir: string): Promise<string[]> {
239+
try {
240+
return await globby('**/{build.gradle,build.gradle.kts}', {
241+
cwd: dir,
242+
absolute: true,
243+
ignore: ['**/node_modules/**', '**/build/**', '**/.git/**', '**/.idea/**', '**/.vscode/**', '**/.gradle/**']
171244
});
245+
} catch {
172246
return [];
173-
}).flat();
247+
}
248+
}
249+
250+
/**
251+
* Parse dependencies from a single Gradle build file
252+
*/
253+
function parseDependenciesFromSingleGradle(gradlePath: string): Set<string> {
254+
const directDeps = new Set<string>();
255+
try {
256+
const gradleContent = fs.readFileSync(gradlePath, 'utf-8');
257+
258+
// Match common dependency configurations:
259+
// implementation 'group:artifact:version'
260+
// implementation "group:artifact:version"
261+
// api 'group:artifact:version'
262+
// compileOnly, runtimeOnly, testImplementation, etc.
263+
const shortFormRegex = /(?:implementation|api|compile|compileOnly|runtimeOnly|testImplementation|testCompileOnly|testRuntimeOnly)\s*\(?['"]([^:'"]+):([^:'"]+)(?::[^'"]*)?['"]\)?/g;
264+
let match = shortFormRegex.exec(gradleContent);
265+
while (match !== null) {
266+
const groupId = match[1].trim();
267+
const artifactId = match[2].trim();
268+
if (!groupId.includes('$') && !artifactId.includes('$')) {
269+
directDeps.add(`${groupId}:${artifactId}`);
270+
}
271+
match = shortFormRegex.exec(gradleContent);
272+
}
273+
274+
// Match map notation: implementation group: 'x', name: 'y', version: 'z'
275+
const mapFormRegex = /(?:implementation|api|compile|compileOnly|runtimeOnly|testImplementation|testCompileOnly|testRuntimeOnly)\s*\(?group:\s*['"]([^'"]+)['"]\s*,\s*name:\s*['"]([^'"]+)['"]/g;
276+
match = mapFormRegex.exec(gradleContent);
277+
while (match !== null) {
278+
const groupId = match[1].trim();
279+
const artifactId = match[2].trim();
280+
if (!groupId.includes('$') && !artifactId.includes('$')) {
281+
directDeps.add(`${groupId}:${artifactId}`);
282+
}
283+
match = mapFormRegex.exec(gradleContent);
284+
}
285+
} catch {
286+
// If we can't read the gradle file, return empty set
287+
}
288+
return directDeps;
289+
}
290+
291+
/**
292+
* Parse direct dependencies from all Gradle build files in the project.
293+
* Finds all build.gradle and build.gradle.kts files and parses them to collect dependencies.
294+
*/
295+
async function parseDirectDependenciesFromGradle(projectPath: string): Promise<Set<string>> {
296+
const directDeps = new Set<string>();
297+
298+
// Find all Gradle build files in the project
299+
const allGradleFiles = await findAllGradleFiles(projectPath);
300+
301+
// Parse each gradle file and collect dependencies
302+
for (const gradleFile of allGradleFiles) {
303+
const deps = parseDependenciesFromSingleGradle(gradleFile);
304+
deps.forEach(dep => directDeps.add(dep));
305+
}
174306

175-
return workspaceIssues;
307+
return directDeps;
176308
}
177309

178-
async function getAllDependencies(projectNode: INodeData): Promise<PackageDescription[]> {
310+
export async function getDirectDependencies(projectNode: INodeData): Promise<PackageDescription[]> {
179311
const projectStructureData = await Jdtls.getPackageData({ kind: NodeKind.Project, projectUri: projectNode.uri });
180-
const packageContainers = projectStructureData.filter(x => x.kind === NodeKind.Container);
312+
// Only include Maven or Gradle containers (not JRE or other containers)
313+
const dependencyContainers = projectStructureData.filter(x =>
314+
x.kind === NodeKind.Container &&
315+
(x.path?.startsWith(ContainerPath.Maven) || x.path?.startsWith(ContainerPath.Gradle))
316+
);
317+
318+
if (dependencyContainers.length === 0) {
319+
return [];
320+
}
181321

182322
const allPackages = await Promise.allSettled(
183-
packageContainers.map(async (packageContainer) => {
323+
dependencyContainers.map(async (packageContainer) => {
184324
const packageNodes = await Jdtls.getPackageData({
185325
kind: NodeKind.Container,
186326
projectUri: projectNode.uri,
187327
path: packageContainer.path,
188328
});
189-
return packageNodes.map(packageNodeToDescription).filter((x): x is PackageDescription => Boolean(x));
329+
return packageNodes
330+
.map(packageNodeToDescription)
331+
.filter((x): x is PackageDescription => Boolean(x));
190332
})
191333
);
192334

193335
const fulfilled = allPackages.filter((x): x is PromiseFulfilledResult<PackageDescription[]> => x.status === "fulfilled");
194336
const failedPackageCount = allPackages.length - fulfilled.length;
195337
if (failedPackageCount > 0) {
196338
sendInfo("", {
197-
operationName: "java.dependency.assessmentManager.getAllDependencies.rejected",
339+
operationName: "java.dependency.assessmentManager.getDirectDependencies.rejected",
198340
failedPackageCount: String(failedPackageCount),
199341
});
200342
}
201-
return fulfilled.map(x => x.value).flat();
343+
344+
let dependencies = fulfilled.map(x => x.value).flat();
345+
346+
if (!dependencies || dependencies.length === 0) {
347+
sendInfo("", {
348+
operationName: "java.dependency.assessmentManager.getDirectDependencies.noDependencyInfo"
349+
});
350+
return [];
351+
}
352+
353+
// Determine build type from dependency containers
354+
const isMaven = dependencyContainers.some(x => x.path?.startsWith(ContainerPath.Maven));
355+
// Get direct dependency identifiers from build files
356+
let directDependencyIds: Set<string> | null = null;
357+
if (projectNode.uri && dependencyContainers.length > 0) {
358+
try {
359+
const projectPath = Uri.parse(projectNode.uri).fsPath;
360+
if (isMaven) {
361+
directDependencyIds = await parseDirectDependenciesFromPom(projectPath);
362+
} else {
363+
directDependencyIds = await parseDirectDependenciesFromGradle(projectPath);
364+
}
365+
} catch {
366+
// Ignore errors
367+
}
368+
}
369+
370+
if (!directDependencyIds || directDependencyIds.size === 0) {
371+
sendInfo("", {
372+
operationName: "java.dependency.assessmentManager.getDirectDependencies.noDirectDependencyInfo"
373+
});
374+
// TODO: fallback to return all dependencies if we cannot parse direct dependencies or just return empty?
375+
return dependencies;
376+
}
377+
// Filter to only direct dependencies if we have build file info
378+
dependencies = dependencies.filter(pkg =>
379+
directDependencyIds!.has(`${pkg.groupId}:${pkg.artifactId}`)
380+
);
381+
382+
return dependencies;
202383
}
203384

204385
async function getCVEIssues(dependencies: PackageDescription[]): Promise<UpgradeIssue[]> {

src/upgrade/display/notificationManager.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,16 @@ class NotificationManager implements IUpgradeIssuesRenderer {
4141
if (issues.length === 0) {
4242
return;
4343
}
44-
const issue = issues[0];
44+
45+
// Filter to only CVE issues and cast to CveUpgradeIssue[]
46+
const cveIssues = issues.filter(
47+
(i): i is CveUpgradeIssue => i.reason === UpgradeReason.CVE
48+
);
49+
const nonCVEIssues = issues.filter(
50+
(i) => i.reason !== UpgradeReason.CVE
51+
);
52+
const hasCVEIssue = cveIssues.length > 0;
53+
const issue = hasCVEIssue ? cveIssues[0] : nonCVEIssues[0];
4554

4655
if (!this.shouldShow()) {
4756
return;
@@ -56,12 +65,8 @@ class NotificationManager implements IUpgradeIssuesRenderer {
5665
const prompt = buildFixPrompt(issue);
5766

5867
let notificationMessage = "";
59-
let cveIssues: CveUpgradeIssue[] = [];
60-
if (issue.reason === UpgradeReason.CVE) {
61-
// Filter to only CVE issues and cast to CveUpgradeIssue[]
62-
cveIssues = issues.filter(
63-
(i): i is CveUpgradeIssue => i.reason === UpgradeReason.CVE
64-
);
68+
69+
if (hasCVEIssue) {
6570
notificationMessage = buildCVENotificationMessage(cveIssues, hasExtension);
6671
} else {
6772
notificationMessage = buildNotificationMessage(issue, hasExtension);
@@ -72,7 +77,7 @@ class NotificationManager implements IUpgradeIssuesRenderer {
7277
operationName: "java.dependency.upgradeNotification.show",
7378
});
7479

75-
const buttons = issue.reason === UpgradeReason.CVE
80+
const buttons = hasCVEIssue
7681
? [fixCVEButtonText, BUTTON_TEXT_NOT_NOW]
7782
: [upgradeButtonText, BUTTON_TEXT_NOT_NOW];
7883

0 commit comments

Comments
 (0)