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
229 changes: 221 additions & 8 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 All @@ -11,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"];
Expand Down Expand Up @@ -145,13 +152,18 @@ 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);
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<UpgradeIssue[]> {
Expand All @@ -175,30 +187,231 @@ async function getWorkspaceIssues(workspaceFolderUri: string): Promise<UpgradeIs
return workspaceIssues;
}

async function getAllDependencies(projectNode: INodeData): Promise<PackageDescription[]> {
/**
* 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 = 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
}
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 = 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;
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
}
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(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(ContainerPath.Maven));

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
12 changes: 0 additions & 12 deletions src/upgrade/upgradeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
import { Settings } from "../settings";
import assessmentManager from "./assessmentManager";
import { checkOrInstallAppModExtensionForUpgrade, checkOrPopupToInstallAppModExtensionForModernization } from "./utility";
import { NodeKind } from "../../extension.bundle";

Check failure on line 15 in src/upgrade/upgradeManager.ts

View workflow job for this annotation

GitHub Actions / Windows

'NodeKind' is declared but its value is never read.

Check failure on line 15 in src/upgrade/upgradeManager.ts

View workflow job for this annotation

GitHub Actions / Windows-UI

'NodeKind' is declared but its value is never read.

Check failure on line 15 in src/upgrade/upgradeManager.ts

View workflow job for this annotation

GitHub Actions / Linux-UI

'NodeKind' is declared but its value is never read.

Check failure on line 15 in src/upgrade/upgradeManager.ts

View workflow job for this annotation

GitHub Actions / Linux

'NodeKind' is declared but its value is never read.

Check failure on line 15 in src/upgrade/upgradeManager.ts

View workflow job for this annotation

GitHub Actions / macOS

'NodeKind' is declared but its value is never read.
import { ContainerPath } from "../views/containerNode";

Check failure on line 16 in src/upgrade/upgradeManager.ts

View workflow job for this annotation

GitHub Actions / Windows

'ContainerPath' is declared but its value is never read.

Check failure on line 16 in src/upgrade/upgradeManager.ts

View workflow job for this annotation

GitHub Actions / Windows-UI

'ContainerPath' is declared but its value is never read.

Check failure on line 16 in src/upgrade/upgradeManager.ts

View workflow job for this annotation

GitHub Actions / Linux-UI

'ContainerPath' is declared but its value is never read.

Check failure on line 16 in src/upgrade/upgradeManager.ts

View workflow job for this annotation

GitHub Actions / Linux

'ContainerPath' is declared but its value is never read.

Check failure on line 16 in src/upgrade/upgradeManager.ts

View workflow job for this annotation

GitHub Actions / macOS

'ContainerPath' is declared but its value is never read.

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

Expand Down Expand Up @@ -61,18 +61,6 @@
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) {
Expand Down
Loading