Skip to content

Commit 0ba82f0

Browse files
committed
chore(Roslyn.Solution): implement a smarter way to selected the latest/most compatible fx
1 parent da644f6 commit 0ba82f0

File tree

4 files changed

+62
-60
lines changed

4 files changed

+62
-60
lines changed

Directory.Packages.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@
3434
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.9" />
3535
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
3636
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
37+
<PackageVersion Include="NuGet.Frameworks" Version="6.14.0" />
3738
<PackageVersion Include="NUnit" Version="3.14.0" />
3839
<PackageVersion Include="NUnit3TestAdapter" Version="4.6.0" />
3940
<PackageVersion Include="System.Collections.Immutable" Version="9.0.0" />
4041
</ItemGroup>
41-
</Project>
42+
</Project>

src/CSharpLanguageServer/CSharpLanguageServer.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,5 +104,6 @@
104104
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
105105
<PackageReference Include="Microsoft.CodeAnalysis.Features" />
106106
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" />
107+
<PackageReference Include="NuGet.Frameworks" />
107108
</ItemGroup>
108109
</Project>

src/CSharpLanguageServer/Roslyn/Solution.fs

Lines changed: 56 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,14 @@ open Microsoft.CodeAnalysis
1515
open Microsoft.CodeAnalysis.MSBuild
1616
open Microsoft.CodeAnalysis.Text
1717
open Microsoft.Extensions.Logging
18+
open NuGet.Frameworks
1819

19-
open CSharpLanguageServer
2020
open CSharpLanguageServer.Lsp
2121
open CSharpLanguageServer.Logging
22-
open CSharpLanguageServer.Util
2322
open CSharpLanguageServer.Roslyn.WorkspaceServices
2423

25-
2624
let private logger = Logging.getLoggerByName "Roslyn.Solution"
2725

28-
2926
let initializeMSBuild () : unit =
3027
let vsInstanceQueryOpt = VisualStudioInstanceQueryOptions.Default
3128
let vsInstanceList = MSBuildLocator.QueryVisualStudioInstances(vsInstanceQueryOpt)
@@ -61,7 +58,6 @@ let initializeMSBuild () : unit =
6158

6259
MSBuildLocator.RegisterInstance(vsInstance)
6360

64-
6561
let solutionLoadProjectFilenames (solutionPath: string) =
6662
assert Path.IsPathRooted solutionPath
6763
let projectFilenames = new List<string>()
@@ -75,49 +71,6 @@ let solutionLoadProjectFilenames (solutionPath: string) =
7571
projectFilenames |> Set.ofSeq
7672

7773

78-
type TfmCategory =
79-
| NetFramework of Version
80-
| NetStandard of Version
81-
| NetCoreApp of Version
82-
| Net of Version
83-
| Unknown
84-
85-
86-
let selectLatestTfm (tfms: string seq) : string option =
87-
let parseTfm (tfm: string) : TfmCategory =
88-
let patterns =
89-
[ @"^net(?<major>\d)(?<minor>\d)?(?<build>\d)?$", NetFramework
90-
@"^netstandard(?<major>\d+)\.(?<minor>\d+)$", NetStandard
91-
@"^netcoreapp(?<major>\d+)\.(?<minor>\d+)$", NetCoreApp
92-
@"^net(?<major>\d+)\.(?<minor>\d+)$", Net ]
93-
94-
let matchingTfmCategory (pat, categoryCtor) =
95-
let m = Regex.Match(tfm.ToLowerInvariant(), pat)
96-
97-
if m.Success then
98-
let readVersionNum (groupName: string) =
99-
let group = m.Groups.[groupName]
100-
if group.Success then int group.Value else 0
101-
102-
Version(readVersionNum "major", readVersionNum "minor", readVersionNum "build")
103-
|> categoryCtor
104-
|> Some
105-
else
106-
None
107-
108-
patterns |> List.tryPick matchingTfmCategory |> Option.defaultValue Unknown
109-
110-
let rankTfm =
111-
function
112-
| Net v -> 3000 + v.Major * 10 + v.Minor
113-
| NetCoreApp v -> 2000 + v.Major * 10 + v.Minor
114-
| NetStandard v -> 1000 + v.Major * 10 + v.Minor
115-
| NetFramework v -> 0 + v.Major * 10 + v.Minor
116-
| Unknown -> -1
117-
118-
tfms |> Seq.sortByDescending (parseTfm >> rankTfm) |> Seq.tryHead
119-
120-
12174
let loadProjectTfms (projs: string seq) : Map<string, list<string>> =
12275
let mutable projectTfms = Map.empty
12376

@@ -156,6 +109,58 @@ let loadProjectTfms (projs: string seq) : Map<string, list<string>> =
156109

157110
projectTfms
158111

112+
let compatibleFx (a: NuGetFramework) (b: NuGetFramework) =
113+
let fxCompatibilityProvider = DefaultCompatibilityProvider.Instance
114+
115+
if fxCompatibilityProvider.IsCompatible(a, b) then
116+
Some a
117+
else if fxCompatibilityProvider.IsCompatible(b, a) then
118+
Some b
119+
else
120+
None
121+
122+
let compatibleTfmsOfTwoSets afxs bfxs = seq {
123+
for a in afxs |> Seq.map NuGetFramework.Parse do
124+
for b in bfxs |> Seq.map NuGetFramework.Parse do
125+
match compatibleFx a b with
126+
| Some fx -> fx.GetShortFolderName()
127+
| None -> ()
128+
}
129+
130+
let compatibleTfmSet (fxSets: list<Set<string>>) : Set<string> =
131+
match fxSets.Length with
132+
| 0 -> Set.empty
133+
| 1 -> fxSets |> List.head
134+
| _ ->
135+
let firstSet = (fxSets |> List.head)
136+
137+
(fxSets |> List.skip 1)
138+
|> Seq.fold compatibleTfmsOfTwoSets firstSet
139+
|> Set.ofSeq
140+
141+
let bestTfm (frameworks: Set<String>) : string option =
142+
let frameworks = frameworks |> Seq.map NuGetFramework.Parse
143+
144+
let provider = DefaultCompatibilityProvider.Instance
145+
146+
let mutable bestCompatibleFx: NuGetFramework option = None
147+
148+
for candidate in frameworks do
149+
if frameworks |> Seq.forall (fun f -> provider.IsCompatible(candidate, f)) then
150+
bestCompatibleFx <- Some candidate
151+
152+
match bestCompatibleFx with
153+
| Some best -> best.GetShortFolderName() |> Some
154+
| _ ->
155+
// select platform-specific fx first
156+
if frameworks |> Seq.exists (fun f -> f.HasPlatform) then
157+
frameworks
158+
|> Seq.filter (fun f -> f.HasPlatform)
159+
|> Seq.maxBy (_.Version)
160+
|> _.GetShortFolderName()
161+
|> Some
162+
else
163+
frameworks |> Seq.maxBy (_.Version) |> _.GetShortFolderName() |> Some
159164

160165
let applyWorkspaceTargetFrameworkProp (tfmsPerProject: Map<string, list<string>>) props : Map<string, string> =
161166
let selectedTfm =
@@ -164,14 +169,14 @@ let applyWorkspaceTargetFrameworkProp (tfmsPerProject: Map<string, list<string>>
164169
| _ ->
165170
tfmsPerProject.Values
166171
|> Seq.map Set.ofSeq
167-
|> Set.intersectMany
168-
|> selectLatestTfm
172+
|> List.ofSeq
173+
|> compatibleTfmSet
174+
|> bestTfm
169175

170176
match selectedTfm with
171177
| Some tfm -> props |> Map.add "TargetFramework" tfm
172178
| None -> props
173179

174-
175180
let resolveDefaultWorkspaceProps projs : Map<string, string> =
176181
let tfmsPerProject = loadProjectTfms projs
177182

@@ -188,7 +193,6 @@ let solutionGetProjectForPath (solution: Solution) (filePath: string) : Project
188193

189194
solution.Projects |> Seq.filter fileOnProjectDir |> Seq.tryHead
190195

191-
192196
let solutionTryAddDocument (docFilePath: string) (text: string) (solution: Solution) : Async<Document option> = async {
193197
let projectOnPath = solutionGetProjectForPath solution docFilePath
194198

@@ -211,7 +215,6 @@ let solutionTryAddDocument (docFilePath: string) (text: string) (solution: Solut
211215
return newDocumentMaybe
212216
}
213217

214-
215218
let selectPreferredSolution (slnFiles: string list) : option<string> =
216219
let getProjectCount (slnPath: string) =
217220
try
@@ -229,7 +232,6 @@ let selectPreferredSolution (slnFiles: string list) : option<string> =
229232
|> Seq.map snd
230233
|> Seq.tryHead
231234

232-
233235
let solutionTryLoadOnPath (lspClient: ILspClient) (solutionPath: string) =
234236
assert Path.IsPathRooted solutionPath
235237
let progress = ProgressReporter lspClient
@@ -282,7 +284,6 @@ let solutionTryLoadOnPath (lspClient: ILspClient) (solutionPath: string) =
282284
return None
283285
}
284286

285-
286287
let solutionTryLoadFromProjectFiles (lspClient: ILspClient) (logMessage: string -> Async<unit>) (projs: string list) =
287288
let progress = ProgressReporter lspClient
288289

@@ -324,7 +325,6 @@ let solutionTryLoadFromProjectFiles (lspClient: ILspClient) (logMessage: string
324325
return Some msbuildWorkspace.CurrentSolution
325326
}
326327

327-
328328
let solutionFindAndLoadOnDir (lspClient: ILspClient) dir = async {
329329
let fileNotOnNodeModules (filename: string) =
330330
filename.Split Path.DirectorySeparatorChar |> Seq.contains "node_modules" |> not
@@ -374,7 +374,6 @@ let solutionFindAndLoadOnDir (lspClient: ILspClient) dir = async {
374374
| Some solutionPath -> return! solutionTryLoadOnPath lspClient solutionPath
375375
}
376376

377-
378377
let solutionLoadSolutionWithPathOrOnCwd (lspClient: ILspClient) (solutionPathMaybe: string option) (cwd: string) =
379378
match solutionPathMaybe with
380379
| Some solutionPath -> async {

tests/CSharpLanguageServer.Tests/InternalTests.fs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ open CSharpLanguageServer.Roslyn.Solution
1010
[<TestCase("1.csproj:net8.0,netstandard2.0", "net8.0")>]
1111
[<TestCase("1.csproj:netstandard1.0,netstandard2.0", "netstandard2.0")>]
1212
[<TestCase("1.csproj:net40,net462,net6.0,net8.0,netcoreapp3.1,netstandard2.0", "net8.0")>]
13+
[<TestCase("1.csproj:net40,net462,net6.0,net8.0,net8.0-windows,netcoreapp3.1,netstandard2.0", "net8.0-windows")>]
1314
[<TestCase("1.csproj:net40,net462", "net462")>]
1415
[<TestCase("1.csproj:net8.0 2.csproj:net8.0", "net8.0")>]
15-
[<TestCase("1.csproj:net8.0,net10.0 2.csproj:netstandard2.0,net462", null)>]
16+
[<TestCase("1.csproj:net8.0,net10.0 2.csproj:netstandard2.0,net462", "net10.0")>]
1617
[<TestCase("1.csproj:net8.0,net10.0 2.csproj:net8.0,net10.0", "net10.0")>]
17-
[<TestCase("1.csproj:net8.0 2.csproj:net8.0,net10.0", "net8.0")>]
18+
[<TestCase("1.csproj:net8.0 2.csproj:net8.0,net10.0", "net10.0")>]
1819
[<TestCase("1.csproj:net8.0 2.csproj:net9.0-windows", "net9.0-windows")>]
1920
[<TestCase("1.csproj:net9.0 2.csproj:net9.0-windows", "net9.0-windows")>]
2021
let testApplyWorkspaceTargetFrameworkProp (tfmList: string, expectedTfm: string | null) =

0 commit comments

Comments
 (0)