Skip to content

Commit c6025eb

Browse files
authored
Merge pull request #437 from Inxton/optimization-references-loader
Optimization references loader
2 parents c8385d7 + 41c3418 commit c6025eb

3 files changed

Lines changed: 130 additions & 34 deletions

File tree

src/AXSharp.compiler/src/AXSharp.Cs.Compiler/CsProject.cs

Lines changed: 126 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
using Polly;
1919
using Serilog.Core;
2020
using System.Text.RegularExpressions;
21+
using Serilog; // added for IsEnabled and message template logging
22+
using Serilog.Events; // added for LogEventLevel
2123

2224
namespace 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
}

src/AXSharp.compiler/src/ixc/Properties/launchSettings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
},
3636
"axopen-traversal": {
3737
"commandName": "Project",
38-
"workingDirectory": "C:\\W\\Develop\\gh\\inxton\\ax\\axopen\\axopen\\src\\traversals\\apax\\"
38+
"commandLineArgs": "--verbosity Information",
39+
"workingDirectory": "C:\\W\\Develop\\gh\\inxton\\simatic-ax\\axopen.templates\\axopen\\src\\traversals\\apax\\"
3940
},
4041
"axopen-core": {
4142
"commandName": "Project",

src/AXSharp.compiler/tests/AXSharp.CompilerTests/ConditionalDependenciesTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ public void should_respect_itemgroup_condition_targetframework()
4949
var target = (CsProject)ax.TargetProject;
5050
var refs = target.LoadReferences().OfType<PackageReference>().ToList();
5151
Assert.Contains(refs, r => r.Include == "PkgOnlyNet8");
52-
Assert.Contains(refs, r => r.Include == "PkgOnlyNet9");
52+
// Assert.Contains(refs, r => r.Include == "PkgOnlyNet9"); we only use single target framework in this test, so net9.0 package should not be included
53+
Assert.DoesNotContain(refs, r => r.Include == "PkgOnlyNet9");
5354
}
5455

5556
[Fact]

0 commit comments

Comments
 (0)