@@ -181,7 +181,7 @@ public RewriteRpcServer(RecipeMarketplace marketplace)
181181 }
182182
183183 [ JsonRpcMethod ( "ParseSolution" , UseSingleObjectParameterDeserialization = true ) ]
184- public async Task < List < ParseSolutionResponseItem > > ParseSolution ( ParseSolutionRequest request )
184+ public async Task < ParseSolutionResponse > ParseSolution ( ParseSolutionRequest request )
185185 {
186186 Log . Debug ( "RPC ParseSolution: received request path={Path} rootDir={RootDir}" , request . Path , request . RootDir ) ;
187187 var solutionParser = new SolutionParser ( ) ;
@@ -190,7 +190,7 @@ public async Task<List<ParseSolutionResponseItem>> ParseSolution(ParseSolutionRe
190190
191191 var solution = await solutionParser . LoadAsync ( path , CancellationToken . None ) ;
192192
193- var items = new List < ParseSolutionResponseItem > ( ) ;
193+ var response = new ParseSolutionResponse ( ) ;
194194 var seenProjects = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
195195 var projectList = solution . Projects . Where ( p => p . FilePath != null ) . ToList ( ) ;
196196 Log . Debug ( "RPC ParseSolution: {ProjectCount} projects to parse" , projectList . Count ) ;
@@ -221,17 +221,299 @@ public async Task<List<ParseSolutionResponseItem>> ParseSolution(ParseSolutionRe
221221 {
222222 var id = cu . Id . ToString ( ) ;
223223 _localObjects [ id ] = cu ;
224- items . Add ( new ParseSolutionResponseItem
224+ response . Items . Add ( new ParseSolutionResponseItem
225225 {
226226 Id = id ,
227227 SourceFileType = "org.openrewrite.csharp.tree.Cs$CompilationUnit" ,
228228 ProjectPath = project . FilePath !
229229 } ) ;
230230 }
231+
232+ // Extract MSBuild project metadata from .csproj
233+ try
234+ {
235+ var metadata = ExtractProjectMetadata ( project . FilePath ! , rootDir ) ;
236+ response . Projects . Add ( metadata ) ;
237+ }
238+ catch ( Exception ex )
239+ {
240+ Log . Debug ( "RPC ParseSolution: failed to extract metadata for {ProjectPath}: {ExType}: {ExMessage}" ,
241+ project . FilePath , ex . GetType ( ) . Name , ex . Message ) ;
242+ }
243+ }
244+
245+ Log . Debug ( "RPC ParseSolution: completed, {ItemCount} compilation units, {ProjectCount} project metadata" ,
246+ response . Items . Count , response . Projects . Count ) ;
247+ return response ;
248+ }
249+
250+ /// <summary>
251+ /// Extracts MSBuild project metadata from a .csproj file by parsing its XML
252+ /// and reading the resolved dependency tree from project.assets.json.
253+ /// </summary>
254+ private static ProjectMetadata ExtractProjectMetadata ( string projectPath , string rootDir )
255+ {
256+ var doc = XDocument . Load ( projectPath ) ;
257+ var root = doc . Root ! ;
258+ var ns = root . Name . Namespace ;
259+
260+ var relativePath = Path . GetRelativePath ( rootDir , projectPath ) ;
261+
262+ var metadata = new ProjectMetadata
263+ {
264+ ProjectPath = relativePath ,
265+ Sdk = root . Attribute ( "Sdk" ) ? . Value
266+ } ;
267+
268+ // Extract TargetFramework(s)
269+ var tfmElement = root . Descendants ( ns + "TargetFramework" ) . FirstOrDefault ( ) ;
270+ var tfmsElement = root . Descendants ( ns + "TargetFrameworks" ) . FirstOrDefault ( ) ;
271+
272+ var frameworks = new List < string > ( ) ;
273+ if ( tfmsElement != null )
274+ {
275+ foreach ( var tfm in tfmsElement . Value . Split ( ';' , StringSplitOptions . RemoveEmptyEntries | StringSplitOptions . TrimEntries ) )
276+ frameworks . Add ( tfm ) ;
277+ }
278+ else if ( tfmElement != null )
279+ {
280+ frameworks . Add ( tfmElement . Value . Trim ( ) ) ;
281+ }
282+
283+ // Extract PackageReferences
284+ var packageRefs = root . Descendants ( ns + "PackageReference" )
285+ . Select ( e => new PackageReferenceEntry
286+ {
287+ Include = e . Attribute ( "Include" ) ? . Value ?? "" ,
288+ RequestedVersion = e . Attribute ( "Version" ) ? . Value ,
289+ ResolvedVersion = e . Attribute ( "Version" ) ? . Value // raw = resolved in XML context
290+ } )
291+ . Where ( r => ! string . IsNullOrEmpty ( r . Include ) )
292+ . ToList ( ) ;
293+
294+ // Extract ProjectReferences
295+ var projectRefs = root . Descendants ( ns + "ProjectReference" )
296+ . Select ( e => new ProjectReferenceEntry
297+ {
298+ Include = e . Attribute ( "Include" ) ? . Value ?? ""
299+ } )
300+ . Where ( r => ! string . IsNullOrEmpty ( r . Include ) )
301+ . ToList ( ) ;
302+
303+ // Read resolved packages from project.assets.json
304+ var assetsPath = Path . Combine ( Path . GetDirectoryName ( projectPath ) ! , "obj" , "project.assets.json" ) ;
305+ var resolvedByTfm = new Dictionary < string , List < ResolvedPackageEntry > > ( ) ;
306+ if ( File . Exists ( assetsPath ) )
307+ {
308+ try
309+ {
310+ resolvedByTfm = ReadResolvedPackages ( assetsPath ) ;
311+ }
312+ catch ( Exception ex )
313+ {
314+ Log . Debug ( "Failed to read project.assets.json at {Path}: {Ex}" , assetsPath , ex . Message ) ;
315+ }
316+ }
317+
318+ // Extract properties with provenance
319+ foreach ( var propGroup in root . Descendants ( ns + "PropertyGroup" ) )
320+ {
321+ foreach ( var prop in propGroup . Elements ( ) )
322+ {
323+ var propName = prop . Name . LocalName ;
324+ if ( ! metadata . Properties . ContainsKey ( propName ) )
325+ {
326+ metadata . Properties [ propName ] = new PropertyEntry
327+ {
328+ Value = prop . Value ,
329+ DefinedIn = relativePath
330+ } ;
331+ }
332+ }
231333 }
232334
233- Log . Debug ( "RPC ParseSolution: completed, {ItemCount} compilation units total" , items . Count ) ;
234- return items ;
335+ // Discover NuGet package sources from nuget.config files
336+ metadata . PackageSources = FindNuGetPackageSources ( projectPath , rootDir ) ;
337+
338+ // Build per-TFM metadata
339+ foreach ( var tfm in frameworks )
340+ {
341+ resolvedByTfm . TryGetValue ( tfm , out var resolved ) ;
342+ metadata . TargetFrameworks . Add ( new TargetFrameworkEntry
343+ {
344+ TargetFramework = tfm ,
345+ PackageReferences = packageRefs ,
346+ ResolvedPackages = resolved ?? new List < ResolvedPackageEntry > ( ) ,
347+ ProjectReferences = projectRefs
348+ } ) ;
349+ }
350+
351+ // If no frameworks found, still include a default entry
352+ if ( frameworks . Count == 0 )
353+ {
354+ metadata . TargetFrameworks . Add ( new TargetFrameworkEntry
355+ {
356+ PackageReferences = packageRefs ,
357+ ProjectReferences = projectRefs
358+ } ) ;
359+ }
360+
361+ return metadata ;
362+ }
363+
364+ /// <summary>
365+ /// Discovers NuGet package sources by walking up from the project directory
366+ /// to the repository root looking for nuget.config files.
367+ /// NuGet resolves sources hierarchically — closest config wins.
368+ /// </summary>
369+ private static List < PackageSourceEntry > FindNuGetPackageSources ( string projectPath , string rootDir )
370+ {
371+ var sources = new List < PackageSourceEntry > ( ) ;
372+ var dir = Path . GetDirectoryName ( projectPath ) ;
373+
374+ while ( dir != null && dir . StartsWith ( rootDir , StringComparison . OrdinalIgnoreCase ) )
375+ {
376+ var configPath = Path . Combine ( dir , "nuget.config" ) ;
377+ // Case-insensitive check (NuGet.Config, nuget.config, NuGet.config all valid)
378+ if ( ! File . Exists ( configPath ) )
379+ {
380+ configPath = Path . Combine ( dir , "NuGet.Config" ) ;
381+ if ( ! File . Exists ( configPath ) )
382+ {
383+ configPath = Path . Combine ( dir , "NuGet.config" ) ;
384+ }
385+ }
386+
387+ if ( File . Exists ( configPath ) )
388+ {
389+ try
390+ {
391+ var configDoc = XDocument . Load ( configPath ) ;
392+ var packageSources = configDoc . Root ?
393+ . Element ( "packageSources" ) ?
394+ . Elements ( "add" ) ;
395+
396+ if ( packageSources != null )
397+ {
398+ foreach ( var source in packageSources )
399+ {
400+ var key = source . Attribute ( "key" ) ? . Value ;
401+ var url = source . Attribute ( "value" ) ? . Value ;
402+ if ( key != null && url != null &&
403+ ! sources . Any ( s => s . Key == key ) )
404+ {
405+ sources . Add ( new PackageSourceEntry
406+ {
407+ Key = key ,
408+ Url = url
409+ } ) ;
410+ }
411+ }
412+ }
413+ }
414+ catch ( Exception ex )
415+ {
416+ Log . Debug ( "Failed to parse nuget.config at {Path}: {Ex}" , configPath , ex . Message ) ;
417+ }
418+ // NuGet uses the closest config — stop walking up once we find one
419+ break ;
420+ }
421+
422+ if ( dir == rootDir ) break ;
423+ dir = Path . GetDirectoryName ( dir ) ;
424+ }
425+
426+ // Default to nuget.org if no sources found
427+ if ( sources . Count == 0 )
428+ {
429+ sources . Add ( new PackageSourceEntry
430+ {
431+ Key = "nuget.org" ,
432+ Url = "https://api.nuget.org/v3/index.json"
433+ } ) ;
434+ }
435+
436+ return sources ;
437+ }
438+
439+ /// <summary>
440+ /// Reads the resolved dependency tree from project.assets.json.
441+ /// Returns a dictionary keyed by target framework moniker.
442+ /// </summary>
443+ private static Dictionary < string , List < ResolvedPackageEntry > > ReadResolvedPackages ( string assetsPath )
444+ {
445+ var result = new Dictionary < string , List < ResolvedPackageEntry > > ( ) ;
446+ var json = JObject . Parse ( File . ReadAllText ( assetsPath ) ) ;
447+ var targets = json [ "targets" ] as JObject ;
448+ if ( targets == null ) return result ;
449+
450+ foreach ( var ( tfmKey , tfmValue ) in targets )
451+ {
452+ // tfmKey is like "net8.0" or ".NETCoreApp,Version=v8.0"
453+ var tfm = tfmKey . Contains ( ',' )
454+ ? NormalizeTfm ( tfmKey )
455+ : tfmKey ;
456+
457+ var packages = new List < ResolvedPackageEntry > ( ) ;
458+ if ( tfmValue is JObject tfmObj )
459+ {
460+ foreach ( var ( pkgKey , pkgValue ) in tfmObj )
461+ {
462+ // pkgKey is "PackageName/Version"
463+ var parts = pkgKey . Split ( '/' , 2 ) ;
464+ if ( parts . Length != 2 ) continue ;
465+
466+ var type = pkgValue ? [ "type" ] ? . Value < string > ( ) ;
467+ if ( type != "package" ) continue ;
468+
469+ var deps = new List < ResolvedPackageEntry > ( ) ;
470+ var dependencies = pkgValue ? [ "dependencies" ] as JObject ;
471+ if ( dependencies != null )
472+ {
473+ foreach ( var ( depName , depVersion ) in dependencies )
474+ {
475+ deps . Add ( new ResolvedPackageEntry
476+ {
477+ Name = depName ,
478+ ResolvedVersion = depVersion ? . Value < string > ( ) ?? "" ,
479+ Depth = 1
480+ } ) ;
481+ }
482+ }
483+
484+ packages . Add ( new ResolvedPackageEntry
485+ {
486+ Name = parts [ 0 ] ,
487+ ResolvedVersion = parts [ 1 ] ,
488+ Dependencies = deps ,
489+ Depth = 0
490+ } ) ;
491+ }
492+ }
493+
494+ result [ tfm ] = packages ;
495+ }
496+
497+ return result ;
498+ }
499+
500+ /// <summary>
501+ /// Normalizes a full framework identifier like ".NETCoreApp,Version=v8.0" to "net8.0".
502+ /// </summary>
503+ private static string NormalizeTfm ( string fullTfm )
504+ {
505+ // Simple heuristic: extract the version part
506+ if ( fullTfm . StartsWith ( ".NETCoreApp,Version=v" ) || fullTfm . StartsWith ( ".NETCoreApp,Version=V" ) )
507+ {
508+ var version = fullTfm . Substring ( ".NETCoreApp,Version=v" . Length ) ;
509+ return "net" + version ;
510+ }
511+ if ( fullTfm . StartsWith ( ".NETStandard,Version=v" ) || fullTfm . StartsWith ( ".NETStandard,Version=V" ) )
512+ {
513+ var version = fullTfm . Substring ( ".NETStandard,Version=v" . Length ) ;
514+ return "netstandard" + version ;
515+ }
516+ return fullTfm ;
235517 }
236518
237519 /// <summary>
@@ -1284,13 +1566,68 @@ public class ParseSolutionRequest
12841566 public string RootDir { get ; set ; } = "" ;
12851567}
12861568
1569+ public class ParseSolutionResponse
1570+ {
1571+ public List < ParseSolutionResponseItem > Items { get ; set ; } = new ( ) ;
1572+ public List < ProjectMetadata > Projects { get ; set ; } = new ( ) ;
1573+ }
1574+
12871575public class ParseSolutionResponseItem
12881576{
12891577 public string Id { get ; set ; } = "" ;
12901578 public string SourceFileType { get ; set ; } = "" ;
12911579 public string ProjectPath { get ; set ; } = "" ;
12921580}
12931581
1582+ public class ProjectMetadata
1583+ {
1584+ public string ProjectPath { get ; set ; } = "" ;
1585+ public string ? Sdk { get ; set ; }
1586+ public Dictionary < string , PropertyEntry > Properties { get ; set ; } = new ( ) ;
1587+ public List < TargetFrameworkEntry > TargetFrameworks { get ; set ; } = new ( ) ;
1588+ public List < PackageSourceEntry > PackageSources { get ; set ; } = new ( ) ;
1589+ }
1590+
1591+ public class PackageSourceEntry
1592+ {
1593+ public string Key { get ; set ; } = "" ;
1594+ public string Url { get ; set ; } = "" ;
1595+ }
1596+
1597+ public class PropertyEntry
1598+ {
1599+ public string Value { get ; set ; } = "" ;
1600+ public string ? DefinedIn { get ; set ; }
1601+ }
1602+
1603+ public class TargetFrameworkEntry
1604+ {
1605+ public string TargetFramework { get ; set ; } = "" ;
1606+ public List < PackageReferenceEntry > PackageReferences { get ; set ; } = new ( ) ;
1607+ public List < ResolvedPackageEntry > ResolvedPackages { get ; set ; } = new ( ) ;
1608+ public List < ProjectReferenceEntry > ProjectReferences { get ; set ; } = new ( ) ;
1609+ }
1610+
1611+ public class PackageReferenceEntry
1612+ {
1613+ public string Include { get ; set ; } = "" ;
1614+ public string ? RequestedVersion { get ; set ; }
1615+ public string ? ResolvedVersion { get ; set ; }
1616+ }
1617+
1618+ public class ResolvedPackageEntry
1619+ {
1620+ public string Name { get ; set ; } = "" ;
1621+ public string ResolvedVersion { get ; set ; } = "" ;
1622+ public List < ResolvedPackageEntry > Dependencies { get ; set ; } = new ( ) ;
1623+ public int Depth { get ; set ; }
1624+ }
1625+
1626+ public class ProjectReferenceEntry
1627+ {
1628+ public string Include { get ; set ; } = "" ;
1629+ }
1630+
12941631public class GetObjectRequest
12951632{
12961633 public string Id { get ; set ; } = "" ;
0 commit comments