11// Copyright (c) Microsoft Corporation. All rights reserved.
22// Licensed under the MIT license.
33
4+ import * as fs from 'fs' ;
45import * as semver from 'semver' ;
6+ import { globby } from 'globby' ;
7+
8+ import { Uri } from 'vscode' ;
59import { Jdtls } from "../java/jdtls" ;
610import { NodeKind , type INodeData } from "../java/nodeData" ;
711import { type DependencyCheckItem , type UpgradeIssue , type PackageDescription , UpgradeReason } from "./type" ;
@@ -11,6 +15,7 @@ import { buildPackageId } from './utility';
1115import metadataManager from './metadataManager' ;
1216import { sendInfo } from 'vscode-extension-telemetry-wrapper' ;
1317import { batchGetCVEIssues } from './cve' ;
18+ import { ContainerPath } from '../views/containerNode' ;
1419
1520function packageNodeToDescription ( node : INodeData ) : PackageDescription | null {
1621 const version = node . metaData ?. [ "maven.version" ] ;
@@ -143,62 +148,238 @@ async function getDependencyIssues(dependencies: PackageDescription[]): Promise<
143148 return issues ;
144149}
145150
146- async function getProjectIssues ( projectNode : INodeData ) : Promise < UpgradeIssue [ ] > {
147- const issues : UpgradeIssue [ ] = [ ] ;
148- const dependencies = await getAllDependencies ( projectNode ) ;
149- issues . push ( ...await getCVEIssues ( dependencies ) ) ;
150- issues . push ( ...getJavaIssues ( projectNode ) ) ;
151- issues . push ( ...await getDependencyIssues ( dependencies ) ) ;
151+ async function getWorkspaceIssues ( projectDeps : { projectNode : INodeData , dependencies : PackageDescription [ ] } [ ] ) : Promise < UpgradeIssue [ ] > {
152152
153+ const issues : UpgradeIssue [ ] = [ ] ;
154+ const dependencyMap : Map < string , PackageDescription > = new Map ( ) ;
155+ for ( const { projectNode, dependencies } of projectDeps ) {
156+ issues . push ( ...getJavaIssues ( projectNode ) ) ;
157+ for ( const dep of dependencies ) {
158+ const key = `${ dep . groupId } :${ dep . artifactId } :${ dep . version ?? "" } ` ;
159+ if ( ! dependencyMap . has ( key ) ) {
160+ dependencyMap . set ( key , dep ) ;
161+ }
162+ }
163+ }
164+ const uniqueDependencies = Array . from ( dependencyMap . values ( ) ) ;
165+ issues . push ( ...await getCVEIssues ( uniqueDependencies ) ) ;
166+ issues . push ( ...await getDependencyIssues ( uniqueDependencies ) ) ;
153167 return issues ;
168+ }
154169
170+ /**
171+ * Find all pom.xml files in a directory using glob
172+ */
173+ async function findAllPomFiles ( dir : string ) : Promise < string [ ] > {
174+ try {
175+ return await globby ( '**/pom.xml' , {
176+ cwd : dir ,
177+ absolute : true ,
178+ ignore : [ '**/node_modules/**' , '**/target/**' , '**/.git/**' , '**/.idea/**' , '**/.vscode/**' ]
179+ } ) ;
180+ } catch {
181+ return [ ] ;
182+ }
155183}
156184
157- async function getWorkspaceIssues ( workspaceFolderUri : string ) : Promise < UpgradeIssue [ ] > {
158- const projects = await Jdtls . getProjects ( workspaceFolderUri ) ;
159- const projectsIssues = await Promise . allSettled ( projects . map ( async ( projectNode ) => {
160- const issues = await getProjectIssues ( projectNode ) ;
161- return issues ;
162- } ) ) ;
185+ /**
186+ * Parse dependencies from a single pom.xml file
187+ */
188+ function parseDependenciesFromSinglePom ( pomPath : string ) : Set < string > {
189+ // TODO : Use a proper XML parser if needed
190+ const directDeps = new Set < string > ( ) ;
191+ try {
192+ const pomContent = fs . readFileSync ( pomPath , 'utf-8' ) ;
193+
194+ // Extract dependencies from <dependencies> section (not inside <dependencyManagement>)
195+ // First, remove dependencyManagement sections to avoid including managed deps
196+ const withoutDepMgmt = pomContent . replace ( / < d e p e n d e n c y M a n a g e m e n t > [ \s \S ] * ?< \/ d e p e n d e n c y M a n a g e m e n t > / g, '' ) ;
163197
164- const workspaceIssues = projectsIssues . map ( x => {
165- if ( x . status === "fulfilled" ) {
166- return x . value ;
198+ // Match <dependency> blocks and extract groupId and artifactId
199+ const dependencyRegex = / < d e p e n d e n c y > \s * < g r o u p I d > ( [ ^ < ] + ) < \/ g r o u p I d > \s * < a r t i f a c t I d > ( [ ^ < ] + ) < \/ a r t i f a c t I d > / g;
200+ let match = dependencyRegex . exec ( withoutDepMgmt ) ;
201+ while ( match !== null ) {
202+ const groupId = match [ 1 ] . trim ( ) ;
203+ const artifactId = match [ 2 ] . trim ( ) ;
204+ // Skip property references like ${project.groupId}
205+ if ( ! groupId . includes ( '${' ) && ! artifactId . includes ( '${' ) ) {
206+ directDeps . add ( `${ groupId } :${ artifactId } ` ) ;
207+ }
208+ match = dependencyRegex . exec ( withoutDepMgmt ) ;
167209 }
210+ } catch {
211+ // If we can't read the pom, return empty set
212+ }
213+ return directDeps ;
214+ }
168215
169- sendInfo ( "" , {
170- operationName : "java.dependency.assessmentManager.getWorkspaceIssues" ,
216+ /**
217+ * Parse direct dependencies from all pom.xml files in the project.
218+ * Finds all pom.xml files starting from the project root and parses them to collect dependencies.
219+ */
220+ async function parseDirectDependenciesFromPom ( projectPath : string ) : Promise < Set < string > > {
221+ const directDeps = new Set < string > ( ) ;
222+
223+ // Find all pom.xml files in the project starting from the project root
224+ const allPomFiles = await findAllPomFiles ( projectPath ) ;
225+
226+ // Parse each pom.xml and collect dependencies
227+ for ( const pom of allPomFiles ) {
228+ const deps = parseDependenciesFromSinglePom ( pom ) ;
229+ deps . forEach ( dep => directDeps . add ( dep ) ) ;
230+ }
231+
232+ return directDeps ;
233+ }
234+
235+ /**
236+ * Find all Gradle build files in a directory using glob
237+ */
238+ async function findAllGradleFiles ( dir : string ) : Promise < string [ ] > {
239+ try {
240+ return await globby ( '**/{build.gradle,build.gradle.kts}' , {
241+ cwd : dir ,
242+ absolute : true ,
243+ ignore : [ '**/node_modules/**' , '**/build/**' , '**/.git/**' , '**/.idea/**' , '**/.vscode/**' , '**/.gradle/**' ]
171244 } ) ;
245+ } catch {
172246 return [ ] ;
173- } ) . flat ( ) ;
247+ }
248+ }
249+
250+ /**
251+ * Parse dependencies from a single Gradle build file
252+ */
253+ function parseDependenciesFromSingleGradle ( gradlePath : string ) : Set < string > {
254+ const directDeps = new Set < string > ( ) ;
255+ try {
256+ const gradleContent = fs . readFileSync ( gradlePath , 'utf-8' ) ;
257+
258+ // Match common dependency configurations:
259+ // implementation 'group:artifact:version'
260+ // implementation "group:artifact:version"
261+ // api 'group:artifact:version'
262+ // compileOnly, runtimeOnly, testImplementation, etc.
263+ const shortFormRegex = / (?: i m p l e m e n t a t i o n | a p i | c o m p i l e | c o m p i l e O n l y | r u n t i m e O n l y | t e s t I m p l e m e n t a t i o n | t e s t C o m p i l e O n l y | t e s t R u n t i m e O n l y ) \s * \( ? [ ' " ] ( [ ^ : ' " ] + ) : ( [ ^ : ' " ] + ) (?: : [ ^ ' " ] * ) ? [ ' " ] \) ? / g;
264+ let match = shortFormRegex . exec ( gradleContent ) ;
265+ while ( match !== null ) {
266+ const groupId = match [ 1 ] . trim ( ) ;
267+ const artifactId = match [ 2 ] . trim ( ) ;
268+ if ( ! groupId . includes ( '$' ) && ! artifactId . includes ( '$' ) ) {
269+ directDeps . add ( `${ groupId } :${ artifactId } ` ) ;
270+ }
271+ match = shortFormRegex . exec ( gradleContent ) ;
272+ }
273+
274+ // Match map notation: implementation group: 'x', name: 'y', version: 'z'
275+ const mapFormRegex = / (?: i m p l e m e n t a t i o n | a p i | c o m p i l e | c o m p i l e O n l y | r u n t i m e O n l y | t e s t I m p l e m e n t a t i o n | t e s t C o m p i l e O n l y | t e s t R u n t i m e O n l y ) \s * \( ? g r o u p : \s * [ ' " ] ( [ ^ ' " ] + ) [ ' " ] \s * , \s * n a m e : \s * [ ' " ] ( [ ^ ' " ] + ) [ ' " ] / g;
276+ match = mapFormRegex . exec ( gradleContent ) ;
277+ while ( match !== null ) {
278+ const groupId = match [ 1 ] . trim ( ) ;
279+ const artifactId = match [ 2 ] . trim ( ) ;
280+ if ( ! groupId . includes ( '$' ) && ! artifactId . includes ( '$' ) ) {
281+ directDeps . add ( `${ groupId } :${ artifactId } ` ) ;
282+ }
283+ match = mapFormRegex . exec ( gradleContent ) ;
284+ }
285+ } catch {
286+ // If we can't read the gradle file, return empty set
287+ }
288+ return directDeps ;
289+ }
290+
291+ /**
292+ * Parse direct dependencies from all Gradle build files in the project.
293+ * Finds all build.gradle and build.gradle.kts files and parses them to collect dependencies.
294+ */
295+ async function parseDirectDependenciesFromGradle ( projectPath : string ) : Promise < Set < string > > {
296+ const directDeps = new Set < string > ( ) ;
297+
298+ // Find all Gradle build files in the project
299+ const allGradleFiles = await findAllGradleFiles ( projectPath ) ;
300+
301+ // Parse each gradle file and collect dependencies
302+ for ( const gradleFile of allGradleFiles ) {
303+ const deps = parseDependenciesFromSingleGradle ( gradleFile ) ;
304+ deps . forEach ( dep => directDeps . add ( dep ) ) ;
305+ }
174306
175- return workspaceIssues ;
307+ return directDeps ;
176308}
177309
178- async function getAllDependencies ( projectNode : INodeData ) : Promise < PackageDescription [ ] > {
310+ export async function getDirectDependencies ( projectNode : INodeData ) : Promise < PackageDescription [ ] > {
179311 const projectStructureData = await Jdtls . getPackageData ( { kind : NodeKind . Project , projectUri : projectNode . uri } ) ;
180- const packageContainers = projectStructureData . filter ( x => x . kind === NodeKind . Container ) ;
312+ // Only include Maven or Gradle containers (not JRE or other containers)
313+ const dependencyContainers = projectStructureData . filter ( x =>
314+ x . kind === NodeKind . Container &&
315+ ( x . path ?. startsWith ( ContainerPath . Maven ) || x . path ?. startsWith ( ContainerPath . Gradle ) )
316+ ) ;
317+
318+ if ( dependencyContainers . length === 0 ) {
319+ return [ ] ;
320+ }
181321
182322 const allPackages = await Promise . allSettled (
183- packageContainers . map ( async ( packageContainer ) => {
323+ dependencyContainers . map ( async ( packageContainer ) => {
184324 const packageNodes = await Jdtls . getPackageData ( {
185325 kind : NodeKind . Container ,
186326 projectUri : projectNode . uri ,
187327 path : packageContainer . path ,
188328 } ) ;
189- return packageNodes . map ( packageNodeToDescription ) . filter ( ( x ) : x is PackageDescription => Boolean ( x ) ) ;
329+ return packageNodes
330+ . map ( packageNodeToDescription )
331+ . filter ( ( x ) : x is PackageDescription => Boolean ( x ) ) ;
190332 } )
191333 ) ;
192334
193335 const fulfilled = allPackages . filter ( ( x ) : x is PromiseFulfilledResult < PackageDescription [ ] > => x . status === "fulfilled" ) ;
194336 const failedPackageCount = allPackages . length - fulfilled . length ;
195337 if ( failedPackageCount > 0 ) {
196338 sendInfo ( "" , {
197- operationName : "java.dependency.assessmentManager.getAllDependencies .rejected" ,
339+ operationName : "java.dependency.assessmentManager.getDirectDependencies .rejected" ,
198340 failedPackageCount : String ( failedPackageCount ) ,
199341 } ) ;
200342 }
201- return fulfilled . map ( x => x . value ) . flat ( ) ;
343+
344+ let dependencies = fulfilled . map ( x => x . value ) . flat ( ) ;
345+
346+ if ( ! dependencies || dependencies . length === 0 ) {
347+ sendInfo ( "" , {
348+ operationName : "java.dependency.assessmentManager.getDirectDependencies.noDependencyInfo"
349+ } ) ;
350+ return [ ] ;
351+ }
352+
353+ // Determine build type from dependency containers
354+ const isMaven = dependencyContainers . some ( x => x . path ?. startsWith ( ContainerPath . Maven ) ) ;
355+ // Get direct dependency identifiers from build files
356+ let directDependencyIds : Set < string > | null = null ;
357+ if ( projectNode . uri && dependencyContainers . length > 0 ) {
358+ try {
359+ const projectPath = Uri . parse ( projectNode . uri ) . fsPath ;
360+ if ( isMaven ) {
361+ directDependencyIds = await parseDirectDependenciesFromPom ( projectPath ) ;
362+ } else {
363+ directDependencyIds = await parseDirectDependenciesFromGradle ( projectPath ) ;
364+ }
365+ } catch {
366+ // Ignore errors
367+ }
368+ }
369+
370+ if ( ! directDependencyIds || directDependencyIds . size === 0 ) {
371+ sendInfo ( "" , {
372+ operationName : "java.dependency.assessmentManager.getDirectDependencies.noDirectDependencyInfo"
373+ } ) ;
374+ // TODO: fallback to return all dependencies if we cannot parse direct dependencies or just return empty?
375+ return dependencies ;
376+ }
377+ // Filter to only direct dependencies if we have build file info
378+ dependencies = dependencies . filter ( pkg =>
379+ directDependencyIds ! . has ( `${ pkg . groupId } :${ pkg . artifactId } ` )
380+ ) ;
381+
382+ return dependencies ;
202383}
203384
204385async function getCVEIssues ( dependencies : PackageDescription [ ] ) : Promise < UpgradeIssue [ ] > {
0 commit comments