|
1 | 1 | // Copyright (c) Microsoft Corporation. All rights reserved. |
2 | 2 | // Licensed under the MIT license. |
3 | 3 |
|
| 4 | +import * as fs from 'fs'; |
| 5 | +import * as path from 'path'; |
4 | 6 | import * as semver from 'semver'; |
| 7 | +import { Uri } from 'vscode'; |
5 | 8 | import { Jdtls } from "../java/jdtls"; |
6 | 9 | import { NodeKind, type INodeData } from "../java/nodeData"; |
7 | 10 | import { type DependencyCheckItem, type UpgradeIssue, type PackageDescription, UpgradeReason } from "./type"; |
@@ -145,7 +148,7 @@ async function getDependencyIssues(dependencies: PackageDescription[]): Promise< |
145 | 148 |
|
146 | 149 | async function getProjectIssues(projectNode: INodeData): Promise<UpgradeIssue[]> { |
147 | 150 | const issues: UpgradeIssue[] = []; |
148 | | - const dependencies = await getAllDependencies(projectNode); |
| 151 | + const dependencies = await getDirectDependencies(projectNode); |
149 | 152 | issues.push(...await getCVEIssues(dependencies)); |
150 | 153 | issues.push(...getJavaIssues(projectNode)); |
151 | 154 | issues.push(...await getDependencyIssues(dependencies)); |
@@ -175,30 +178,181 @@ async function getWorkspaceIssues(workspaceFolderUri: string): Promise<UpgradeIs |
175 | 178 | return workspaceIssues; |
176 | 179 | } |
177 | 180 |
|
178 | | -async function getAllDependencies(projectNode: INodeData): Promise<PackageDescription[]> { |
| 181 | +const MAVEN_CONTAINER_PATH = "org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"; |
| 182 | +const GRADLE_CONTAINER_PATH = "org.eclipse.buildship.core.gradleclasspathcontainer"; |
| 183 | + |
| 184 | +/** |
| 185 | + * Parse direct dependencies from pom.xml file. |
| 186 | + * Also checks parent pom.xml for multi-module projects. |
| 187 | + */ |
| 188 | +function parseDirectDependenciesFromPom(pomPath: string): Set<string> { |
| 189 | + const directDeps = new Set<string>(); |
| 190 | + try { |
| 191 | + const pomContent = fs.readFileSync(pomPath, 'utf-8'); |
| 192 | + |
| 193 | + // Extract dependencies from <dependencies> section (not inside <dependencyManagement>) |
| 194 | + // First, remove dependencyManagement sections to avoid including managed deps |
| 195 | + const withoutDepMgmt = pomContent.replace(/<dependencyManagement>[\s\S]*?<\/dependencyManagement>/g, ''); |
| 196 | + |
| 197 | + // Match <dependency> blocks and extract groupId and artifactId |
| 198 | + const dependencyRegex = /<dependency>\s*<groupId>([^<]+)<\/groupId>\s*<artifactId>([^<]+)<\/artifactId>/g; |
| 199 | + let match; |
| 200 | + while ((match = dependencyRegex.exec(withoutDepMgmt)) !== null) { |
| 201 | + const groupId = match[1].trim(); |
| 202 | + const artifactId = match[2].trim(); |
| 203 | + // Skip property references like ${project.groupId} |
| 204 | + if (!groupId.includes('${') && !artifactId.includes('${')) { |
| 205 | + directDeps.add(`${groupId}:${artifactId}`); |
| 206 | + } |
| 207 | + } |
| 208 | + |
| 209 | + // Check for parent pom in multi-module projects |
| 210 | + const parentPomPath = path.join(path.dirname(pomPath), '..', 'pom.xml'); |
| 211 | + if (fs.existsSync(parentPomPath)) { |
| 212 | + const parentDeps = parseDirectDependenciesFromPom(parentPomPath); |
| 213 | + parentDeps.forEach(dep => directDeps.add(dep)); |
| 214 | + } |
| 215 | + } catch { |
| 216 | + // If we can't read the pom, return empty set |
| 217 | + } |
| 218 | + return directDeps; |
| 219 | +} |
| 220 | + |
| 221 | +/** |
| 222 | + * Parse direct dependencies from build.gradle or build.gradle.kts file |
| 223 | + */ |
| 224 | +function parseDirectDependenciesFromGradle(gradlePath: string): Set<string> { |
| 225 | + const directDeps = new Set<string>(); |
| 226 | + try { |
| 227 | + const gradleContent = fs.readFileSync(gradlePath, 'utf-8'); |
| 228 | + |
| 229 | + // Match common dependency configurations: |
| 230 | + // implementation 'group:artifact:version' |
| 231 | + // implementation "group:artifact:version" |
| 232 | + // api 'group:artifact:version' |
| 233 | + // compileOnly, runtimeOnly, testImplementation, etc. |
| 234 | + const shortFormRegex = /(?:implementation|api|compile|compileOnly|runtimeOnly|testImplementation|testCompileOnly|testRuntimeOnly)\s*\(?['"]([^:'"]+):([^:'"]+)(?::[^'"]*)?['"]\)?/g; |
| 235 | + let match; |
| 236 | + while ((match = shortFormRegex.exec(gradleContent)) !== null) { |
| 237 | + const groupId = match[1].trim(); |
| 238 | + const artifactId = match[2].trim(); |
| 239 | + if (!groupId.includes('$') && !artifactId.includes('$')) { |
| 240 | + directDeps.add(`${groupId}:${artifactId}`); |
| 241 | + } |
| 242 | + } |
| 243 | + |
| 244 | + // Match map notation: implementation group: 'x', name: 'y', version: 'z' |
| 245 | + const mapFormRegex = /(?:implementation|api|compile|compileOnly|runtimeOnly|testImplementation|testCompileOnly|testRuntimeOnly)\s*\(?group:\s*['"]([^'"]+)['"]\s*,\s*name:\s*['"]([^'"]+)['"]/g; |
| 246 | + while ((match = mapFormRegex.exec(gradleContent)) !== null) { |
| 247 | + const groupId = match[1].trim(); |
| 248 | + const artifactId = match[2].trim(); |
| 249 | + if (!groupId.includes('$') && !artifactId.includes('$')) { |
| 250 | + directDeps.add(`${groupId}:${artifactId}`); |
| 251 | + } |
| 252 | + } |
| 253 | + } catch { |
| 254 | + // If we can't read the gradle file, return empty set |
| 255 | + } |
| 256 | + return directDeps; |
| 257 | +} |
| 258 | + |
| 259 | +/** |
| 260 | + * Find the build file (pom.xml or build.gradle) for a project |
| 261 | + */ |
| 262 | +function findBuildFile(projectUri: string | undefined): { path: string; type: 'maven' | 'gradle' } | null { |
| 263 | + if (!projectUri) { |
| 264 | + return null; |
| 265 | + } |
| 266 | + try { |
| 267 | + const projectPath = Uri.parse(projectUri).fsPath; |
| 268 | + |
| 269 | + // Check for Maven |
| 270 | + const pomPath = path.join(projectPath, 'pom.xml'); |
| 271 | + if (fs.existsSync(pomPath)) { |
| 272 | + return { path: pomPath, type: 'maven' }; |
| 273 | + } |
| 274 | + |
| 275 | + // Check for Gradle Kotlin DSL |
| 276 | + const gradleKtsPath = path.join(projectPath, 'build.gradle.kts'); |
| 277 | + if (fs.existsSync(gradleKtsPath)) { |
| 278 | + return { path: gradleKtsPath, type: 'gradle' }; |
| 279 | + } |
| 280 | + |
| 281 | + // Check for Gradle Groovy DSL |
| 282 | + const gradlePath = path.join(projectPath, 'build.gradle'); |
| 283 | + if (fs.existsSync(gradlePath)) { |
| 284 | + return { path: gradlePath, type: 'gradle' }; |
| 285 | + } |
| 286 | + } catch { |
| 287 | + // Ignore errors |
| 288 | + } |
| 289 | + return null; |
| 290 | +} |
| 291 | + |
| 292 | +/** |
| 293 | + * Parse direct dependencies from build file (Maven or Gradle) |
| 294 | + */ |
| 295 | +function parseDirectDependencies(buildFile: { path: string; type: 'maven' | 'gradle' }): Set<string> { |
| 296 | + if (buildFile.type === 'maven') { |
| 297 | + return parseDirectDependenciesFromPom(buildFile.path); |
| 298 | + } else { |
| 299 | + return parseDirectDependenciesFromGradle(buildFile.path); |
| 300 | + } |
| 301 | +} |
| 302 | + |
| 303 | +async function getDirectDependencies(projectNode: INodeData): Promise<PackageDescription[]> { |
179 | 304 | const projectStructureData = await Jdtls.getPackageData({ kind: NodeKind.Project, projectUri: projectNode.uri }); |
180 | | - const packageContainers = projectStructureData.filter(x => x.kind === NodeKind.Container); |
| 305 | + // Only include Maven or Gradle containers (not JRE or other containers) |
| 306 | + const dependencyContainers = projectStructureData.filter(x => |
| 307 | + x.kind === NodeKind.Container && |
| 308 | + (x.path?.startsWith(MAVEN_CONTAINER_PATH) || x.path?.startsWith(GRADLE_CONTAINER_PATH)) |
| 309 | + ); |
| 310 | + |
| 311 | + // Get direct dependency identifiers from build file |
| 312 | + const buildFile = findBuildFile(projectNode.uri); |
| 313 | + const directDependencyIds = buildFile ? parseDirectDependencies(buildFile) : null; |
181 | 314 |
|
182 | 315 | const allPackages = await Promise.allSettled( |
183 | | - packageContainers.map(async (packageContainer) => { |
| 316 | + dependencyContainers.map(async (packageContainer) => { |
184 | 317 | const packageNodes = await Jdtls.getPackageData({ |
185 | 318 | kind: NodeKind.Container, |
186 | 319 | projectUri: projectNode.uri, |
187 | 320 | path: packageContainer.path, |
188 | 321 | }); |
189 | | - return packageNodes.map(packageNodeToDescription).filter((x): x is PackageDescription => Boolean(x)); |
| 322 | + return packageNodes |
| 323 | + .map(packageNodeToDescription) |
| 324 | + .filter((x): x is PackageDescription => Boolean(x)); |
190 | 325 | }) |
191 | 326 | ); |
192 | 327 |
|
193 | 328 | const fulfilled = allPackages.filter((x): x is PromiseFulfilledResult<PackageDescription[]> => x.status === "fulfilled"); |
194 | 329 | const failedPackageCount = allPackages.length - fulfilled.length; |
195 | 330 | if (failedPackageCount > 0) { |
196 | 331 | sendInfo("", { |
197 | | - operationName: "java.dependency.assessmentManager.getAllDependencies.rejected", |
| 332 | + operationName: "java.dependency.assessmentManager.getDirectDependencies.rejected", |
198 | 333 | failedPackageCount: String(failedPackageCount), |
199 | 334 | }); |
200 | 335 | } |
201 | | - return fulfilled.map(x => x.value).flat(); |
| 336 | + |
| 337 | + let dependencies = fulfilled.map(x => x.value).flat(); |
| 338 | + |
| 339 | + // Filter to only direct dependencies if we have build file info |
| 340 | + if (directDependencyIds && directDependencyIds.size > 0) { |
| 341 | + dependencies = dependencies.filter(pkg => |
| 342 | + directDependencyIds.has(`${pkg.groupId}:${pkg.artifactId}`) |
| 343 | + ); |
| 344 | + } |
| 345 | + |
| 346 | + // Deduplicate by GAV coordinates |
| 347 | + const seen = new Set<string>(); |
| 348 | + return dependencies.filter(pkg => { |
| 349 | + const key = `${pkg.groupId}:${pkg.artifactId}:${pkg.version}`; |
| 350 | + if (seen.has(key)) { |
| 351 | + return false; |
| 352 | + } |
| 353 | + seen.add(key); |
| 354 | + return true; |
| 355 | + }); |
202 | 356 | } |
203 | 357 |
|
204 | 358 | async function getCVEIssues(dependencies: PackageDescription[]): Promise<UpgradeIssue[]> { |
|
0 commit comments