1717using NuGet . Versioning ;
1818using Polly ;
1919using Serilog . Core ;
20+ using System . Text . RegularExpressions ;
2021
2122namespace AXSharp . Compiler ;
2223
@@ -77,7 +78,7 @@ private static string MakeValidIdentifier(string name)
7778 {
7879 var fileName = string . Empty ;
7980
80- if ( SyntaxFacts . IsIdentifierPartCharacter ( name . FirstOrDefault ( ) ) ) fileName = $ "{ name . FirstOrDefault ( ) } ";
81+ if ( SyntaxFacts . IsIdentifierPartCharacter ( name . FirstOrDefault ( ) ) ) fileName = $ "{ name . FirstOrDefault ( ) } " ;
8182
8283 for ( var i = 1 ; i < name . Length ; i ++ )
8384 {
@@ -176,7 +177,7 @@ private void EnsureCsProjFile()
176177
177178 private static readonly string NugetDir =
178179 SettingsUtility . GetGlobalPackagesFolder ( Settings . LoadDefaultSettings ( null ) ) ;
179-
180+
180181
181182 /// <inheritdoc />
182183 public void ProvisionProjectStructure ( )
@@ -398,8 +399,10 @@ private static IEnumerable<IReference> GetDirectDependencies(string projectFile)
398399 {
399400 var csproj = XDocument . Load ( projectPath ) ;
400401 var csprojDir = FileDirectory ( projectFile ) ;
401- var nugets = PackageReferences ( csproj , projectFile ) ;
402- var projects = ProjectReferences ( csproj , csprojDir ) ;
402+ // Collect MSBuild properties (Directory.Build.props + project)
403+ var ( baseProperties , targetFrameworks ) = MsBuildConditionEvaluator . CollectBaseProperties ( projectPath , csproj ) ;
404+ var nugets = PackageReferences ( csproj , projectPath , targetFrameworks , baseProperties ) ;
405+ var projects = ProjectReferences ( csproj , csprojDir , targetFrameworks , baseProperties ) ;
403406 return nugets . Concat ( projects ) ;
404407 }
405408 catch ( Exception ex )
@@ -470,8 +473,9 @@ private static IEnumerable<IReference> GetProjectDependencies(ProjectReference p
470473 {
471474 var csproj = XDocument . Load ( project . ProjectFilePath ) ;
472475 var csprojDir = FileDirectory ( project . ProjectFilePath ) ;
473- var nugets = PackageReferences ( csproj , project . ProjectFilePath ) ;
474- var projects = ProjectReferences ( csproj , csprojDir ) ;
476+ var ( baseProps , tfs ) = MsBuildConditionEvaluator . CollectBaseProperties ( project . ProjectFilePath , csproj ) ;
477+ var nugets = PackageReferences ( csproj , project . ProjectFilePath , tfs , baseProps ) ;
478+ var projects = ProjectReferences ( csproj , csprojDir , tfs , baseProps ) ;
475479 project . References = nugets . Concat ( projects ) ;
476480 return project . References ;
477481 }
@@ -483,13 +487,30 @@ private static IEnumerable<IReference> GetProjectDependencies(ProjectReference p
483487 }
484488 }
485489
486- private static IEnumerable < IReference > PackageReferences ( XDocument csproj , string projectFile )
490+ private static IEnumerable < IReference > PackageReferences ( XDocument csproj , string projectFile , IEnumerable < string > targetFrameworks , Dictionary < string , string > baseProperties )
487491 {
488- return csproj
489- . Root !
490- . Elements ( "ItemGroup" )
491- . SelectMany ( ig => ig . Elements ( "PackageReference" ) )
492- . Select ( pr => PackageReference . CreateFromReferenceNode ( pr , projectFile ) ) ;
492+ var result = new List < IReference > ( ) ;
493+ var itemGroups = csproj . Root ! . Elements ( "ItemGroup" ) ;
494+ foreach ( var ig in itemGroups )
495+ {
496+ foreach ( var tf in targetFrameworks )
497+ {
498+ if ( ! MsBuildConditionEvaluator . ItemGroupConditionPasses ( ig , baseProperties , tf ) ) continue ;
499+ foreach ( var pr in ig . Elements ( "PackageReference" ) )
500+ {
501+ if ( ! MsBuildConditionEvaluator . ElementConditionPasses ( pr , baseProperties , tf ) ) continue ;
502+ try
503+ {
504+ result . Add ( PackageReference . CreateFromReferenceNode ( pr , projectFile ) ) ;
505+ }
506+ catch ( Exception e )
507+ {
508+ Log . Logger . Warning ( e , $ "Failed to parse PackageReference '{ pr } ' in '{ projectFile } '") ;
509+ }
510+ }
511+ }
512+ }
513+ return result ;
493514 }
494515
495516 private static string PackageReferenceNugetPath ( PackageReference package )
@@ -523,13 +544,25 @@ internal static string GetBestMatchedVersion(string packageName, string packageV
523544
524545 }
525546
526- private static IEnumerable < IReference > ProjectReferences ( XDocument csproj , string directory )
547+ private static IEnumerable < IReference > ProjectReferences ( XDocument csproj , string directory , IEnumerable < string > targetFrameworks , Dictionary < string , string > baseProperties )
527548 {
528- return csproj
529- . Root !
530- . Elements ( "ItemGroup" )
531- . SelectMany ( ig => ig . Elements ( "ProjectReference" ) )
532- . Select ( pr => new ProjectReference ( directory , pr . Attribute ( "Include" ) ! . Value . Replace ( "\\ " , Path . DirectorySeparatorChar . ToString ( ) ) ) ) ;
549+ var result = new List < IReference > ( ) ;
550+ var itemGroups = csproj . Root ! . Elements ( "ItemGroup" ) ;
551+ foreach ( var ig in itemGroups )
552+ {
553+ foreach ( var tf in targetFrameworks )
554+ {
555+ if ( ! MsBuildConditionEvaluator . ItemGroupConditionPasses ( ig , baseProperties , tf ) ) continue ;
556+ foreach ( var pr in ig . Elements ( "ProjectReference" ) )
557+ {
558+ if ( ! MsBuildConditionEvaluator . ElementConditionPasses ( pr , baseProperties , tf ) ) continue ;
559+ var include = pr . Attribute ( "Include" ) ? . Value . Replace ( "\\ " , Path . DirectorySeparatorChar . ToString ( ) ) ;
560+ if ( string . IsNullOrWhiteSpace ( include ) ) continue ;
561+ result . Add ( new ProjectReference ( directory , include ) ) ;
562+ }
563+ }
564+ }
565+ return result ;
533566 }
534567
535568 #endregion
@@ -563,4 +596,230 @@ public void GenerateCompanionData()
563596 }
564597 }
565598 }
599+
600+ /// <summary>
601+ /// Helper class for naïve MSBuild condition evaluation for dependency discovery.
602+ /// Supports equality/inequality with $(Property) and logical AND/OR operators.
603+ /// Only properties needed for typical ItemGroup conditions are handled (TargetFramework / Configuration etc.).
604+ /// </summary>
605+ private static class MsBuildConditionEvaluator
606+ {
607+ internal static ( Dictionary < string , string > properties , List < string > targetFrameworks ) CollectBaseProperties ( string projectFile , XDocument csproj )
608+ {
609+ var props = new Dictionary < string , string > ( StringComparer . OrdinalIgnoreCase ) ;
610+
611+ // Walk up directories gathering Directory.Build.props (outermost first)
612+ var dir = Path . GetDirectoryName ( projectFile ) ! ;
613+ var stack = new Stack < string > ( ) ;
614+ var current = dir ;
615+ while ( ! string . IsNullOrEmpty ( current ) )
616+ {
617+ var dbp = Path . Combine ( current , "Directory.Build.props" ) ;
618+ if ( File . Exists ( dbp ) ) stack . Push ( dbp ) ;
619+ var parent = Directory . GetParent ( current ) ;
620+ if ( parent == null ) break ;
621+ current = parent . FullName ;
622+ }
623+
624+ foreach ( var dbp in stack )
625+ {
626+ TryLoadProps ( dbp , props ) ;
627+ }
628+
629+ // Project properties override
630+ TryLoadProjectProperties ( csproj , props ) ;
631+
632+ // Derive target frameworks (TargetFramework overrides TargetFrameworks if present)
633+ var frameworks = new List < string > ( ) ;
634+ if ( props . TryGetValue ( "TargetFramework" , out var singleTf ) && ! string . IsNullOrWhiteSpace ( singleTf ) )
635+ {
636+ frameworks . Add ( singleTf . Trim ( ) ) ;
637+ }
638+ else if ( props . TryGetValue ( "TargetFrameworks" , out var tfs ) && ! string . IsNullOrWhiteSpace ( tfs ) )
639+ {
640+ frameworks . AddRange ( tfs . Split ( new [ ] { ';' } , StringSplitOptions . RemoveEmptyEntries | StringSplitOptions . TrimEntries ) ) ;
641+ }
642+ if ( frameworks . Count == 0 )
643+ {
644+ // default fallback
645+ frameworks . Add ( "netstandard2.0" ) ;
646+ }
647+ return ( props , frameworks ) ;
648+ }
649+
650+ private static void TryLoadProps ( string file , Dictionary < string , string > props )
651+ {
652+ try
653+ {
654+ var x = XDocument . Load ( file ) ;
655+ TryLoadProjectProperties ( x , props ) ;
656+ }
657+ catch { /* ignore */ }
658+ }
659+
660+ private static void TryLoadProjectProperties ( XDocument xdoc , Dictionary < string , string > props )
661+ {
662+ foreach ( var pg in xdoc . Root ! . Elements ( "PropertyGroup" ) )
663+ {
664+ // Ignore conditional property groups for now (most TF definitions are unconditional in props files)
665+ foreach ( var el in pg . Elements ( ) )
666+ {
667+ if ( ! el . HasElements && ! string . IsNullOrWhiteSpace ( el . Value ) )
668+ {
669+ props [ el . Name . LocalName ] = el . Value . Trim ( ) ;
670+ }
671+ }
672+ }
673+ }
674+
675+ internal static bool ItemGroupConditionPasses ( XElement itemGroup , Dictionary < string , string > baseProps , string targetFramework )
676+ => ConditionPasses ( itemGroup . Attribute ( "Condition" ) ? . Value , baseProps , targetFramework ) ;
677+
678+ internal static bool ElementConditionPasses ( XElement element , Dictionary < string , string > baseProps , string targetFramework )
679+ {
680+ var condition = element . Attribute ( "Condition" ) ? . Value ;
681+ var passes = ConditionPasses ( condition , baseProps , targetFramework ) ;
682+ if ( ! string . IsNullOrWhiteSpace ( condition ) )
683+ {
684+ try
685+ {
686+ var include = element . Attribute ( "Include" ) ? . Value ;
687+ Log . Logger . Debug ( $ "[MsBuildConditionEvaluator] Element '{ element . Name . LocalName } ' Include='{ include } ' condition='{ condition } ' tf='{ targetFramework } ' => { passes } ") ;
688+ }
689+ catch { /* ignore */ }
690+ }
691+ return passes ;
692+ }
693+
694+ private static bool ConditionPasses ( string ? condition , Dictionary < string , string > baseProps , string targetFramework )
695+ {
696+ if ( string . IsNullOrWhiteSpace ( condition ) ) return true ;
697+ try
698+ {
699+ // Fast path for simple TF equality/inequality expressions to avoid parser quirks
700+ var simpleTfEq = Regex . Match ( condition , @"^\s*'\$\(TargetFramework\)'\s*==\s*'([^']+)'\s*$" , RegexOptions . IgnoreCase ) ;
701+ if ( simpleTfEq . Success )
702+ {
703+ var expected = simpleTfEq . Groups [ 1 ] . Value . Trim ( ) ;
704+ var ok = string . Equals ( expected , targetFramework , StringComparison . OrdinalIgnoreCase ) ;
705+ Log . Logger . Debug ( $ "[MsBuildConditionEvaluator] simple == TF condition '{ condition } ' => { ok } ") ;
706+ if ( ! ok ) return false ; // short circuit
707+ }
708+ var simpleTfNe = Regex . Match ( condition , @"^\s*'\$\(TargetFramework\)'\s*!=\s*'([^']+)'\s*$" , RegexOptions . IgnoreCase ) ;
709+ if ( simpleTfNe . Success )
710+ {
711+ var notExpected = simpleTfNe . Groups [ 1 ] . Value . Trim ( ) ;
712+ var ok = ! string . Equals ( notExpected , targetFramework , StringComparison . OrdinalIgnoreCase ) ;
713+ Log . Logger . Debug ( $ "[MsBuildConditionEvaluator] simple != TF condition '{ condition } ' => { ok } ") ;
714+ if ( ! ok ) return false ; // short circuit
715+ }
716+
717+ var expanded = condition ;
718+ var props = new Dictionary < string , string > ( baseProps , StringComparer . OrdinalIgnoreCase )
719+ {
720+ [ "TargetFramework" ] = targetFramework
721+ } ;
722+ foreach ( var kvp in props )
723+ {
724+ expanded = expanded . Replace ( $ "$({ kvp . Key } )", kvp . Value , StringComparison . OrdinalIgnoreCase ) ;
725+ }
726+ var result = Evaluate ( expanded ) ;
727+ if ( expanded . Contains ( "==" ) )
728+ {
729+ var parts = expanded . Split ( new [ ] { "==" } , StringSplitOptions . RemoveEmptyEntries | StringSplitOptions . TrimEntries ) ;
730+ if ( parts . Length == 2 )
731+ {
732+ var left = NormalizeLiteral ( parts [ 0 ] ) ;
733+ var right = NormalizeLiteral ( parts [ 1 ] ) ;
734+ result = string . Equals ( left , right , StringComparison . OrdinalIgnoreCase ) ;
735+ }
736+ }
737+ else if ( expanded . Contains ( "!=" ) )
738+ {
739+ var parts = expanded . Split ( new [ ] { "!=" } , StringSplitOptions . RemoveEmptyEntries | StringSplitOptions . TrimEntries ) ;
740+ if ( parts . Length == 2 )
741+ {
742+ var left = NormalizeLiteral ( parts [ 0 ] ) ;
743+ var right = NormalizeLiteral ( parts [ 1 ] ) ;
744+ result = ! string . Equals ( left , right , StringComparison . OrdinalIgnoreCase ) ;
745+ }
746+ }
747+ Log . Logger . Debug ( $ "[MsBuildConditionEvaluator] expanded='{ expanded } ' tf='{ targetFramework } ' => { result } ") ;
748+ return result ;
749+ }
750+ catch ( Exception e )
751+ {
752+ Log . Logger . Debug ( e , $ "Failed to evaluate MSBuild condition '{ condition } '. Assuming false.") ;
753+ return false ;
754+ }
755+ }
756+
757+ private static bool Evaluate ( string expr )
758+ {
759+ // Very small parser: handle parentheses removal, quotes, ==, !=, And/Or
760+ expr = expr . Trim ( ) ;
761+ if ( expr . StartsWith ( "(" ) && expr . EndsWith ( ")" ) )
762+ {
763+ expr = expr . Substring ( 1 , expr . Length - 2 ) . Trim ( ) ;
764+ }
765+
766+ // Split by OR
767+ var orParts = Split ( expr , new [ ] { " Or " , " or " , " OR " } ) ;
768+ if ( orParts . Length > 1 )
769+ {
770+ return orParts . Any ( p => Evaluate ( p ) ) ;
771+ }
772+ var andParts = Split ( expr , new [ ] { " And " , " and " , " AND " } ) ;
773+ if ( andParts . Length > 1 )
774+ {
775+ return andParts . All ( p => Evaluate ( p ) ) ;
776+ }
777+
778+ // Equality / inequality
779+ if ( expr . Contains ( "!=" ) )
780+ {
781+ var sides = expr . Split ( new [ ] { "!=" } , StringSplitOptions . RemoveEmptyEntries ) ;
782+ if ( sides . Length == 2 )
783+ {
784+ return ! NormalizeLiteral ( sides [ 0 ] ) . Equals ( NormalizeLiteral ( sides [ 1 ] ) , StringComparison . OrdinalIgnoreCase ) ;
785+ }
786+ }
787+ if ( expr . Contains ( "==" ) )
788+ {
789+ var sides = expr . Split ( new [ ] { "==" } , StringSplitOptions . RemoveEmptyEntries ) ;
790+ if ( sides . Length == 2 )
791+ {
792+ return NormalizeLiteral ( sides [ 0 ] ) . Equals ( NormalizeLiteral ( sides [ 1 ] ) , StringComparison . OrdinalIgnoreCase ) ;
793+ }
794+ }
795+
796+ // If cannot parse, default true (MSBuild would treat non-empty?) but safer false.
797+ return false ;
798+ }
799+
800+ private static string NormalizeLiteral ( string val )
801+ {
802+ val = val . Trim ( ) ;
803+ if ( val . StartsWith ( "'" ) ) val = val . Trim ( '\' ' ) ;
804+ if ( val . StartsWith ( "\" " ) ) val = val . Trim ( '"' ) ;
805+ return val . Trim ( ) ;
806+ }
807+
808+ private static string [ ] Split ( string expr , string [ ] separators )
809+ {
810+ foreach ( var sep in separators )
811+ {
812+ var idx = IndexOfIgnoreCase ( expr , sep ) ;
813+ if ( idx >= 0 )
814+ {
815+ // simple split (no nested support)
816+ return expr . Split ( separators , StringSplitOptions . RemoveEmptyEntries | StringSplitOptions . TrimEntries ) ;
817+ }
818+ }
819+ return new [ ] { expr } ;
820+ }
821+
822+ private static int IndexOfIgnoreCase ( string source , string value )
823+ => source . IndexOf ( value , StringComparison . OrdinalIgnoreCase ) ;
824+ }
566825}
0 commit comments