1818using Polly ;
1919using Serilog . Core ;
2020using System . Text . RegularExpressions ;
21+ using Serilog ; // added for IsEnabled and message template logging
22+ using Serilog . Events ; // added for LogEventLevel
2123
2224namespace AXSharp . Compiler ;
2325
@@ -493,22 +495,23 @@ private static IEnumerable<IReference> PackageReferences(XDocument csproj, strin
493495 var itemGroups = csproj . Root ! . Elements ( "ItemGroup" ) ;
494496 foreach ( var ig in itemGroups )
495497 {
496- foreach ( var tf in targetFrameworks )
498+ var tf = targetFrameworks . First ( ) ;
499+
500+ if ( ! MsBuildConditionEvaluator . ItemGroupConditionPasses ( ig , baseProperties , tf ) ) continue ;
501+
502+ foreach ( var pr in ig . Elements ( "PackageReference" ) )
497503 {
498- if ( ! MsBuildConditionEvaluator . ItemGroupConditionPasses ( ig , baseProperties , tf ) ) continue ;
499- foreach ( var pr in ig . Elements ( "PackageReference" ) )
504+ if ( ! MsBuildConditionEvaluator . ElementConditionPasses ( pr , baseProperties , tf ) ) continue ;
505+ try
500506 {
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- }
507+ result . Add ( PackageReference . CreateFromReferenceNode ( pr , projectFile ) ) ;
508+ }
509+ catch ( Exception e )
510+ {
511+ Log . Logger . Warning ( e , $ "Failed to parse PackageReference '{ pr } ' in '{ projectFile } '") ;
510512 }
511513 }
514+
512515 }
513516 return result ;
514517 }
@@ -546,19 +549,72 @@ internal static string GetBestMatchedVersion(string packageName, string packageV
546549
547550 private static IEnumerable < IReference > ProjectReferences ( XDocument csproj , string directory , IEnumerable < string > targetFrameworks , Dictionary < string , string > baseProperties )
548551 {
552+ // Optimized: de-duplicate includes, avoid redundant TF loops for unconditional groups/elements,
553+ // and short-circuit once a condition passes for any TF.
549554 var result = new List < IReference > ( ) ;
555+ var seen = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
550556 var itemGroups = csproj . Root ! . Elements ( "ItemGroup" ) ;
557+
551558 foreach ( var ig in itemGroups )
552559 {
553- foreach ( var tf in targetFrameworks )
560+ var igCondition = ig . Attribute ( "Condition" ) ? . Value ;
561+ var igHasCondition = ! string . IsNullOrWhiteSpace ( igCondition ) ;
562+
563+ if ( ! igHasCondition )
554564 {
555- if ( ! MsBuildConditionEvaluator . ItemGroupConditionPasses ( ig , baseProperties , tf ) ) continue ;
565+ // Unconditional ItemGroup – process elements smartly
556566 foreach ( var pr in ig . Elements ( "ProjectReference" ) )
557567 {
568+ var includeRaw = pr . Attribute ( "Include" ) ? . Value ;
569+ if ( string . IsNullOrWhiteSpace ( includeRaw ) ) continue ;
570+
571+ var include = includeRaw . Replace ( "\\ " , Path . DirectorySeparatorChar . ToString ( ) ) ;
572+ var prCondition = pr . Attribute ( "Condition" ) ? . Value ;
573+ var prHasCondition = ! string . IsNullOrWhiteSpace ( prCondition ) ;
574+
575+ var shouldInclude = false ;
576+ if ( ! prHasCondition )
577+ {
578+ shouldInclude = true ;
579+ }
580+ else
581+ {
582+ // Evaluate once per TF until first pass
583+ var tf = targetFrameworks . First ( ) ;
584+ if ( MsBuildConditionEvaluator . ElementConditionPasses ( pr , baseProperties , tf ) )
585+ {
586+ shouldInclude = true ;
587+ break ;
588+ }
589+
590+ }
591+
592+ if ( shouldInclude && seen . Add ( include ) )
593+ {
594+ result . Add ( new ProjectReference ( directory , include ) ) ;
595+ }
596+ }
597+
598+ continue ; // done with this ItemGroup
599+ }
600+
601+ // Conditional ItemGroup – we must evaluate per TF, but still de-duplicate and short-circuit per element
602+ foreach ( var pr in ig . Elements ( "ProjectReference" ) )
603+ {
604+ var includeRaw = pr . Attribute ( "Include" ) ? . Value ;
605+ if ( string . IsNullOrWhiteSpace ( includeRaw ) ) continue ;
606+ var include = includeRaw . Replace ( "\\ " , Path . DirectorySeparatorChar . ToString ( ) ) ;
607+
608+ foreach ( var tf in targetFrameworks )
609+ {
610+ if ( ! MsBuildConditionEvaluator . ItemGroupConditionPasses ( ig , baseProperties , tf ) ) continue ;
558611 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 ) ) ;
612+
613+ if ( seen . Add ( include ) )
614+ {
615+ result . Add ( new ProjectReference ( directory , include ) ) ;
616+ }
617+ break ; // Once added for any TF, don't re-evaluate for others
562618 }
563619 }
564620 }
@@ -604,6 +660,17 @@ public void GenerateCompanionData()
604660 /// </summary>
605661 private static class MsBuildConditionEvaluator
606662 {
663+ // Precompiled regex for faster evaluation
664+ private static readonly Regex SimpleTfEqRegex = new (
665+ pattern : "^\\ s*'\\ $\\ (TargetFramework\\ )'\\ s*==\\ s*'([^']+)'\\ s*$" ,
666+ options : RegexOptions . IgnoreCase | RegexOptions . Compiled ) ;
667+ private static readonly Regex SimpleTfNeRegex = new (
668+ pattern : "^\\ s*'\\ $\\ (TargetFramework\\ )'\\ s*!=\\ s*'([^']+)'\\ s*$" ,
669+ options : RegexOptions . IgnoreCase | RegexOptions . Compiled ) ;
670+ private static readonly Regex PropertyRefRegex = new (
671+ pattern : "\\ $\\ (([^)]+)\\ )" ,
672+ options : RegexOptions . Compiled ) ;
673+
607674 internal static ( Dictionary < string , string > properties , List < string > targetFrameworks ) CollectBaseProperties ( string projectFile , XDocument csproj )
608675 {
609676 var props = new Dictionary < string , string > ( StringComparer . OrdinalIgnoreCase ) ;
@@ -642,7 +709,7 @@ internal static (Dictionary<string,string> properties, List<string> targetFramew
642709 if ( frameworks . Count == 0 )
643710 {
644711 // default fallback
645- frameworks . Add ( "netstandard2 .0" ) ;
712+ frameworks . Add ( "net9 .0" ) ;
646713 }
647714 return ( props , frameworks ) ;
648715 }
@@ -679,12 +746,12 @@ internal static bool ElementConditionPasses(XElement element, Dictionary<string,
679746 {
680747 var condition = element . Attribute ( "Condition" ) ? . Value ;
681748 var passes = ConditionPasses ( condition , baseProps , targetFramework ) ;
682- if ( ! string . IsNullOrWhiteSpace ( condition ) )
749+ if ( ! string . IsNullOrWhiteSpace ( condition ) && Log . Logger . IsEnabled ( LogEventLevel . Debug ) )
683750 {
684751 try
685752 {
686753 var include = element . Attribute ( "Include" ) ? . Value ;
687- Log . Logger . Debug ( $ "[MsBuildConditionEvaluator] Element '{ element . Name . LocalName } ' Include='{ include } ' condition='{ condition } ' tf='{ targetFramework } ' => { passes } " ) ;
754+ Log . Logger . Debug ( "[MsBuildConditionEvaluator] Element '{Elem }' Include='{Include }' condition='{Condition }' tf='{TF }' => {Pass}" , element . Name . LocalName , include , condition , targetFramework , passes ) ;
688755 }
689756 catch { /* ignore */ }
690757 }
@@ -697,32 +764,53 @@ private static bool ConditionPasses(string? condition, Dictionary<string,string>
697764 try
698765 {
699766 // 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 ) ;
767+ var simpleTfEq = SimpleTfEqRegex . Match ( condition ) ;
701768 if ( simpleTfEq . Success )
702769 {
703770 var expected = simpleTfEq . Groups [ 1 ] . Value . Trim ( ) ;
704771 var ok = string . Equals ( expected , targetFramework , StringComparison . OrdinalIgnoreCase ) ;
705- Log . Logger . Debug ( $ "[MsBuildConditionEvaluator] simple == TF condition '{ condition } ' => { ok } ") ;
706772 if ( ! ok ) return false ; // short circuit
707773 }
708- var simpleTfNe = Regex . Match ( condition , @"^\s*'\$\(TargetFramework\)'\s*!=\s*'([^']+)'\s*$" , RegexOptions . IgnoreCase ) ;
774+ var simpleTfNe = SimpleTfNeRegex . Match ( condition ) ;
709775 if ( simpleTfNe . Success )
710776 {
711777 var notExpected = simpleTfNe . Groups [ 1 ] . Value . Trim ( ) ;
712778 var ok = ! string . Equals ( notExpected , targetFramework , StringComparison . OrdinalIgnoreCase ) ;
713- Log . Logger . Debug ( $ "[MsBuildConditionEvaluator] simple != TF condition '{ condition } ' => { ok } ") ;
714779 if ( ! ok ) return false ; // short circuit
715780 }
716781
782+ // Replace only referenced properties
717783 var expanded = condition ;
718- var props = new Dictionary < string , string > ( baseProps , StringComparer . OrdinalIgnoreCase )
719- {
720- [ "TargetFramework" ] = targetFramework
721- } ;
722- foreach ( var kvp in props )
784+ var matches = PropertyRefRegex . Matches ( condition ) ;
785+ if ( matches . Count > 0 )
723786 {
724- expanded = expanded . Replace ( $ "$({ kvp . Key } )", kvp . Value , StringComparison . OrdinalIgnoreCase ) ;
787+ // Use a small set to avoid repeating replaces
788+ var propsToExpand = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
789+ foreach ( Match m in matches )
790+ {
791+ if ( m . Success && m . Groups . Count > 1 )
792+ {
793+ propsToExpand . Add ( m . Groups [ 1 ] . Value ) ;
794+ }
795+ }
796+ foreach ( var key in propsToExpand )
797+ {
798+ string ? value = null ;
799+ if ( string . Equals ( key , "TargetFramework" , StringComparison . OrdinalIgnoreCase ) )
800+ {
801+ value = targetFramework ;
802+ }
803+ else
804+ {
805+ baseProps . TryGetValue ( key , out value ) ;
806+ }
807+ if ( ! string . IsNullOrEmpty ( value ) )
808+ {
809+ expanded = expanded . Replace ( $ "$({ key } )", value , StringComparison . OrdinalIgnoreCase ) ;
810+ }
811+ }
725812 }
813+
726814 var result = Evaluate ( expanded ) ;
727815 if ( expanded . Contains ( "==" ) )
728816 {
@@ -744,12 +832,18 @@ private static bool ConditionPasses(string? condition, Dictionary<string,string>
744832 result = ! string . Equals ( left , right , StringComparison . OrdinalIgnoreCase ) ;
745833 }
746834 }
747- Log . Logger . Debug ( $ "[MsBuildConditionEvaluator] expanded='{ expanded } ' tf='{ targetFramework } ' => { result } ") ;
835+ if ( Log . Logger . IsEnabled ( LogEventLevel . Debug ) )
836+ {
837+ Log . Logger . Debug ( "[MsBuildConditionEvaluator] expanded='{Expanded}' tf='{TF}' => {Result}" , expanded , targetFramework , result ) ;
838+ }
748839 return result ;
749840 }
750841 catch ( Exception e )
751842 {
752- Log . Logger . Debug ( e , $ "Failed to evaluate MSBuild condition '{ condition } '. Assuming false.") ;
843+ if ( Log . Logger . IsEnabled ( LogEventLevel . Debug ) )
844+ {
845+ Log . Logger . Debug ( e , $ "Failed to evaluate MSBuild condition '{ condition } '. Assuming false.") ;
846+ }
753847 return false ;
754848 }
755849 }
0 commit comments