Skip to content
Merged
9 changes: 9 additions & 0 deletions CodeConv/CodeConv.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@

<ItemGroup>
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.2" />
<!--
System.Linq.AsyncEnumerable only ships as a 10.x package, so it transitively pulls in
Microsoft.Bcl.AsyncInterfaces 10.0.0.0. That's fine here because the CodeConv CLI never
loads into devenv.exe (unlike the Vsix), so the VS-shipped binding redirect ceiling on
Microsoft.Bcl.AsyncInterfaces does not apply. The CodeConverter library itself avoids
depending on this package precisely so the Vsix output stays compatible with VS 17.x.
See VsixAssemblyCompatibilityTests.
-->
<PackageReference Include="System.Linq.AsyncEnumerable" Version="10.0.0" />
<PackageReference Include="Microsoft.Build.Tasks.Core" Version="17.8.43" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" />
<PackageReference Include="Microsoft.CodeAnalysis.VisualBasic.Workspaces" Version="4.14.0" />
Expand Down
6 changes: 4 additions & 2 deletions CodeConverter/CSharp/HandledEventsAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ private async Task<HandledEventsAnalysis> AnalyzeAsync()
#pragma warning restore RS1024 // Compare symbols correctly


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

var eventContainerToMethods = _type.GetMembers().OfType<IMethodSymbol>()
.SelectMany(HandledEvents)
Expand Down
2 changes: 1 addition & 1 deletion CodeConverter/CSharp/ProjectMergedDeclarationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public static async Task<Project> WithRenamedMergedMyNamespaceAsync(this Project
var projectDir = Path.Combine(vbProject.GetDirectoryPath(), "My Project");

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

foreach (var (text, suffix) in embeddedSourceTexts.Concat(generatedSourceTexts)) {
Expand Down
3 changes: 0 additions & 3 deletions CodeConverter/CodeConverter.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,6 @@
<PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0" />
<PackageReference Include="System.Globalization.Extensions" Version="4.3.0" />
<PackageReference Include="System.IO.Abstractions" Version="13.2.33" />
<PackageReference Include="System.Linq.AsyncEnumerable" Version="10.0.0" />
<PackageReference Include="System.Text.Encodings.Web" Version="10.0.0" />
<PackageReference Include="System.Text.Json" Version="10.0.0" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="5.0.0">
<IncludeAssets>all</IncludeAssets>
</PackageReference>
Expand Down
64 changes: 64 additions & 0 deletions CodeConverter/Common/AsyncEnumerableTaskExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,68 @@ public static async Task<TResult[]> SelectAsync<TArg, TResult>(this IEnumerable<

return partitionResults.ToArray();
}

/// <summary>
/// Hand-rolled to avoid depending on <c>System.Linq.AsyncEnumerable</c>, which only ships as a 10.x
/// package and so transitively forces Microsoft.Bcl.AsyncInterfaces 10.0.0.0 into the Vsix output —
/// a version Visual Studio 17.x cannot bind to. See <c>VsixAssemblyCompatibilityTests</c>.
/// </summary>
/// <remarks>
/// Deliberately not named <c>ToArrayAsync</c> so it does not clash with
/// <see cref="System.Linq.AsyncEnumerable"/>.<c>ToArrayAsync</c> on .NET 10+ when this assembly is
/// referenced by a project whose target framework provides the BCL version.
/// </remarks>
public static async Task<TSource[]> ToArraySafeAsync<TSource>(this IAsyncEnumerable<TSource> source,
CancellationToken cancellationToken = default)
{
var list = new List<TSource>();
await foreach (var item in source.WithCancellation(cancellationToken)) {
list.Add(item);
}
return list.ToArray();
}

/// <summary>
/// Adapts a synchronous sequence to <see cref="IAsyncEnumerable{T}"/>. Hand-rolled for the same
/// reason as <see cref="ToArraySafeAsync{TSource}"/>, and likewise renamed to avoid clashing with
/// <see cref="System.Linq.AsyncEnumerable"/>.<c>ToAsyncEnumerable</c> on .NET 10+.
/// </summary>
#pragma warning disable 1998 // async method without await; required for the iterator to compile to IAsyncEnumerable.
#pragma warning disable VSTHRD200 // The method returns IAsyncEnumerable, not a Task; "Async" suffix would be misleading.
public static async IAsyncEnumerable<TSource> AsAsyncEnumerable<TSource>(this IEnumerable<TSource> source)
{
foreach (var item in source) {
yield return item;
}
}
#pragma warning restore 1998

/// <summary>
/// Lazy projection over an <see cref="IAsyncEnumerable{T}"/>. Hand-rolled for the same reason
/// as <see cref="ToArraySafeAsync{TSource}"/>, and renamed to avoid clashing with
/// <see cref="System.Linq.AsyncEnumerable"/>.<c>Select</c> on .NET 10+.
/// </summary>
public static async IAsyncEnumerable<TResult> SelectSafe<TSource, TResult>(this IAsyncEnumerable<TSource> source,
Func<TSource, TResult> selector,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await foreach (var item in source.WithCancellation(cancellationToken)) {
yield return selector(item);
}
}

/// <summary>
/// Lazy projection (with index) over an <see cref="IAsyncEnumerable{T}"/>. Hand-rolled for the same
/// reason as <see cref="ToArraySafeAsync{TSource}"/>.
/// </summary>
public static async IAsyncEnumerable<TResult> SelectSafe<TSource, TResult>(this IAsyncEnumerable<TSource> source,
Func<TSource, int, TResult> selector,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var index = 0;
await foreach (var item in source.WithCancellation(cancellationToken)) {
yield return selector(item, index++);
}
}
#pragma warning restore VSTHRD200
}
6 changes: 3 additions & 3 deletions CodeConverter/Common/ProjectConversion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ private ProjectConversion(IProjectContentsConverter projectContentsConverter, IE
if (conversionOptions.SelectedTextSpan is { Length: > 0 } span) {
document = await WithAnnotatedSelectionAsync(document, span);
}
var conversionResults = await ConvertDocumentsAsync<TLanguageConversion>(new[] {document}, conversionOptions, progress, cancellationToken).ToArrayAsync(cancellationToken);
var conversionResults = await ConvertDocumentsAsync<TLanguageConversion>(new[] {document}, conversionOptions, progress, cancellationToken).ToArraySafeAsync(cancellationToken);
var codeResult = conversionResults.First(r => r.SourcePathOrNull == document.FilePath);
codeResult.Exceptions = conversionResults.SelectMany(x => x.Exceptions).ToArray();
return codeResult;
Expand Down Expand Up @@ -183,7 +183,7 @@ private async IAsyncEnumerable<ConversionResult> ConvertAsync(IProgress<Conversi
{
var phaseProgress = StartPhase(progress, "Phase 1 of 2:");
var firstPassResults = _documentsToConvert.ParallelSelectAwaitAsync(d => FirstPassLoggedAsync(d, phaseProgress), Env.MaxDop, _cancellationToken);
var (proj1, docs1) = await _projectContentsConverter.GetConvertedProjectAsync(await firstPassResults.ToArrayAsync(_cancellationToken));
var (proj1, docs1) = await _projectContentsConverter.GetConvertedProjectAsync(await firstPassResults.ToArraySafeAsync(_cancellationToken));

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

phaseProgress = StartPhase(progress, "Phase 2 of 2:");
var secondPassResults = proj1.GetDocuments(docs1).ParallelSelectAwaitAsync(d => SecondPassLoggedAsync(d, phaseProgress), Env.MaxDop, _cancellationToken);
await foreach (var result in secondPassResults.Select(CreateConversionResult).WithCancellation(_cancellationToken)) {
await foreach (var result in secondPassResults.SelectSafe(CreateConversionResult).WithCancellation(_cancellationToken)) {
yield return result;
}
await foreach (var result in _projectContentsConverter.GetAdditionalConversionResultsAsync(_additionalDocumentsToConvert, _cancellationToken)) {
Expand Down
21 changes: 12 additions & 9 deletions CodeConverter/Common/SolutionConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,20 @@ private SolutionConverter(string solutionFilePath,
public async IAsyncEnumerable<ConversionResult> ConvertAsync()
{
var projectsToUpdateReferencesOnly = _projectsToConvert.First().Solution.Projects.Except(_projectsToConvert);
var solutionResult = string.IsNullOrWhiteSpace(_sourceSolutionContents) ? Enumerable.Empty<ConversionResult>() : ConvertSolutionFile().Yield();
var convertedProjects = await ConvertProjectsAsync();
var projectsAndSolutionResults = UpdateProjectReferences(projectsToUpdateReferencesOnly).Concat(solutionResult).ToAsyncEnumerable();
await foreach (var p in convertedProjects.Concat(projectsAndSolutionResults)) {
yield return p;

foreach (var project in _projectsToConvert) {
await foreach (var result in ConvertProjectAsync(project).WithCancellation(_cancellationToken)) {
yield return result;
}
}
}

private async Task<IAsyncEnumerable<ConversionResult>> ConvertProjectsAsync()
{
return _projectsToConvert.ToAsyncEnumerable().SelectMany(ConvertProjectAsync);
foreach (var result in UpdateProjectReferences(projectsToUpdateReferencesOnly)) {
yield return result;
}

if (!string.IsNullOrWhiteSpace(_sourceSolutionContents)) {
yield return ConvertSolutionFile();
}
}

private IAsyncEnumerable<ConversionResult> ConvertProjectAsync(Project project)
Expand Down
9 changes: 9 additions & 0 deletions Tests/Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,13 @@
<ItemGroup>
<ProjectReference Include="..\CodeConv\CodeConv.csproj" />
</ItemGroup>
<!--
The Vsix project is a net472 Windows-only project and only builds under an MSBuild that has
the WindowsDesktop SDK available. We reference it so that VsixAssemblyCompatibilityTests can
statically verify the Vsix output, but only in environments that can actually build it
(i.e. Windows). Elsewhere the tests that depend on Vsix output will skip.
-->
<ItemGroup Condition="'$(OS)' == 'Windows_NT'">
<ProjectReference Include="..\Vsix\Vsix.csproj" ReferenceOutputAssembly="false" SkipGetTargetFrameworkProperties="true" PrivateAssets="all" />
</ItemGroup>
</Project>
Loading
Loading