Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
221 changes: 214 additions & 7 deletions src/upgrade/assessmentManager.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -145,7 +151,7 @@ async function getDependencyIssues(dependencies: PackageDescription[]): Promise<

async function getProjectIssues(projectNode: INodeData): Promise<UpgradeIssue[]> {
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));
Expand Down Expand Up @@ -175,30 +181,231 @@ async function getWorkspaceIssues(workspaceFolderUri: string): Promise<UpgradeIs
return workspaceIssues;
}

async function getAllDependencies(projectNode: INodeData): Promise<PackageDescription[]> {
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<string[]> {
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<string> {
const directDeps = new Set<string>();
try {
const pomContent = fs.readFileSync(pomPath, 'utf-8');

// Extract dependencies from <dependencies> section (not inside <dependencyManagement>)
// First, remove dependencyManagement sections to avoid including managed deps
const withoutDepMgmt = pomContent.replace(/<dependencyManagement>[\s\S]*?<\/dependencyManagement>/g, '');

// Match <dependency> blocks and extract groupId and artifactId
const dependencyRegex = /<dependency>\s*<groupId>([^<]+)<\/groupId>\s*<artifactId>([^<]+)<\/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<Set<string>> {
const directDeps = new Set<string>();

// 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<string[]> {
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<string> {
const directDeps = new Set<string>();
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<Set<string>> {
const directDeps = new Set<string>();

// 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<PackageDescription[]> {
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));
})
);

const fulfilled = allPackages.filter((x): x is PromiseFulfilledResult<PackageDescription[]> => x.status === "fulfilled");
const failedPackageCount = allPackages.length - fulfilled.length;
if (failedPackageCount > 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<string> | 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<string>();
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<UpgradeIssue[]> {
Expand Down
6 changes: 3 additions & 3 deletions src/upgrade/utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
}
}
}
Expand Down
Loading