diff --git a/src/upgrade/assessmentManager.ts b/src/upgrade/assessmentManager.ts index 6b0ba95e..2dbea1ca 100644 --- a/src/upgrade/assessmentManager.ts +++ b/src/upgrade/assessmentManager.ts @@ -1,7 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +import * as fs from 'fs'; 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"; import { type DependencyCheckItem, type UpgradeIssue, type PackageDescription, UpgradeReason } from "./type"; @@ -145,7 +151,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 +181,171 @@ 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"; + +/** + * Find all pom.xml files in a directory using glob + */ +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'); + + // 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}`); + } + } + } catch { + // If we can't read the pom, return empty set + } + return directDeps; +} + +/** + * 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 + */ +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'); + + // 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; +} + +/** + * 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. + */ +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 { 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)) + ); + + if (dependencyContainers.length === 0) { + return []; + } + // Determine build type from dependency containers + const isMaven = dependencyContainers.some(x => x.path?.startsWith(MAVEN_CONTAINER_PATH)); + 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 +353,59 @@ 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(); + + 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}`) + ); + } + + // 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`; } } }