Skip to content

Commit c8385d7

Browse files
authored
Merge pull request #436 from Inxton/435-bug-ixc-parsing-csproj-files-does-not-take-attribs-into-consideration
[BUG] IXC Parsing csproj files does not take attribs into consideration.
2 parents 888f74c + 1d5b125 commit c8385d7

4 files changed

Lines changed: 413 additions & 21 deletions

File tree

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

Lines changed: 277 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using NuGet.Versioning;
1818
using Polly;
1919
using Serilog.Core;
20+
using System.Text.RegularExpressions;
2021

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

src/AXSharp.compiler/tests/AXSharp.Compiler.CsTests/Integration.Cs/IxProjectTests.IntegrationCs.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
using AXSharp.Compiler.Cs.Onliner;
1111
using AXSharp.Compiler.Cs.Plain;
1212
using AXSharp.Compiler.CsTests;
13-
using Castle.Core.Resource;
1413
using Polly;
1514
using Xunit.Abstractions;
1615

src/AXSharp.compiler/tests/AXSharp.CompilerTests/AXSharp.CompilerTests.csproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@
3939
</PackageReference>
4040
</ItemGroup>
4141

42-
43-
4442
<ItemGroup>
4543
<ProjectReference Include="..\..\src\AXSharp.Compiler\AXSharp.Compiler.csproj" />
4644
<ProjectReference Include="..\..\src\AXSharp.Cs.Compiler\AXSharp.Compiler.Cs.csproj" />
@@ -52,6 +50,9 @@
5250
</Content>
5351
<Content Include="samples\plt\**">
5452
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
53+
</Content>
54+
<Content Include="samples\conditional\**">
55+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
5556
</Content>
5657
</ItemGroup>
5758

0 commit comments

Comments
 (0)