Skip to content

Commit 2bb1568

Browse files
Merge pull request #1258 from GrahamTheCoder/claude/fix-asyncinterfaces-vs-compatibility-lIFLk
VS Extensibility
2 parents dabb93a + 8eb9df9 commit 2bb1568

17 files changed

+453
-137
lines changed

CodeConv/CodeConv.csproj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@
3131

3232
<ItemGroup>
3333
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.2" />
34+
<!--
35+
System.Linq.AsyncEnumerable only ships as a 10.x package, so it transitively pulls in
36+
Microsoft.Bcl.AsyncInterfaces 10.0.0.0. That's fine here because the CodeConv CLI never
37+
loads into devenv.exe (unlike the Vsix), so the VS-shipped binding redirect ceiling on
38+
Microsoft.Bcl.AsyncInterfaces does not apply. The CodeConverter library itself avoids
39+
depending on this package precisely so the Vsix output stays compatible with VS 17.x.
40+
See VsixAssemblyCompatibilityTests.
41+
-->
42+
<PackageReference Include="System.Linq.AsyncEnumerable" Version="10.0.0" />
3443
<PackageReference Include="Microsoft.Build.Tasks.Core" Version="17.8.43" />
3544
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" />
3645
<PackageReference Include="Microsoft.CodeAnalysis.VisualBasic.Workspaces" Version="4.14.0" />

CodeConverter/CSharp/HandledEventsAnalyzer.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,10 @@ private async Task<HandledEventsAnalysis> AnalyzeAsync()
3737
#pragma warning restore RS1024 // Compare symbols correctly
3838

3939

40-
var writtenWithEventsProperties = await ancestorPropsMembersByName.Values.OfType<IPropertySymbol>().ToAsyncEnumerable()
41-
.ToDictionaryAsync(async (p, _) => p.Name, async (p, cancellationToken) => (p, await IsNeverWrittenOrOverriddenAsync(p, cancellationToken)));
40+
var writtenWithEventsProperties = new Dictionary<string, (IPropertySymbol p, bool)>();
41+
foreach (var prop in ancestorPropsMembersByName.Values.OfType<IPropertySymbol>()) {
42+
writtenWithEventsProperties[prop.Name] = (prop, await IsNeverWrittenOrOverriddenAsync(prop));
43+
}
4244

4345
var eventContainerToMethods = _type.GetMembers().OfType<IMethodSymbol>()
4446
.SelectMany(HandledEvents)

CodeConverter/CSharp/ProjectMergedDeclarationExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public static async Task<Project> WithRenamedMergedMyNamespaceAsync(this Project
6363
var projectDir = Path.Combine(vbProject.GetDirectoryPath(), "My Project");
6464

6565
var compilation = await vbProject.GetCompilationAsync(cancellationToken);
66-
var embeddedSourceTexts = await GetAllEmbeddedSourceTextAsync(compilation).Select((r, i) => (Text: r, Suffix: $".Static.{i+1}")).ToArrayAsync(cancellationToken);
66+
var embeddedSourceTexts = await GetAllEmbeddedSourceTextAsync(compilation).SelectSafe((r, i) => (Text: r, Suffix: $".Static.{i+1}")).ToArraySafeAsync(cancellationToken);
6767
var generatedSourceTexts = (Text: await GetDynamicallyGeneratedSourceTextAsync(compilation), Suffix: ".Dynamic").Yield();
6868

6969
foreach (var (text, suffix) in embeddedSourceTexts.Concat(generatedSourceTexts)) {

CodeConverter/CodeConverter.csproj

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,6 @@
5454
<PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0" />
5555
<PackageReference Include="System.Globalization.Extensions" Version="4.3.0" />
5656
<PackageReference Include="System.IO.Abstractions" Version="13.2.33" />
57-
<PackageReference Include="System.Linq.AsyncEnumerable" Version="10.0.0" />
58-
<PackageReference Include="System.Text.Encodings.Web" Version="10.0.0" />
59-
<PackageReference Include="System.Text.Json" Version="10.0.0" />
6057
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="5.0.0">
6158
<IncludeAssets>all</IncludeAssets>
6259
</PackageReference>

CodeConverter/Common/AsyncEnumerableTaskExtensions.cs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,68 @@ public static async Task<TResult[]> SelectAsync<TArg, TResult>(this IEnumerable<
9292

9393
return partitionResults.ToArray();
9494
}
95+
96+
/// <summary>
97+
/// Hand-rolled to avoid depending on <c>System.Linq.AsyncEnumerable</c>, which only ships as a 10.x
98+
/// package and so transitively forces Microsoft.Bcl.AsyncInterfaces 10.0.0.0 into the Vsix output —
99+
/// a version Visual Studio 17.x cannot bind to. See <c>VsixAssemblyCompatibilityTests</c>.
100+
/// </summary>
101+
/// <remarks>
102+
/// Deliberately not named <c>ToArrayAsync</c> so it does not clash with
103+
/// <see cref="System.Linq.AsyncEnumerable"/>.<c>ToArrayAsync</c> on .NET 10+ when this assembly is
104+
/// referenced by a project whose target framework provides the BCL version.
105+
/// </remarks>
106+
public static async Task<TSource[]> ToArraySafeAsync<TSource>(this IAsyncEnumerable<TSource> source,
107+
CancellationToken cancellationToken = default)
108+
{
109+
var list = new List<TSource>();
110+
await foreach (var item in source.WithCancellation(cancellationToken)) {
111+
list.Add(item);
112+
}
113+
return list.ToArray();
114+
}
115+
116+
/// <summary>
117+
/// Adapts a synchronous sequence to <see cref="IAsyncEnumerable{T}"/>. Hand-rolled for the same
118+
/// reason as <see cref="ToArraySafeAsync{TSource}"/>, and likewise renamed to avoid clashing with
119+
/// <see cref="System.Linq.AsyncEnumerable"/>.<c>ToAsyncEnumerable</c> on .NET 10+.
120+
/// </summary>
121+
#pragma warning disable 1998 // async method without await; required for the iterator to compile to IAsyncEnumerable.
122+
#pragma warning disable VSTHRD200 // The method returns IAsyncEnumerable, not a Task; "Async" suffix would be misleading.
123+
public static async IAsyncEnumerable<TSource> AsAsyncEnumerable<TSource>(this IEnumerable<TSource> source)
124+
{
125+
foreach (var item in source) {
126+
yield return item;
127+
}
128+
}
129+
#pragma warning restore 1998
130+
131+
/// <summary>
132+
/// Lazy projection over an <see cref="IAsyncEnumerable{T}"/>. Hand-rolled for the same reason
133+
/// as <see cref="ToArraySafeAsync{TSource}"/>, and renamed to avoid clashing with
134+
/// <see cref="System.Linq.AsyncEnumerable"/>.<c>Select</c> on .NET 10+.
135+
/// </summary>
136+
public static async IAsyncEnumerable<TResult> SelectSafe<TSource, TResult>(this IAsyncEnumerable<TSource> source,
137+
Func<TSource, TResult> selector,
138+
[EnumeratorCancellation] CancellationToken cancellationToken = default)
139+
{
140+
await foreach (var item in source.WithCancellation(cancellationToken)) {
141+
yield return selector(item);
142+
}
143+
}
144+
145+
/// <summary>
146+
/// Lazy projection (with index) over an <see cref="IAsyncEnumerable{T}"/>. Hand-rolled for the same
147+
/// reason as <see cref="ToArraySafeAsync{TSource}"/>.
148+
/// </summary>
149+
public static async IAsyncEnumerable<TResult> SelectSafe<TSource, TResult>(this IAsyncEnumerable<TSource> source,
150+
Func<TSource, int, TResult> selector,
151+
[EnumeratorCancellation] CancellationToken cancellationToken = default)
152+
{
153+
var index = 0;
154+
await foreach (var item in source.WithCancellation(cancellationToken)) {
155+
yield return selector(item, index++);
156+
}
157+
}
158+
#pragma warning restore VSTHRD200
95159
}

CodeConverter/Common/ProjectConversion.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ private ProjectConversion(IProjectContentsConverter projectContentsConverter, IE
5050
if (conversionOptions.SelectedTextSpan is { Length: > 0 } span) {
5151
document = await WithAnnotatedSelectionAsync(document, span);
5252
}
53-
var conversionResults = await ConvertDocumentsAsync<TLanguageConversion>(new[] {document}, conversionOptions, progress, cancellationToken).ToArrayAsync(cancellationToken);
53+
var conversionResults = await ConvertDocumentsAsync<TLanguageConversion>(new[] {document}, conversionOptions, progress, cancellationToken).ToArraySafeAsync(cancellationToken);
5454
var codeResult = conversionResults.First(r => r.SourcePathOrNull == document.FilePath);
5555
codeResult.Exceptions = conversionResults.SelectMany(x => x.Exceptions).ToArray();
5656
return codeResult;
@@ -183,7 +183,7 @@ private async IAsyncEnumerable<ConversionResult> ConvertAsync(IProgress<Conversi
183183
{
184184
var phaseProgress = StartPhase(progress, "Phase 1 of 2:");
185185
var firstPassResults = _documentsToConvert.ParallelSelectAwaitAsync(d => FirstPassLoggedAsync(d, phaseProgress), Env.MaxDop, _cancellationToken);
186-
var (proj1, docs1) = await _projectContentsConverter.GetConvertedProjectAsync(await firstPassResults.ToArrayAsync(_cancellationToken));
186+
var (proj1, docs1) = await _projectContentsConverter.GetConvertedProjectAsync(await firstPassResults.ToArraySafeAsync(_cancellationToken));
187187

188188
var warnings = await GetProjectWarningsAsync(_projectContentsConverter.SourceProject, proj1);
189189
if (!string.IsNullOrWhiteSpace(warnings)) {
@@ -193,7 +193,7 @@ private async IAsyncEnumerable<ConversionResult> ConvertAsync(IProgress<Conversi
193193

194194
phaseProgress = StartPhase(progress, "Phase 2 of 2:");
195195
var secondPassResults = proj1.GetDocuments(docs1).ParallelSelectAwaitAsync(d => SecondPassLoggedAsync(d, phaseProgress), Env.MaxDop, _cancellationToken);
196-
await foreach (var result in secondPassResults.Select(CreateConversionResult).WithCancellation(_cancellationToken)) {
196+
await foreach (var result in secondPassResults.SelectSafe(CreateConversionResult).WithCancellation(_cancellationToken)) {
197197
yield return result;
198198
}
199199
await foreach (var result in _projectContentsConverter.GetAdditionalConversionResultsAsync(_additionalDocumentsToConvert, _cancellationToken)) {

CodeConverter/Common/SolutionConverter.cs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,20 @@ private SolutionConverter(string solutionFilePath,
7171
public async IAsyncEnumerable<ConversionResult> ConvertAsync()
7272
{
7373
var projectsToUpdateReferencesOnly = _projectsToConvert.First().Solution.Projects.Except(_projectsToConvert);
74-
var solutionResult = string.IsNullOrWhiteSpace(_sourceSolutionContents) ? Enumerable.Empty<ConversionResult>() : ConvertSolutionFile().Yield();
75-
var convertedProjects = await ConvertProjectsAsync();
76-
var projectsAndSolutionResults = UpdateProjectReferences(projectsToUpdateReferencesOnly).Concat(solutionResult).ToAsyncEnumerable();
77-
await foreach (var p in convertedProjects.Concat(projectsAndSolutionResults)) {
78-
yield return p;
74+
75+
foreach (var project in _projectsToConvert) {
76+
await foreach (var result in ConvertProjectAsync(project).WithCancellation(_cancellationToken)) {
77+
yield return result;
78+
}
7979
}
80-
}
8180

82-
private async Task<IAsyncEnumerable<ConversionResult>> ConvertProjectsAsync()
83-
{
84-
return _projectsToConvert.ToAsyncEnumerable().SelectMany(ConvertProjectAsync);
81+
foreach (var result in UpdateProjectReferences(projectsToUpdateReferencesOnly)) {
82+
yield return result;
83+
}
84+
85+
if (!string.IsNullOrWhiteSpace(_sourceSolutionContents)) {
86+
yield return ConvertSolutionFile();
87+
}
8588
}
8689

8790
private IAsyncEnumerable<ConversionResult> ConvertProjectAsync(Project project)

Tests/Tests.csproj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,13 @@
4141
<ItemGroup>
4242
<ProjectReference Include="..\CodeConv\CodeConv.csproj" />
4343
</ItemGroup>
44+
<!--
45+
The Vsix project is a net472 Windows-only project and only builds under an MSBuild that has
46+
the WindowsDesktop SDK available. We reference it so that VsixAssemblyCompatibilityTests can
47+
statically verify the Vsix output, but only in environments that can actually build it
48+
(i.e. Windows). Elsewhere the tests that depend on Vsix output will skip.
49+
-->
50+
<ItemGroup Condition="'$(OS)' == 'Windows_NT'">
51+
<ProjectReference Include="..\Vsix\Vsix.csproj" ReferenceOutputAssembly="false" SkipGetTargetFrameworkProperties="true" PrivateAssets="all" />
52+
</ItemGroup>
4453
</Project>

0 commit comments

Comments
 (0)