diff --git a/mcp/CWM.RoslynNavigator/src/CWM.RoslynNavigator.csproj b/mcp/CWM.RoslynNavigator/src/CWM.RoslynNavigator.csproj index 68f5663..80c8834 100644 --- a/mcp/CWM.RoslynNavigator/src/CWM.RoslynNavigator.csproj +++ b/mcp/CWM.RoslynNavigator/src/CWM.RoslynNavigator.csproj @@ -15,7 +15,7 @@ CWM.RoslynNavigator - 0.7.2 + 0.7.3 Mukesh Murugan codewithmukesh CWM.RoslynNavigator @@ -40,6 +40,10 @@ + + + + diff --git a/mcp/CWM.RoslynNavigator/src/Tools/GetProjectGraphTool.cs b/mcp/CWM.RoslynNavigator/src/Tools/GetProjectGraphTool.cs index 45ec3ba..d298cd3 100644 --- a/mcp/CWM.RoslynNavigator/src/Tools/GetProjectGraphTool.cs +++ b/mcp/CWM.RoslynNavigator/src/Tools/GetProjectGraphTool.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using System.Text.Json; +using System.Text.RegularExpressions; using System.Xml.Linq; using CWM.RoslynNavigator.Responses; using ModelContextProtocol.Server; @@ -67,18 +68,48 @@ private static string DetectTargetFramework(Microsoft.CodeAnalysis.Project proje } // Strategy 2: Check preprocessor symbols (e.g., NET10_0, NET8_0) - var preprocessorSymbol = project.ParseOptions?.PreprocessorSymbolNames - .Where(s => s.StartsWith("NET")) - .OrderByDescending(s => s.Length) - .FirstOrDefault(); - - if (preprocessorSymbol is not null) + if (project.ParseOptions?.PreprocessorSymbolNames is { } symbols) { - // Convert "NET10_0" to "net10.0", "NET8_0_OR_GREATER" stays as-is but we prefer exact match - var exact = preprocessorSymbol.Replace("_OR_GREATER", ""); - return exact.ToLowerInvariant().Replace('_', '.'); + var detected = DetectFromPreprocessorSymbols(symbols); + if (detected is not null) + return detected; } return "unknown"; } + + // Exact TFM symbols: NET10_0, NETSTANDARD2_0, NETCOREAPP3_1. Anchored so compat + // symbols like NETCOREAPP1_0_OR_GREATER (issue #19) never match. + private static readonly Regex ExactTfmSymbol = + new(@"^(NETSTANDARD|NETCOREAPP|NET)(\d+)_(\d+)$", RegexOptions.Compiled); + + // .NET Framework symbols have no underscore: NET48, NET472 + private static readonly Regex FrameworkTfmSymbol = new(@"^NET(\d{2,3})$", RegexOptions.Compiled); + + internal static string? DetectFromPreprocessorSymbols(IEnumerable symbols) + { + var symbolList = symbols.ToList(); + + // A compilation defines exactly one exact TFM symbol alongside many *_OR_GREATER + // compat symbols. Only the exact one names the real target framework. + var exact = symbolList + .Select(s => ExactTfmSymbol.Match(s)) + .Where(m => m.Success) + .Select(m => (Prefix: m.Groups[1].Value, Major: int.Parse(m.Groups[2].Value), Minor: int.Parse(m.Groups[3].Value))) + .OrderByDescending(t => t.Major) + .ThenByDescending(t => t.Minor) + .Select(t => (string?)$"{t.Prefix.ToLowerInvariant()}{t.Major}.{t.Minor}") + .FirstOrDefault(); + + if (exact is not null) + return exact; + + return symbolList + .Select(s => FrameworkTfmSymbol.Match(s)) + .Where(m => m.Success) + .Select(m => m.Groups[1].Value) + .OrderByDescending(v => int.Parse(v.PadRight(3, '0'))) + .Select(v => (string?)$"net{v}") + .FirstOrDefault(); + } } diff --git a/mcp/CWM.RoslynNavigator/tests/Tools/DetectTargetFrameworkTests.cs b/mcp/CWM.RoslynNavigator/tests/Tools/DetectTargetFrameworkTests.cs new file mode 100644 index 0000000..061db5c --- /dev/null +++ b/mcp/CWM.RoslynNavigator/tests/Tools/DetectTargetFrameworkTests.cs @@ -0,0 +1,90 @@ +using CWM.RoslynNavigator.Tools; + +namespace CWM.RoslynNavigator.Tests.Tools; + +/// +/// Regression tests for issue #19: net10.0 projects were reported as "netcoreapp1.0" +/// because the longest NET-prefixed preprocessor symbol (NETCOREAPP1_0_OR_GREATER) +/// won over the exact TFM symbol (NET10_0). +/// +public class DetectTargetFrameworkTests +{ + [Fact] + public void DetectFromPreprocessorSymbols_Net10_ReturnsNet10() + { + // Real symbol set the SDK defines for a net10.0 compilation + string[] symbols = + [ + "TRACE", "DEBUG", "NET", "NET10_0", "NETCOREAPP", + "NET5_0_OR_GREATER", "NET6_0_OR_GREATER", "NET7_0_OR_GREATER", + "NET8_0_OR_GREATER", "NET9_0_OR_GREATER", "NET10_0_OR_GREATER", + "NETCOREAPP1_0_OR_GREATER", "NETCOREAPP1_1_OR_GREATER", + "NETCOREAPP2_0_OR_GREATER", "NETCOREAPP2_1_OR_GREATER", + "NETCOREAPP2_2_OR_GREATER", "NETCOREAPP3_0_OR_GREATER", + "NETCOREAPP3_1_OR_GREATER", + ]; + + Assert.Equal("net10.0", GetProjectGraphTool.DetectFromPreprocessorSymbols(symbols)); + } + + [Fact] + public void DetectFromPreprocessorSymbols_Net8_ReturnsNet8() + { + string[] symbols = + [ + "TRACE", "NET", "NET8_0", "NETCOREAPP", + "NET5_0_OR_GREATER", "NET6_0_OR_GREATER", "NET7_0_OR_GREATER", "NET8_0_OR_GREATER", + "NETCOREAPP1_0_OR_GREATER", "NETCOREAPP2_0_OR_GREATER", + "NETCOREAPP3_0_OR_GREATER", "NETCOREAPP3_1_OR_GREATER", + ]; + + Assert.Equal("net8.0", GetProjectGraphTool.DetectFromPreprocessorSymbols(symbols)); + } + + [Fact] + public void DetectFromPreprocessorSymbols_NetStandard20_ReturnsNetStandard20() + { + string[] symbols = + [ + "TRACE", "NETSTANDARD", "NETSTANDARD2_0", + "NETSTANDARD1_0_OR_GREATER", "NETSTANDARD1_1_OR_GREATER", + "NETSTANDARD1_6_OR_GREATER", "NETSTANDARD2_0_OR_GREATER", + ]; + + Assert.Equal("netstandard2.0", GetProjectGraphTool.DetectFromPreprocessorSymbols(symbols)); + } + + [Fact] + public void DetectFromPreprocessorSymbols_NetCoreApp31_ReturnsNetCoreApp31() + { + string[] symbols = + [ + "TRACE", "NETCOREAPP", "NETCOREAPP3_1", + "NETCOREAPP1_0_OR_GREATER", "NETCOREAPP2_0_OR_GREATER", + "NETCOREAPP3_0_OR_GREATER", "NETCOREAPP3_1_OR_GREATER", + ]; + + Assert.Equal("netcoreapp3.1", GetProjectGraphTool.DetectFromPreprocessorSymbols(symbols)); + } + + [Fact] + public void DetectFromPreprocessorSymbols_NetFramework48_ReturnsNet48() + { + string[] symbols = + [ + "TRACE", "NETFRAMEWORK", "NET48", + "NET20_OR_GREATER", "NET35_OR_GREATER", "NET40_OR_GREATER", + "NET45_OR_GREATER", "NET472_OR_GREATER", "NET48_OR_GREATER", + ]; + + Assert.Equal("net48", GetProjectGraphTool.DetectFromPreprocessorSymbols(symbols)); + } + + [Fact] + public void DetectFromPreprocessorSymbols_NoTfmSymbols_ReturnsNull() + { + string[] symbols = ["TRACE", "DEBUG", "MY_CUSTOM_FLAG"]; + + Assert.Null(GetProjectGraphTool.DetectFromPreprocessorSymbols(symbols)); + } +}