From eedc65e878e923c3c6154c3f0870e0e5f48a563c Mon Sep 17 00:00:00 2001 From: Ye Zhu Date: Mon, 15 Dec 2025 17:00:10 +0800 Subject: [PATCH 1/4] get the direct dependencies --- src/upgrade/assessmentManager.ts | 168 +++++++++++++++++++++++++++++-- 1 file changed, 161 insertions(+), 7 deletions(-) diff --git a/src/upgrade/assessmentManager.ts b/src/upgrade/assessmentManager.ts index 6b0ba95e..39bb77e1 100644 --- a/src/upgrade/assessmentManager.ts +++ b/src/upgrade/assessmentManager.ts @@ -1,7 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +import * as fs from 'fs'; +import * as path from 'path'; import * as semver from 'semver'; +import { Uri } from 'vscode'; import { Jdtls } from "../java/jdtls"; import { NodeKind, type INodeData } from "../java/nodeData"; import { type DependencyCheckItem, type UpgradeIssue, type PackageDescription, UpgradeReason } from "./type"; @@ -145,7 +148,7 @@ async function getDependencyIssues(dependencies: PackageDescription[]): Promise< async function getProjectIssues(projectNode: INodeData): Promise { const issues: UpgradeIssue[] = []; - const dependencies = await getAllDependencies(projectNode); + const dependencies = await getDirectDependencies(projectNode); issues.push(...await getCVEIssues(dependencies)); issues.push(...getJavaIssues(projectNode)); issues.push(...await getDependencyIssues(dependencies)); @@ -175,18 +178,150 @@ async function getWorkspaceIssues(workspaceFolderUri: string): Promise { +const MAVEN_CONTAINER_PATH = "org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"; +const GRADLE_CONTAINER_PATH = "org.eclipse.buildship.core.gradleclasspathcontainer"; + +/** + * Parse direct dependencies from pom.xml file. + * Also checks parent pom.xml for multi-module projects. + */ +function parseDirectDependenciesFromPom(pomPath: string): Set { + const directDeps = new Set(); + try { + const pomContent = fs.readFileSync(pomPath, 'utf-8'); + + // Extract dependencies from section (not inside ) + // First, remove dependencyManagement sections to avoid including managed deps + const withoutDepMgmt = pomContent.replace(/[\s\S]*?<\/dependencyManagement>/g, ''); + + // Match blocks and extract groupId and artifactId + const dependencyRegex = /\s*([^<]+)<\/groupId>\s*([^<]+)<\/artifactId>/g; + let match; + while ((match = dependencyRegex.exec(withoutDepMgmt)) !== null) { + const groupId = match[1].trim(); + const artifactId = match[2].trim(); + // Skip property references like ${project.groupId} + if (!groupId.includes('${') && !artifactId.includes('${')) { + directDeps.add(`${groupId}:${artifactId}`); + } + } + + // Check for parent pom in multi-module projects + const parentPomPath = path.join(path.dirname(pomPath), '..', 'pom.xml'); + if (fs.existsSync(parentPomPath)) { + const parentDeps = parseDirectDependenciesFromPom(parentPomPath); + parentDeps.forEach(dep => directDeps.add(dep)); + } + } catch { + // If we can't read the pom, return empty set + } + return directDeps; +} + +/** + * Parse direct dependencies from build.gradle or build.gradle.kts file + */ +function parseDirectDependenciesFromGradle(gradlePath: string): Set { + const directDeps = new Set(); + try { + const gradleContent = fs.readFileSync(gradlePath, 'utf-8'); + + // Match common dependency configurations: + // implementation 'group:artifact:version' + // implementation "group:artifact:version" + // api 'group:artifact:version' + // compileOnly, runtimeOnly, testImplementation, etc. + const shortFormRegex = /(?:implementation|api|compile|compileOnly|runtimeOnly|testImplementation|testCompileOnly|testRuntimeOnly)\s*\(?['"]([^:'"]+):([^:'"]+)(?::[^'"]*)?['"]\)?/g; + let match; + while ((match = shortFormRegex.exec(gradleContent)) !== null) { + const groupId = match[1].trim(); + const artifactId = match[2].trim(); + if (!groupId.includes('$') && !artifactId.includes('$')) { + directDeps.add(`${groupId}:${artifactId}`); + } + } + + // Match map notation: implementation group: 'x', name: 'y', version: 'z' + const mapFormRegex = /(?:implementation|api|compile|compileOnly|runtimeOnly|testImplementation|testCompileOnly|testRuntimeOnly)\s*\(?group:\s*['"]([^'"]+)['"]\s*,\s*name:\s*['"]([^'"]+)['"]/g; + while ((match = mapFormRegex.exec(gradleContent)) !== null) { + const groupId = match[1].trim(); + const artifactId = match[2].trim(); + if (!groupId.includes('$') && !artifactId.includes('$')) { + directDeps.add(`${groupId}:${artifactId}`); + } + } + } catch { + // If we can't read the gradle file, return empty set + } + return directDeps; +} + +/** + * Find the build file (pom.xml or build.gradle) for a project + */ +function findBuildFile(projectUri: string | undefined): { path: string; type: 'maven' | 'gradle' } | null { + if (!projectUri) { + return null; + } + try { + const projectPath = Uri.parse(projectUri).fsPath; + + // Check for Maven + const pomPath = path.join(projectPath, 'pom.xml'); + if (fs.existsSync(pomPath)) { + return { path: pomPath, type: 'maven' }; + } + + // Check for Gradle Kotlin DSL + const gradleKtsPath = path.join(projectPath, 'build.gradle.kts'); + if (fs.existsSync(gradleKtsPath)) { + return { path: gradleKtsPath, type: 'gradle' }; + } + + // Check for Gradle Groovy DSL + const gradlePath = path.join(projectPath, 'build.gradle'); + if (fs.existsSync(gradlePath)) { + return { path: gradlePath, type: 'gradle' }; + } + } catch { + // Ignore errors + } + return null; +} + +/** + * Parse direct dependencies from build file (Maven or Gradle) + */ +function parseDirectDependencies(buildFile: { path: string; type: 'maven' | 'gradle' }): Set { + if (buildFile.type === 'maven') { + return parseDirectDependenciesFromPom(buildFile.path); + } else { + return parseDirectDependenciesFromGradle(buildFile.path); + } +} + +async function getDirectDependencies(projectNode: INodeData): Promise { const projectStructureData = await Jdtls.getPackageData({ kind: NodeKind.Project, projectUri: projectNode.uri }); - const packageContainers = projectStructureData.filter(x => x.kind === NodeKind.Container); + // Only include Maven or Gradle containers (not JRE or other containers) + const dependencyContainers = projectStructureData.filter(x => + x.kind === NodeKind.Container && + (x.path?.startsWith(MAVEN_CONTAINER_PATH) || x.path?.startsWith(GRADLE_CONTAINER_PATH)) + ); + + // Get direct dependency identifiers from build file + const buildFile = findBuildFile(projectNode.uri); + const directDependencyIds = buildFile ? parseDirectDependencies(buildFile) : null; const allPackages = await Promise.allSettled( - packageContainers.map(async (packageContainer) => { + dependencyContainers.map(async (packageContainer) => { const packageNodes = await Jdtls.getPackageData({ kind: NodeKind.Container, projectUri: projectNode.uri, path: packageContainer.path, }); - return packageNodes.map(packageNodeToDescription).filter((x): x is PackageDescription => Boolean(x)); + return packageNodes + .map(packageNodeToDescription) + .filter((x): x is PackageDescription => Boolean(x)); }) ); @@ -194,11 +329,30 @@ async function getAllDependencies(projectNode: INodeData): Promise 0) { sendInfo("", { - operationName: "java.dependency.assessmentManager.getAllDependencies.rejected", + operationName: "java.dependency.assessmentManager.getDirectDependencies.rejected", failedPackageCount: String(failedPackageCount), }); } - return fulfilled.map(x => x.value).flat(); + + let dependencies = fulfilled.map(x => x.value).flat(); + + // Filter to only direct dependencies if we have build file info + if (directDependencyIds && directDependencyIds.size > 0) { + dependencies = dependencies.filter(pkg => + directDependencyIds.has(`${pkg.groupId}:${pkg.artifactId}`) + ); + } + + // Deduplicate by GAV coordinates + const seen = new Set(); + return dependencies.filter(pkg => { + const key = `${pkg.groupId}:${pkg.artifactId}:${pkg.version}`; + if (seen.has(key)) { + return false; + } + seen.add(key); + return true; + }); } async function getCVEIssues(dependencies: PackageDescription[]): Promise { From 1d4ca96f057daa8721f2b6dabee64048157c0f81 Mon Sep 17 00:00:00 2001 From: Ye Zhu Date: Fri, 19 Dec 2025 18:27:02 +0800 Subject: [PATCH 2/4] fix comments --- src/upgrade/assessmentManager.ts | 165 ++++++++++++++++++++----------- 1 file changed, 109 insertions(+), 56 deletions(-) diff --git a/src/upgrade/assessmentManager.ts b/src/upgrade/assessmentManager.ts index 39bb77e1..2dbea1ca 100644 --- a/src/upgrade/assessmentManager.ts +++ b/src/upgrade/assessmentManager.ts @@ -2,8 +2,11 @@ // Licensed under the MIT license. import * as fs from 'fs'; -import * as path from 'path'; import * as semver from 'semver'; +import * as glob from 'glob'; +import { promisify } from 'util'; + +const globAsync = promisify(glob); import { Uri } from 'vscode'; import { Jdtls } from "../java/jdtls"; import { NodeKind, type INodeData } from "../java/nodeData"; @@ -182,10 +185,25 @@ const MAVEN_CONTAINER_PATH = "org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"; const GRADLE_CONTAINER_PATH = "org.eclipse.buildship.core.gradleclasspathcontainer"; /** - * Parse direct dependencies from pom.xml file. - * Also checks parent pom.xml for multi-module projects. + * Find all pom.xml files in a directory using glob */ -function parseDirectDependenciesFromPom(pomPath: string): Set { +async function findAllPomFiles(dir: string): Promise { + try { + return await globAsync('**/pom.xml', { + cwd: dir, + absolute: true, + nodir: true, + ignore: ['**/node_modules/**', '**/target/**', '**/.git/**', '**/.idea/**', '**/.vscode/**'] + }); + } catch { + return []; + } +} + +/** + * Parse dependencies from a single pom.xml file + */ +function parseDependenciesFromSinglePom(pomPath: string): Set { const directDeps = new Set(); try { const pomContent = fs.readFileSync(pomPath, 'utf-8'); @@ -205,13 +223,6 @@ function parseDirectDependenciesFromPom(pomPath: string): Set { directDeps.add(`${groupId}:${artifactId}`); } } - - // Check for parent pom in multi-module projects - const parentPomPath = path.join(path.dirname(pomPath), '..', 'pom.xml'); - if (fs.existsSync(parentPomPath)) { - const parentDeps = parseDirectDependenciesFromPom(parentPomPath); - parentDeps.forEach(dep => directDeps.add(dep)); - } } catch { // If we can't read the pom, return empty set } @@ -219,9 +230,44 @@ function parseDirectDependenciesFromPom(pomPath: string): Set { } /** - * Parse direct dependencies from build.gradle or build.gradle.kts file + * Parse direct dependencies from all pom.xml files in the project. + * Finds all pom.xml files starting from the project root and parses them to collect dependencies. + */ +async function parseDirectDependenciesFromPom(projectPath: string): Promise> { + const directDeps = new Set(); + + // Find all pom.xml files in the project starting from the project root + const allPomFiles = await findAllPomFiles(projectPath); + + // Parse each pom.xml and collect dependencies + for (const pom of allPomFiles) { + const deps = parseDependenciesFromSinglePom(pom); + deps.forEach(dep => directDeps.add(dep)); + } + + return directDeps; +} + +/** + * Find all Gradle build files in a directory using glob */ -function parseDirectDependenciesFromGradle(gradlePath: string): Set { +async function findAllGradleFiles(dir: string): Promise { + try { + return await globAsync('**/{build.gradle,build.gradle.kts}', { + cwd: dir, + absolute: true, + nodir: true, + ignore: ['**/node_modules/**', '**/build/**', '**/.git/**', '**/.idea/**', '**/.vscode/**', '**/.gradle/**'] + }); + } catch { + return []; + } +} + +/** + * Parse dependencies from a single Gradle build file + */ +function parseDependenciesFromSingleGradle(gradlePath: string): Set { const directDeps = new Set(); try { const gradleContent = fs.readFileSync(gradlePath, 'utf-8'); @@ -257,47 +303,22 @@ function parseDirectDependenciesFromGradle(gradlePath: string): Set { } /** - * Find the build file (pom.xml or build.gradle) for a project - */ -function findBuildFile(projectUri: string | undefined): { path: string; type: 'maven' | 'gradle' } | null { - if (!projectUri) { - return null; - } - try { - const projectPath = Uri.parse(projectUri).fsPath; - - // Check for Maven - const pomPath = path.join(projectPath, 'pom.xml'); - if (fs.existsSync(pomPath)) { - return { path: pomPath, type: 'maven' }; - } - - // Check for Gradle Kotlin DSL - const gradleKtsPath = path.join(projectPath, 'build.gradle.kts'); - if (fs.existsSync(gradleKtsPath)) { - return { path: gradleKtsPath, type: 'gradle' }; - } - - // Check for Gradle Groovy DSL - const gradlePath = path.join(projectPath, 'build.gradle'); - if (fs.existsSync(gradlePath)) { - return { path: gradlePath, type: 'gradle' }; - } - } catch { - // Ignore errors - } - return null; -} - -/** - * Parse direct dependencies from build file (Maven or Gradle) + * Parse direct dependencies from all Gradle build files in the project. + * Finds all build.gradle and build.gradle.kts files and parses them to collect dependencies. */ -function parseDirectDependencies(buildFile: { path: string; type: 'maven' | 'gradle' }): Set { - if (buildFile.type === 'maven') { - return parseDirectDependenciesFromPom(buildFile.path); - } else { - return parseDirectDependenciesFromGradle(buildFile.path); +async function parseDirectDependenciesFromGradle(projectPath: string): Promise> { + const directDeps = new Set(); + + // Find all Gradle build files in the project + const allGradleFiles = await findAllGradleFiles(projectPath); + + // Parse each gradle file and collect dependencies + for (const gradleFile of allGradleFiles) { + const deps = parseDependenciesFromSingleGradle(gradleFile); + deps.forEach(dep => directDeps.add(dep)); } + + return directDeps; } async function getDirectDependencies(projectNode: INodeData): Promise { @@ -308,9 +329,12 @@ async function getDirectDependencies(projectNode: INodeData): Promise x.path?.startsWith(MAVEN_CONTAINER_PATH)); + const allPackages = await Promise.allSettled( dependencyContainers.map(async (packageContainer) => { @@ -336,10 +360,39 @@ async function getDirectDependencies(projectNode: INodeData): Promise x.value).flat(); + if (!dependencies) { + sendInfo("", { + operationName: "java.dependency.assessmentManager.getDirectDependencies.noDependencyInfo", + buildType: isMaven ? "maven" : "gradle", + }); + return []; + } + // Get direct dependency identifiers from build files + let directDependencyIds: Set | null = null; + if (projectNode.uri && dependencyContainers.length > 0) { + try { + const projectPath = Uri.parse(projectNode.uri).fsPath; + if (isMaven) { + directDependencyIds = await parseDirectDependenciesFromPom(projectPath); + } else { + directDependencyIds = await parseDirectDependenciesFromGradle(projectPath); + } + } catch { + // Ignore errors + } + } + + if (!directDependencyIds) { + sendInfo("", { + operationName: "java.dependency.assessmentManager.getDirectDependencies.noDirectDependencyInfo", + buildType: isMaven ? "maven" : "gradle", + }); + return []; + } // Filter to only direct dependencies if we have build file info if (directDependencyIds && directDependencyIds.size > 0) { dependencies = dependencies.filter(pkg => - directDependencyIds.has(`${pkg.groupId}:${pkg.artifactId}`) + directDependencyIds!.has(`${pkg.groupId}:${pkg.artifactId}`) ); } From d1124212d2c2841bb17eb021c41f0d81cbc0a210 Mon Sep 17 00:00:00 2001 From: Ye Zhu Date: Wed, 24 Dec 2025 15:02:47 +0800 Subject: [PATCH 3/4] Put the projectype checking in the getDirectDependencies && fix tslint errors --- src/upgrade/assessmentManager.ts | 50 ++++++++++++++++++-------------- src/upgrade/upgradeManager.ts | 12 -------- 2 files changed, 28 insertions(+), 34 deletions(-) diff --git a/src/upgrade/assessmentManager.ts b/src/upgrade/assessmentManager.ts index 2dbea1ca..b9b43d14 100644 --- a/src/upgrade/assessmentManager.ts +++ b/src/upgrade/assessmentManager.ts @@ -17,6 +17,7 @@ import { buildPackageId } from './utility'; import metadataManager from './metadataManager'; import { sendInfo } from 'vscode-extension-telemetry-wrapper'; import { batchGetCVEIssues } from './cve'; +import { ContainerPath } from '../views/containerNode'; function packageNodeToDescription(node: INodeData): PackageDescription | null { const version = node.metaData?.["maven.version"]; @@ -152,12 +153,17 @@ async function getDependencyIssues(dependencies: PackageDescription[]): Promise< async function getProjectIssues(projectNode: INodeData): Promise { const issues: UpgradeIssue[] = []; const dependencies = await getDirectDependencies(projectNode); + if (dependencies.length === 0) { + sendInfo("", { + operationName: "java.dependency.assessmentManager.getProjectIssues.noDirectDependencies" + }); + return issues; + } issues.push(...await getCVEIssues(dependencies)); issues.push(...getJavaIssues(projectNode)); issues.push(...await getDependencyIssues(dependencies)); return issues; - } async function getWorkspaceIssues(workspaceFolderUri: string): Promise { @@ -181,9 +187,6 @@ async function getWorkspaceIssues(workspaceFolderUri: string): Promise { const directDeps = new Set(); try { const pomContent = fs.readFileSync(pomPath, 'utf-8'); - + // Extract dependencies from section (not inside ) // First, remove dependencyManagement sections to avoid including managed deps const withoutDepMgmt = pomContent.replace(/[\s\S]*?<\/dependencyManagement>/g, ''); - + // Match blocks and extract groupId and artifactId const dependencyRegex = /\s*([^<]+)<\/groupId>\s*([^<]+)<\/artifactId>/g; - let match; - while ((match = dependencyRegex.exec(withoutDepMgmt)) !== null) { + let match = dependencyRegex.exec(withoutDepMgmt); + while (match !== null) { const groupId = match[1].trim(); const artifactId = match[2].trim(); // Skip property references like ${project.groupId} if (!groupId.includes('${') && !artifactId.includes('${')) { directDeps.add(`${groupId}:${artifactId}`); } + match = dependencyRegex.exec(withoutDepMgmt); } } catch { // If we can't read the pom, return empty set @@ -235,16 +239,16 @@ function parseDependenciesFromSinglePom(pomPath: string): Set { */ async function parseDirectDependenciesFromPom(projectPath: string): Promise> { const directDeps = new Set(); - + // Find all pom.xml files in the project starting from the project root const allPomFiles = await findAllPomFiles(projectPath); - + // Parse each pom.xml and collect dependencies for (const pom of allPomFiles) { const deps = parseDependenciesFromSinglePom(pom); deps.forEach(dep => directDeps.add(dep)); } - + return directDeps; } @@ -271,30 +275,33 @@ function parseDependenciesFromSingleGradle(gradlePath: string): Set { const directDeps = new Set(); try { const gradleContent = fs.readFileSync(gradlePath, 'utf-8'); - + // Match common dependency configurations: // implementation 'group:artifact:version' // implementation "group:artifact:version" // api 'group:artifact:version' // compileOnly, runtimeOnly, testImplementation, etc. const shortFormRegex = /(?:implementation|api|compile|compileOnly|runtimeOnly|testImplementation|testCompileOnly|testRuntimeOnly)\s*\(?['"]([^:'"]+):([^:'"]+)(?::[^'"]*)?['"]\)?/g; - let match; - while ((match = shortFormRegex.exec(gradleContent)) !== null) { + let match = shortFormRegex.exec(gradleContent); + while (match !== null) { const groupId = match[1].trim(); const artifactId = match[2].trim(); if (!groupId.includes('$') && !artifactId.includes('$')) { directDeps.add(`${groupId}:${artifactId}`); } + match = shortFormRegex.exec(gradleContent); } // Match map notation: implementation group: 'x', name: 'y', version: 'z' const mapFormRegex = /(?:implementation|api|compile|compileOnly|runtimeOnly|testImplementation|testCompileOnly|testRuntimeOnly)\s*\(?group:\s*['"]([^'"]+)['"]\s*,\s*name:\s*['"]([^'"]+)['"]/g; - while ((match = mapFormRegex.exec(gradleContent)) !== null) { + match = mapFormRegex.exec(gradleContent); + while (match !== null) { const groupId = match[1].trim(); const artifactId = match[2].trim(); if (!groupId.includes('$') && !artifactId.includes('$')) { directDeps.add(`${groupId}:${artifactId}`); } + match = mapFormRegex.exec(gradleContent); } } catch { // If we can't read the gradle file, return empty set @@ -308,16 +315,16 @@ function parseDependenciesFromSingleGradle(gradlePath: string): Set { */ async function parseDirectDependenciesFromGradle(projectPath: string): Promise> { const directDeps = new Set(); - + // Find all Gradle build files in the project const allGradleFiles = await findAllGradleFiles(projectPath); - + // Parse each gradle file and collect dependencies for (const gradleFile of allGradleFiles) { const deps = parseDependenciesFromSingleGradle(gradleFile); deps.forEach(dep => directDeps.add(dep)); } - + return directDeps; } @@ -326,15 +333,14 @@ async function getDirectDependencies(projectNode: INodeData): Promise x.kind === NodeKind.Container && - (x.path?.startsWith(MAVEN_CONTAINER_PATH) || x.path?.startsWith(GRADLE_CONTAINER_PATH)) + (x.path?.startsWith(ContainerPath.Maven) || x.path?.startsWith(ContainerPath.Gradle)) ); if (dependencyContainers.length === 0) { return []; } // Determine build type from dependency containers - const isMaven = dependencyContainers.some(x => x.path?.startsWith(MAVEN_CONTAINER_PATH)); - + const isMaven = dependencyContainers.some(x => x.path?.startsWith(ContainerPath.Maven)); const allPackages = await Promise.allSettled( dependencyContainers.map(async (packageContainer) => { @@ -391,7 +397,7 @@ async function getDirectDependencies(projectNode: INodeData): Promise 0) { - dependencies = dependencies.filter(pkg => + dependencies = dependencies.filter(pkg => directDependencyIds!.has(`${pkg.groupId}:${pkg.artifactId}`) ); } diff --git a/src/upgrade/upgradeManager.ts b/src/upgrade/upgradeManager.ts index c61c786b..082441c4 100644 --- a/src/upgrade/upgradeManager.ts +++ b/src/upgrade/upgradeManager.ts @@ -61,18 +61,6 @@ class UpgradeManager { sendInfo(_operationId, { "skipReason": "languageServerNotReady" }); return; } - const projectData = await Jdtls.getPackageData({ - kind: NodeKind.Project, - projectUri: folder.uri.toString(), - }); - const isMavenGradleProject = projectData.some( - (dep) => dep.kind === NodeKind.Container && - (dep.path?.startsWith(ContainerPath.Maven) || dep.path?.startsWith(ContainerPath.Gradle)) - ); - if (!isMavenGradleProject) { - sendInfo(_operationId, { "skipReason": "notMavenGradleProject" }); - return; - } const hasJavaError: boolean = await Jdtls.checkImportStatus(); if (hasJavaError) { From ea52c38f9f1be91bae9640e8ccea336e06a3fd1a Mon Sep 17 00:00:00 2001 From: Ye Zhu Date: Thu, 25 Dec 2025 11:14:32 +0800 Subject: [PATCH 4/4] Move getDirectDeps into upgradeManager and just check the dependencies in the assessmentManager --- src/upgrade/assessmentManager.ts | 67 ++++++---------------- src/upgrade/display/notificationManager.ts | 21 ++++--- src/upgrade/upgradeManager.ts | 60 +++++++++++-------- 3 files changed, 66 insertions(+), 82 deletions(-) diff --git a/src/upgrade/assessmentManager.ts b/src/upgrade/assessmentManager.ts index b9b43d14..1367b73a 100644 --- a/src/upgrade/assessmentManager.ts +++ b/src/upgrade/assessmentManager.ts @@ -150,43 +150,19 @@ async function getDependencyIssues(dependencies: PackageDescription[]): Promise< return issues; } -async function getProjectIssues(projectNode: INodeData): Promise { +async function getWorkspaceIssues(projectDeps:{projectNode: INodeData, dependencies: PackageDescription[]}[]): Promise { + const issues: UpgradeIssue[] = []; - const dependencies = await getDirectDependencies(projectNode); - if (dependencies.length === 0) { - sendInfo("", { - operationName: "java.dependency.assessmentManager.getProjectIssues.noDirectDependencies" - }); - return issues; + const dependenciesSet: Set = 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 { - 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 */ @@ -328,7 +304,7 @@ async function parseDirectDependenciesFromGradle(projectPath: string): Promise { +export async function getDirectDependencies(projectNode: INodeData): Promise { 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 => @@ -339,8 +315,6 @@ async function getDirectDependencies(projectNode: INodeData): Promise x.path?.startsWith(ContainerPath.Maven)); const allPackages = await Promise.allSettled( dependencyContainers.map(async (packageContainer) => { @@ -368,11 +342,13 @@ async function getDirectDependencies(projectNode: INodeData): Promise x.path?.startsWith(ContainerPath.Maven)); // Get direct dependency identifiers from build files let directDependencyIds: Set | null = null; if (projectNode.uri && dependencyContainers.length > 0) { @@ -390,10 +366,10 @@ async function getDirectDependencies(projectNode: INodeData): Promise 0) { @@ -402,16 +378,7 @@ async function getDirectDependencies(projectNode: INodeData): Promise(); - 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 { diff --git a/src/upgrade/display/notificationManager.ts b/src/upgrade/display/notificationManager.ts index 3061e179..c3bb0429 100644 --- a/src/upgrade/display/notificationManager.ts +++ b/src/upgrade/display/notificationManager.ts @@ -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; @@ -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); @@ -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]; diff --git a/src/upgrade/upgradeManager.ts b/src/upgrade/upgradeManager.ts index 082441c4..73c3a1ab 100644 --- a/src/upgrade/upgradeManager.ts +++ b/src/upgrade/upgradeManager.ts @@ -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."; @@ -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> }> => + 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); + } + })(); } }