Repository navigation
Reuse a C# project's in-memory PE reference while its semantic version holds #20460
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5afdc13
2077365
f3bd094
201e191
4823f74
14184b2
aa6c30d
7816741
d110727
c342bba
e9231df
cca0df7
881f5b0
af6c259
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,7 +14,6 @@ open FSharp.Compiler.CodeAnalysis | |
| open Microsoft.VisualStudio.FSharp.Editor | ||
| open System.Threading | ||
| open Microsoft.VisualStudio.FSharp.Interactive.Session | ||
| open System.Runtime.CompilerServices | ||
| open CancellableTasks | ||
| open Microsoft.VisualStudio.FSharp.Editor.Extensions | ||
| open System.Windows | ||
|
|
@@ -110,6 +109,50 @@ type private SingleFileCacheEntry = | |
| Subscription: ConnectionPointSubscription | ||
| } | ||
|
|
||
| module internal PEReferenceVersion = | ||
|
|
||
| // A source generator can turn an edit below the declaration level into other declarations. Only Roslyn's | ||
| // out-of-process host knows which analyzer references carry one: asking here would load them into Visual Studio. | ||
| let private mayRunSourceGenerators (project: Project) = | ||
| let solution = project.Solution | ||
|
|
||
| let hasAnalyzerReferences (projectId: ProjectId) = | ||
| match solution.GetProject projectId with | ||
| | null -> false | ||
| | dependency -> dependency.AnalyzerReferences.Count > 0 | ||
|
|
||
| project.AnalyzerReferences.Count > 0 | ||
| || solution.GetProjectDependencyGraph().GetProjectsThatThisProjectTransitivelyDependsOn(project.Id) | ||
| |> Seq.exists hasAnalyzerReferences | ||
|
|
||
| let getAsync (referencedProject: Project) (ct: CancellationToken) = | ||
| if mayRunSourceGenerators referencedProject then | ||
| referencedProject.GetDependentVersionAsync(ct) | ||
| else | ||
| referencedProject.GetDependentSemanticVersionAsync(ct) | ||
|
|
||
| /// <summary> | ||
| /// The in-memory PE reference of a referenced project, kept while its <c>PEReferenceVersion</c> is unchanged. | ||
| /// Roslyn recreates a <see cref="T:Microsoft.CodeAnalysis.Compilation"/> on every solution fork, and a reference per | ||
| /// instance has a fresh stamp, which invalidates every FCS cache keyed on it. | ||
| /// </summary> | ||
| [<Sealed>] | ||
| type private PEReferenceCacheEntry(version: VersionStamp, compilation: Compilation) = | ||
| let mutable pinnedUntilEmitted = compilation | ||
| let latest = WeakReference<Compilation>(compilation) | ||
|
|
||
| member _.Version = version | ||
|
|
||
| member _.TryGetCompilation() = | ||
| match pinnedUntilEmitted with | ||
| | null -> | ||
| match latest.TryGetTarget() with | ||
| | true, compilation -> ValueSome compilation | ||
| | _ -> ValueNone | ||
| | pinned -> ValueSome pinned | ||
|
|
||
| member _.Emitted() = pinnedUntilEmitted <- null | ||
|
|
||
| [<RequireQualifiedAccess>] | ||
| type private FSharpProjectOptionsMessage = | ||
| | TryGetOptionsByDocument of | ||
|
|
@@ -139,74 +182,75 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = | |
|
|
||
| let singleFileCache = ConcurrentDictionary<DocumentId, SingleFileCacheEntry>() | ||
|
|
||
| // This is used to not constantly emit the same compilation. | ||
| let weakPEReferences = ConditionalWeakTable<Compilation, FSharpReferencedProject>() | ||
| let peReferences = | ||
| ConcurrentDictionary<ProjectId, PEReferenceCacheEntry * FSharpReferencedProject>() | ||
|
|
||
| let lastSuccessfulCompilations = ConcurrentDictionary<ProjectId, Compilation>() | ||
|
|
||
| let scriptUpdatedEvent = Event<FSharpProjectOptions>() | ||
|
|
||
| let createPEReference (referencedProject: Project) (comp: Compilation) = | ||
| let buildPEReference (referencedProject: Project) (entry: PEReferenceCacheEntry) = | ||
| let projectId = referencedProject.Id | ||
| let mutable stamp = DateTime.UtcNow | ||
|
|
||
| match weakPEReferences.TryGetValue comp with | ||
| | true, fsRefProj -> fsRefProj | ||
| | _ -> | ||
| let mutable strongComp = comp | ||
| let weakComp = WeakReference<Compilation>(comp) | ||
| let mutable stamp = DateTime.UtcNow | ||
| // Emitting can fail on unresolvable compilation errors: fall back to the project's last successful compilation. | ||
| let getStream = | ||
| fun ct -> | ||
| let tryStream (comp: Compilation) = | ||
| let ms = new MemoryStream() // do not dispose the stream as it will be owned on the reference. | ||
|
|
||
| // Getting a C# reference assembly can fail if there are compilation errors that cannot be resolved. | ||
| // To mitigate this, we store the last successful compilation of a C# project and re-use it until we get a new successful compilation. | ||
| let getStream = | ||
| fun ct -> | ||
| let tryStream (comp: Compilation) = | ||
| let ms = new MemoryStream() // do not dispose the stream as it will be owned on the reference. | ||
| let emitOptions = | ||
| Emit.EmitOptions(metadataOnly = true, includePrivateMembers = false, tolerateErrors = true) | ||
|
|
||
| let emitOptions = | ||
| Emit.EmitOptions(metadataOnly = true, includePrivateMembers = false, tolerateErrors = true) | ||
| try | ||
| let result = comp.Emit(ms, options = emitOptions, cancellationToken = ct) | ||
|
|
||
| try | ||
| let result = comp.Emit(ms, options = emitOptions, cancellationToken = ct) | ||
|
|
||
| if result.Success then | ||
| strongComp <- Unchecked.defaultof<_> // Stop strongly holding the compilation since we have a result. | ||
| lastSuccessfulCompilations.[projectId] <- comp | ||
| ms.Position <- 0L | ||
| ms :> Stream |> Some | ||
| else | ||
| strongComp <- Unchecked.defaultof<_> // Stop strongly holding the compilation since we have a result. | ||
| ms.Dispose() // it failed, dispose of stream | ||
| None | ||
| with | ||
| | :? OperationCanceledException -> | ||
| // Since we cancelled, do not null out the strong compilation ref and update the stamp. | ||
| stamp <- DateTime.UtcNow | ||
| ms.Dispose() | ||
| None | ||
| | _ -> | ||
| strongComp <- Unchecked.defaultof<_> // Stop strongly holding the compilation since we have a result. | ||
| if result.Success then | ||
| entry.Emitted() | ||
| lastSuccessfulCompilations.[projectId] <- comp | ||
| ms.Position <- 0L | ||
| ms :> Stream |> Some | ||
| else | ||
| entry.Emitted() | ||
| ms.Dispose() // it failed, dispose of stream | ||
| None | ||
| with | ||
| | :? OperationCanceledException -> | ||
| // Since we cancelled, keep the compilation pinned and update the stamp. | ||
| stamp <- DateTime.UtcNow | ||
| ms.Dispose() | ||
| None | ||
| | _ -> | ||
| entry.Emitted() | ||
| ms.Dispose() // it failed, dispose of stream | ||
| None | ||
|
|
||
| let resultOpt = | ||
| match weakComp.TryGetTarget() with | ||
| | true, comp -> tryStream comp | ||
| | _ -> None | ||
| let resultOpt = | ||
| match entry.TryGetCompilation() with | ||
| | ValueSome comp -> tryStream comp | ||
| | ValueNone -> None | ||
|
|
||
| match resultOpt with | ||
| | Some _ -> resultOpt | ||
| | _ -> | ||
| match lastSuccessfulCompilations.TryGetValue(projectId) with | ||
| | true, comp -> tryStream comp | ||
| | _ -> None | ||
| match resultOpt with | ||
| | Some _ -> resultOpt | ||
| | _ -> | ||
| match lastSuccessfulCompilations.TryGetValue(projectId) with | ||
| | true, comp -> tryStream comp | ||
| | _ -> None | ||
|
|
||
| let getStamp = fun () -> stamp | ||
| let getStamp = fun () -> stamp | ||
|
|
||
| let fsRefProj = | ||
| FSharpReferencedProject.PEReference(getStamp, DelayedILModuleReader(referencedProject.OutputFilePath, getStream)) | ||
| FSharpReferencedProject.PEReference(getStamp, DelayedILModuleReader(referencedProject.OutputFilePath, getStream)) | ||
|
|
||
| weakPEReferences.Add(comp, fsRefProj) | ||
| fsRefProj | ||
| let tryGetPEReference (referencedProject: Project) (version: VersionStamp) = | ||
| match peReferences.TryGetValue referencedProject.Id with | ||
| | true, (entry, fsRefProj) when entry.Version = version -> ValueSome fsRefProj | ||
| | _ -> ValueNone | ||
|
|
||
| let createPEReference (referencedProject: Project) (version: VersionStamp) (comp: Compilation) = | ||
| let entry = PEReferenceCacheEntry(version, comp) | ||
| let fsRefProj = buildPEReference referencedProject entry | ||
| peReferences.[referencedProject.Id] <- (entry, fsRefProj) | ||
| fsRefProj | ||
|
|
||
| let rec tryComputeOptionsBySingleScriptOrFile (document: Document) userOpName = | ||
| cancellableTask { | ||
|
|
@@ -377,9 +421,13 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = | |
| FSharpReferencedProject.FSharpReference(referencedProject.OutputFilePath, projectOptions) | ||
| ) | ||
| elif referencedProject.SupportsCompilation then | ||
| let! comp = referencedProject.GetCompilationAsync(ct) | ||
| let peRef = createPEReference referencedProject comp | ||
| referencedProjects.Add(peRef) | ||
| let! version = PEReferenceVersion.getAsync referencedProject ct | ||
|
|
||
| match tryGetPEReference referencedProject version with | ||
| | ValueSome peRef -> referencedProjects.Add peRef | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖🕵️ Stale source-generated API after a C# body edit — unchanged dependent semantic version despite different public declarations. Extracted cache functions with deployed Roslyn 5.12.0-1.26428.6: merge-base succeeds, HEAD gives FS0039. Key generator-backed references on their generation inputs. using System.Linq;
using Microsoft.CodeAnalysis;
[Generator]
public sealed class BodyGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context) { }
public void Execute(GeneratorExecutionContext context)
{
var name = context.Compilation.SyntaxTrees.First().GetText().ToString().Contains("=> 1;") ? "Before" : "After";
context.AddSource("Generated.cs", $"public class Generated {{ public static int {name} => 42; }}");
}
}// Referenced C# project, with that generator installed:
public class Input { public int M() => 1; } // edit only 1 -> 2let result = Generated.After // FS0039 on HEAD, no diagnostics at merge-base
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in cfaf397: a referenced project that has analyzer references, or depends on a project that has them, is versioned by Analyzer references stand in for "has a generator": only Roslyn's out-of-process host knows which of them carry one, and asking in-process would load the generators into Visual Studio. The dependency part matters too: a generator-free C# project that takes a constant from a generated one went stale the same way. Test:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The manager-level test |
||
| | ValueNone -> | ||
| let! comp = referencedProject.GetCompilationAsync(ct) | ||
| referencedProjects.Add(createPEReference referencedProject version comp) | ||
|
|
||
| if canBail then | ||
| return ValueNone | ||
|
|
@@ -455,6 +503,11 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = | |
| if not (currentSolution.ContainsProject(pair.Key)) then | ||
| lastSuccessfulCompilations.TryRemove(pair.Key) |> ignore) | ||
|
|
||
| peReferences.ToArray() | ||
| |> Array.iter (fun pair -> | ||
| if not (currentSolution.ContainsProject(pair.Key)) then | ||
| peReferences.TryRemove(pair.Key) |> ignore) | ||
|
|
||
| checker.InvalidateConfiguration(projectOptions, userOpName = "tryComputeOptions") | ||
|
|
||
| let parsingOptions, _ = checker.GetParsingOptionsFromProjectOptions(projectOptions) | ||
|
|
@@ -540,11 +593,11 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = | |
|
|
||
| | FSharpProjectOptionsMessage.ClearOptions(projectId) -> | ||
| match cache.TryRemove(projectId) with | ||
| | true, struct (_, _, projectOptions) -> | ||
| lastSuccessfulCompilations.TryRemove(projectId) |> ignore | ||
| checker.ClearCache([ projectOptions ]) | ||
| | true, struct (_, _, projectOptions) -> checker.ClearCache([ projectOptions ]) | ||
| | _ -> () | ||
|
|
||
|
xperiandri marked this conversation as resolved.
Resolved
|
||
| lastSuccessfulCompilations.TryRemove(projectId) |> ignore | ||
| peReferences.TryRemove(projectId) |> ignore | ||
| legacyProjectSites.TryRemove(projectId) |> ignore | ||
| | FSharpProjectOptionsMessage.ClearSingleFileOptionsCache(documentId) -> | ||
| match singleFileCache.TryRemove(documentId) with | ||
|
|
@@ -553,7 +606,6 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = | |
| ProjectOptions = projectOptions | ||
| Subscription = subscription | ||
| } -> | ||
| lastSuccessfulCompilations.TryRemove(documentId.ProjectId) |> ignore | ||
| checker.ClearCache([ projectOptions ]) | ||
| subscription |> ValueOption.iter (fun handler -> handler.Dispose()) | ||
| | _ -> () | ||
|
|
@@ -591,6 +643,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = | |
| cache.Clear() | ||
| singleFileCache.Clear() | ||
| lastSuccessfulCompilations.Clear() | ||
| peReferences.Clear() | ||
|
|
||
| member _.ScriptUpdated = scriptUpdatedEvent.Publish | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.