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 { diff --git a/src/upgrade/utility.ts b/src/upgrade/utility.ts index dd1c87bf..9db7e829 100644 --- a/src/upgrade/utility.ts +++ b/src/upgrade/utility.ts @@ -95,15 +95,15 @@ export function buildFixPrompt(issue: UpgradeIssue): string { switch (reason) { case UpgradeReason.JRE_TOO_OLD: { const { suggestedVersion: { name: suggestedVersionName } } = issue; - return `upgrade java runtime to the LTS version ${suggestedVersionName} using java upgrade tools`; + return `upgrade java runtime to the LTS version ${suggestedVersionName} using java upgrade tools by invoking #generate_upgrade_plan`; } case UpgradeReason.END_OF_LIFE: case UpgradeReason.DEPRECATED: { const { suggestedVersion: { name: suggestedVersionName } } = issue; - return `upgrade ${packageDisplayName} to ${suggestedVersionName} using java upgrade tools`; + return `upgrade ${packageDisplayName} to ${suggestedVersionName} using java upgrade tools by invoking #generate_upgrade_plan`; } case UpgradeReason.CVE: { - return `fix all critical and high-severity CVE vulnerabilities in this project`; + return `fix all critical and high-severity CVE vulnerabilities in this project by invoking #validate_cves_for_java`; } } }