Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Move getDirectDeps into upgradeManager and just check the dependencie…
…s in the assessmentManager
  • Loading branch information
Ye Zhu committed Dec 25, 2025
commit ea52c38f9f1be91bae9640e8ccea336e06a3fd1a
67 changes: 17 additions & 50 deletions src/upgrade/assessmentManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,43 +150,19 @@ async function getDependencyIssues(dependencies: PackageDescription[]): Promise<
return issues;
}

async function getProjectIssues(projectNode: INodeData): Promise<UpgradeIssue[]> {
async function getWorkspaceIssues(projectDeps:{projectNode: INodeData, dependencies: PackageDescription[]}[]): Promise<UpgradeIssue[]> {

const issues: UpgradeIssue[] = [];
const dependencies = await getDirectDependencies(projectNode);
if (dependencies.length === 0) {
sendInfo("", {
operationName: "java.dependency.assessmentManager.getProjectIssues.noDirectDependencies"
});
return issues;
const dependenciesSet: Set<PackageDescription> = new Set();
for (const { projectNode, dependencies } of projectDeps) {
issues.push(...getJavaIssues(projectNode));
dependencies.forEach(dep => dependenciesSet.add(dep));
}
issues.push(...await getCVEIssues(dependencies));
issues.push(...getJavaIssues(projectNode));
issues.push(...await getDependencyIssues(dependencies));

issues.push(...await getCVEIssues(Array.from(dependenciesSet)));
issues.push(...await getDependencyIssues(Array.from(dependenciesSet)));
return issues;
}

async function getWorkspaceIssues(workspaceFolderUri: string): Promise<UpgradeIssue[]> {
const projects = await Jdtls.getProjects(workspaceFolderUri);
const projectsIssues = await Promise.allSettled(projects.map(async (projectNode) => {
const issues = await getProjectIssues(projectNode);
return issues;
}));

const workspaceIssues = projectsIssues.map(x => {
if (x.status === "fulfilled") {
return x.value;
}

sendInfo("", {
operationName: "java.dependency.assessmentManager.getWorkspaceIssues",
});
return [];
}).flat();

return workspaceIssues;
}

/**
* Find all pom.xml files in a directory using glob
*/
Expand Down Expand Up @@ -328,7 +304,7 @@ async function parseDirectDependenciesFromGradle(projectPath: string): Promise<S
return directDeps;
}

async function getDirectDependencies(projectNode: INodeData): Promise<PackageDescription[]> {
export async function getDirectDependencies(projectNode: INodeData): Promise<PackageDescription[]> {
const projectStructureData = await Jdtls.getPackageData({ kind: NodeKind.Project, projectUri: projectNode.uri });
// Only include Maven or Gradle containers (not JRE or other containers)
const dependencyContainers = projectStructureData.filter(x =>
Expand All @@ -339,8 +315,6 @@ async function getDirectDependencies(projectNode: INodeData): Promise<PackageDes
if (dependencyContainers.length === 0) {
return [];
}
// Determine build type from dependency containers
const isMaven = dependencyContainers.some(x => x.path?.startsWith(ContainerPath.Maven));

const allPackages = await Promise.allSettled(
dependencyContainers.map(async (packageContainer) => {
Expand Down Expand Up @@ -368,11 +342,13 @@ async function getDirectDependencies(projectNode: INodeData): Promise<PackageDes

if (!dependencies) {
sendInfo("", {
operationName: "java.dependency.assessmentManager.getDirectDependencies.noDependencyInfo",
buildType: isMaven ? "maven" : "gradle",
operationName: "java.dependency.assessmentManager.getDirectDependencies.noDependencyInfo"
});
return [];
}

// Determine build type from dependency containers
const isMaven = dependencyContainers.some(x => x.path?.startsWith(ContainerPath.Maven));
// Get direct dependency identifiers from build files
let directDependencyIds: Set<string> | null = null;
if (projectNode.uri && dependencyContainers.length > 0) {
Expand All @@ -390,10 +366,10 @@ async function getDirectDependencies(projectNode: INodeData): Promise<PackageDes

if (!directDependencyIds) {
sendInfo("", {
operationName: "java.dependency.assessmentManager.getDirectDependencies.noDirectDependencyInfo",
buildType: isMaven ? "maven" : "gradle",
operationName: "java.dependency.assessmentManager.getDirectDependencies.noDirectDependencyInfo"
});
return [];
//TODO: fallback to return all dependencies if we cannot parse direct dependencies or just return empty?
return dependencies;
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback behavior at line 372 returns all dependencies when direct dependency parsing fails, which undermines the primary goal of this PR to exclude transitive dependencies. This could lead to false positives in upgrade notifications. Consider either:

  1. Returning an empty array when parsing fails (with appropriate telemetry)
  2. Using a different heuristic to identify direct dependencies
  3. At minimum, logging a warning that the direct dependency filtering failed so users understand the behavior
Suggested change
//TODO: fallback to return all dependencies if we cannot parse direct dependencies or just return empty?
return dependencies;
// When we cannot parse direct dependencies, avoid returning transitive dependencies; return empty instead.
return [];

Copilot uses AI. Check for mistakes.
}
// Filter to only direct dependencies if we have build file info
if (directDependencyIds && directDependencyIds.size > 0) {
Expand All @@ -402,16 +378,7 @@ async function getDirectDependencies(projectNode: INodeData): Promise<PackageDes
);
}

// Deduplicate by GAV coordinates
const seen = new Set<string>();
return dependencies.filter(pkg => {
const key = `${pkg.groupId}:${pkg.artifactId}:${pkg.version}`;
if (seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
return dependencies;
}

async function getCVEIssues(dependencies: PackageDescription[]): Promise<UpgradeIssue[]> {
Expand Down
21 changes: 13 additions & 8 deletions src/upgrade/display/notificationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,16 @@ class NotificationManager implements IUpgradeIssuesRenderer {
if (issues.length === 0) {
return;
}
const issue = issues[0];

// Filter to only CVE issues and cast to CveUpgradeIssue[]
const cveIssues = issues.filter(
(i): i is CveUpgradeIssue => i.reason === UpgradeReason.CVE
);
const nonCVEIssues = issues.filter(
(i) => i.reason !== UpgradeReason.CVE
);
const hasCVEIssue = cveIssues.length > 0;
const issue = hasCVEIssue ? cveIssues[0] : nonCVEIssues[0];

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

let notificationMessage = "";
let cveIssues: CveUpgradeIssue[] = [];
if (issue.reason === UpgradeReason.CVE) {
// Filter to only CVE issues and cast to CveUpgradeIssue[]
cveIssues = issues.filter(
(i): i is CveUpgradeIssue => i.reason === UpgradeReason.CVE
);

if (hasCVEIssue) {
notificationMessage = buildCVENotificationMessage(cveIssues, hasExtension);
} else {
notificationMessage = buildNotificationMessage(issue, hasExtension);
Expand All @@ -72,7 +77,7 @@ class NotificationManager implements IUpgradeIssuesRenderer {
operationName: "java.dependency.upgradeNotification.show",
});

const buttons = issue.reason === UpgradeReason.CVE
const buttons = hasCVEIssue
? [fixCVEButtonText, BUTTON_TEXT_NOT_NOW]
: [upgradeButtonText, BUTTON_TEXT_NOT_NOW];

Expand Down
60 changes: 36 additions & 24 deletions src/upgrade/upgradeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ import { instrumentOperation, instrumentOperationAsVsCodeCommand, sendInfo } fro
import { Commands } from "../commands";
import notificationManager from "./display/notificationManager";
import { Settings } from "../settings";
import assessmentManager from "./assessmentManager";
import assessmentManager, { getDirectDependencies } from "./assessmentManager";
import { checkOrInstallAppModExtensionForUpgrade, checkOrPopupToInstallAppModExtensionForModernization } from "./utility";
import { NodeKind } from "../../extension.bundle";
import { ContainerPath } from "../views/containerNode";

const DEFAULT_UPGRADE_PROMPT = "Upgrade Java project dependency to latest version.";

Expand Down Expand Up @@ -55,28 +53,42 @@ class UpgradeManager {
}

private static async runDependencyCheckup(folder: WorkspaceFolder) {
return (instrumentOperation("java.dependency.runDependencyCheckup",
async (_operationId: string) => {
if (!(await languageServerApiManager.ready())) {
sendInfo(_operationId, { "skipReason": "languageServerNotReady" });
return;
}

const hasJavaError: boolean = await Jdtls.checkImportStatus();
if (hasJavaError) {
sendInfo(_operationId, { "skipReason": "hasJavaError" });
return;
}

const uri = folder.uri.toString();
const workspaceIssues = await assessmentManager.getWorkspaceIssues(uri);

if (workspaceIssues.length > 0) {
// only show one issue in notifications
notificationManager.render(workspaceIssues);
}
return instrumentOperation("java.dependency.runDependencyCheckup", async (_operationId: string) => {
if (!(await languageServerApiManager.ready())) {
sendInfo(_operationId, { skipReason: "languageServerNotReady" });
return;
}
))();

const hasJavaError: boolean = await Jdtls.checkImportStatus();
if (hasJavaError) {
sendInfo(_operationId, { skipReason: "hasJavaError" });
return;
}

const projects = await Jdtls.getProjects(folder.uri.toString());
const projectDirectDepsResults = await Promise.allSettled(
projects.map(async (projectNode) => ({
projectNode,
dependencies: await getDirectDependencies(projectNode),
}))
);

const allProjectDirectDeps = projectDirectDepsResults
.filter((result): result is PromiseFulfilledResult<{ projectNode: typeof projects[0]; dependencies: Awaited<ReturnType<typeof getDirectDependencies>> }> =>
result.status === "fulfilled"
)
.map((result) => result.value);

if (allProjectDirectDeps.every((x) => x.dependencies.length === 0)) {
sendInfo(_operationId, { skipReason: "notMavenGradleProject" });
return;
}

const workspaceIssues = await assessmentManager.getWorkspaceIssues(allProjectDirectDeps);
if (workspaceIssues.length > 0) {
notificationManager.render(workspaceIssues);
}
})();
}
}

Expand Down
Loading