|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.ImplementType;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.ImplementInterface
{
internal abstract partial class AbstractImplementInterfaceService
{
// Parts of the name `disposedValue`. Used so we can generate a field correctly with
// the naming style that the user has specified.
private static readonly ImmutableArray<string> s_disposedValueNameParts =
ImmutableArray.Create("disposed", "value");
// C#: `Dispose(bool disposed)`. VB: `Dispose(disposed As Boolean)`
private static readonly SymbolDisplayFormat s_format = new(
memberOptions: SymbolDisplayMemberOptions.IncludeParameters,
parameterOptions: SymbolDisplayParameterOptions.IncludeName | SymbolDisplayParameterOptions.IncludeType,
miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
private static IMethodSymbol? TryGetIDisposableDispose(Compilation compilation)
{
// Get symbol for 'System.IDisposable'.
var idisposable = compilation.GetSpecialType(SpecialType.System_IDisposable);
if (idisposable?.TypeKind == TypeKind.Interface)
{
var idisposableMembers = idisposable.GetMembers(nameof(IDisposable.Dispose));
foreach (var member in idisposableMembers)
{
if (member is IMethodSymbol disposeMethod &&
!disposeMethod.IsStatic &&
disposeMethod.ReturnsVoid &&
disposeMethod.Arity == 0 &&
disposeMethod.Parameters.Length == 0)
{
return disposeMethod;
}
}
}
return null;
}
private static bool ShouldImplementDisposePattern(State state, bool explicitly)
{
// Dispose pattern should be implemented only if -
// 1. An interface named 'System.IDisposable' is unimplemented.
// 2. This interface has one and only one member - a non-generic method named 'Dispose' that takes no arguments and returns 'void'.
// 3. The implementing type is a class that does not already declare any conflicting members named 'disposedValue' or 'Dispose'
// (because we will be generating a 'disposedValue' field and a couple of methods named 'Dispose' as part of implementing
// the dispose pattern).
if (state.ClassOrStructType.TypeKind != TypeKind.Class)
return false;
var disposeMethod = TryGetIDisposableDispose(state.Model.Compilation);
if (disposeMethod == null)
return false;
var idisposableType = disposeMethod.ContainingType;
var unimplementedMembers = explicitly
? state.MembersWithoutExplicitImplementation
: state.MembersWithoutExplicitOrImplicitImplementationWhichCanBeImplicitlyImplemented;
if (!unimplementedMembers.Any(static (m, idisposableType) => m.type.Equals(idisposableType), idisposableType))
return false;
// The dispose pattern is only applicable if the implementing type does
// not already have an implementation of IDisposableDispose.
return state.ClassOrStructType.FindImplementationForInterfaceMember(disposeMethod) == null;
}
private sealed class ImplementInterfaceWithDisposePatternCodeAction : ImplementInterfaceCodeAction
{
public ImplementInterfaceWithDisposePatternCodeAction(
AbstractImplementInterfaceService service,
Document document,
ImplementTypeGenerationOptions options,
State state,
bool explicitly,
bool abstractly,
ISymbol? throughMember) : base(service, document, options, state, explicitly, abstractly, onlyRemaining: !explicitly, throughMember)
{
}
public static ImplementInterfaceWithDisposePatternCodeAction CreateImplementWithDisposePatternCodeAction(
AbstractImplementInterfaceService service,
Document document,
ImplementTypeGenerationOptions options,
State state)
{
return new ImplementInterfaceWithDisposePatternCodeAction(service, document, options, state, explicitly: false, abstractly: false, throughMember: null);
}
public static ImplementInterfaceWithDisposePatternCodeAction CreateImplementExplicitlyWithDisposePatternCodeAction(
AbstractImplementInterfaceService service,
Document document,
ImplementTypeGenerationOptions options,
State state)
{
return new ImplementInterfaceWithDisposePatternCodeAction(service, document, options, state, explicitly: true, abstractly: false, throughMember: null);
}
public override string Title
=> Explicitly
? FeaturesResources.Implement_interface_explicitly_with_Dispose_pattern
: FeaturesResources.Implement_interface_with_Dispose_pattern;
public override async Task<Document> GetUpdatedDocumentAsync(
Document document,
ImmutableArray<(INamedTypeSymbol type, ImmutableArray<ISymbol> members)> unimplementedMembers,
INamedTypeSymbol classType,
SyntaxNode classDecl,
CancellationToken cancellationToken)
{
var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
var disposedValueField = await CreateDisposedValueFieldAsync(
document, classType, cancellationToken).ConfigureAwait(false);
var disposeMethod = TryGetIDisposableDispose(compilation)!;
var (disposableMethods, finalizer) = CreateDisposableMethods(compilation, document, classType, disposeMethod, disposedValueField);
// First, implement all the interfaces (except for IDisposable).
var docWithCoreMembers = await GetUpdatedDocumentAsync(
document,
unimplementedMembers.WhereAsArray(m => !m.type.Equals(disposeMethod.ContainingType)),
classType,
classDecl,
extraMembers: ImmutableArray.Create<ISymbol>(disposedValueField),
cancellationToken).ConfigureAwait(false);
// Next, add the Dispose pattern methods at the end of the type (we want to keep all
// the members together).
var rootWithCoreMembers = await docWithCoreMembers.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var firstGeneratedMember = rootWithCoreMembers.GetAnnotatedNodes(CodeGenerator.Annotation).First();
var typeDeclarationWithCoreMembers = firstGeneratedMember.Parent!;
var context = new CodeGenerationContext(
addImports: false,
sortMembers: false,
autoInsertionLocation: false);
var info = await document.GetCodeGenerationInfoAsync(context, Options.FallbackOptions, cancellationToken).ConfigureAwait(false);
var typeDeclarationWithAllMembers = info.Service.AddMembers(
typeDeclarationWithCoreMembers,
disposableMethods,
info,
cancellationToken);
var docWithAllMembers = docWithCoreMembers.WithSyntaxRoot(
rootWithCoreMembers.ReplaceNode(
typeDeclarationWithCoreMembers, typeDeclarationWithAllMembers));
// Finally, add a commented out finalizer with the Dispose methods. We have to do
// this ourselves as our code-gen helpers can create real methods, but not commented
// out ones.
return await AddFinalizerCommentAsync(docWithAllMembers, finalizer, cancellationToken).ConfigureAwait(false);
}
private static async Task<Document> AddFinalizerCommentAsync(
Document document, SyntaxNode finalizer, CancellationToken cancellationToken)
{
var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var lastGeneratedMember = root.GetAnnotatedNodes(CodeGenerator.Annotation)
.OrderByDescending(n => n.SpanStart)
.First();
finalizer = finalizer.NormalizeWhitespace();
var finalizerLines = finalizer.ToFullString().Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
var generator = document.GetRequiredLanguageService<SyntaxGenerator>();
var finalizerComments = CreateCommentTrivia(generator, finalizerLines);
var lastMemberWithComments = lastGeneratedMember.WithPrependedLeadingTrivia(
finalizerComments.Insert(0, generator.CarriageReturnLineFeed)
.Add(generator.CarriageReturnLineFeed));
var finalRoot = root.ReplaceNode(lastGeneratedMember, lastMemberWithComments);
return document.WithSyntaxRoot(finalRoot);
}
private (ImmutableArray<ISymbol>, SyntaxNode) CreateDisposableMethods(
Compilation compilation,
Document document,
INamedTypeSymbol classType,
IMethodSymbol disposeMethod,
IFieldSymbol disposedValueField)
{
var disposeImplMethod = CreateDisposeImplementationMethod(compilation, document, classType, disposeMethod, disposedValueField);
var disposeMethodDisplayString = this.Service.ToDisplayString(disposeImplMethod, s_format);
var disposeInterfaceMethod = CreateDisposeInterfaceMethod(
compilation, document, classType, disposeMethod,
disposedValueField, disposeMethodDisplayString);
var g = document.GetRequiredLanguageService<SyntaxGenerator>();
var finalizer = Service.CreateFinalizer(g, classType, disposeMethodDisplayString);
return (ImmutableArray.Create<ISymbol>(disposeImplMethod, disposeInterfaceMethod), finalizer);
}
private IMethodSymbol CreateDisposeImplementationMethod(
Compilation compilation,
Document document,
INamedTypeSymbol classType,
IMethodSymbol disposeMethod,
IFieldSymbol disposedValueField)
{
var accessibility = classType.IsSealed
? Accessibility.Private
: Accessibility.Protected;
var modifiers = classType.IsSealed
? DeclarationModifiers.None
: DeclarationModifiers.Virtual;
var g = document.GetRequiredLanguageService<SyntaxGenerator>();
// if (disposing)
// {
// // TODO: dispose managed state...
// }
var ifDisposingStatement = g.IfStatement(g.IdentifierName(DisposingName), Array.Empty<SyntaxNode>());
ifDisposingStatement = Service.AddCommentInsideIfStatement(
ifDisposingStatement,
CreateCommentTrivia(g, FeaturesResources.TODO_colon_dispose_managed_state_managed_objects))
.WithoutTrivia().WithTrailingTrivia(g.CarriageReturnLineFeed, g.CarriageReturnLineFeed);
// TODO: free unmanaged ...
// TODO: set large fields...
// disposedValue = true
var disposedValueEqualsTrueStatement = AddComments(g,
FeaturesResources.TODO_colon_free_unmanaged_resources_unmanaged_objects_and_override_finalizer,
FeaturesResources.TODO_colon_set_large_fields_to_null,
g.AssignmentStatement(
g.IdentifierName(disposedValueField.Name), g.TrueLiteralExpression()));
var ifStatement = g.IfStatement(
g.LogicalNotExpression(g.IdentifierName(disposedValueField.Name)),
new[] { ifDisposingStatement, disposedValueEqualsTrueStatement });
return CodeGenerationSymbolFactory.CreateMethodSymbol(
disposeMethod,
containingType: classType,
accessibility: accessibility,
modifiers: modifiers,
name: disposeMethod.Name,
parameters: ImmutableArray.Create(
CodeGenerationSymbolFactory.CreateParameterSymbol(
compilation.GetSpecialType(SpecialType.System_Boolean),
DisposingName)),
statements: ImmutableArray.Create(ifStatement));
}
private IMethodSymbol CreateDisposeInterfaceMethod(
Compilation compilation,
Document document,
INamedTypeSymbol classType,
IMethodSymbol disposeMethod,
IFieldSymbol disposedValueField,
string disposeMethodDisplayString)
{
using var _ = ArrayBuilder<SyntaxNode>.GetInstance(out var statements);
var g = document.GetRequiredLanguageService<SyntaxGenerator>();
var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
// // Do not change...
// Dispose(true);
statements.Add(AddComment(g,
string.Format(FeaturesResources.Do_not_change_this_code_Put_cleanup_code_in_0_method, disposeMethodDisplayString),
g.ExpressionStatement(
g.InvocationExpression(
g.IdentifierName(nameof(IDisposable.Dispose)),
g.Argument(DisposingName, RefKind.None, g.TrueLiteralExpression())))));
// GC.SuppressFinalize(this);
var gcType = compilation.GetTypeByMetadataName(typeof(GC).FullName!);
if (gcType != null)
{
statements.Add(g.ExpressionStatement(
g.InvocationExpression(
g.MemberAccessExpression(
g.TypeExpression(gcType),
nameof(GC.SuppressFinalize)),
g.ThisExpression())));
}
var modifiers = DeclarationModifiers.From(disposeMethod);
modifiers = modifiers.WithIsAbstract(false);
var explicitInterfaceImplementations = Explicitly || !Service.CanImplementImplicitly
? ImmutableArray.Create(disposeMethod) : default;
var result = CodeGenerationSymbolFactory.CreateMethodSymbol(
disposeMethod,
accessibility: Explicitly ? Accessibility.NotApplicable : Accessibility.Public,
modifiers: modifiers,
explicitInterfaceImplementations: explicitInterfaceImplementations,
statements: statements.ToImmutable());
return result;
}
/// <summary>
/// This helper is implementing access to the editorconfig option. This would usually be done via <see cref="CodeFixOptionsProvider"/> but
/// we do not have access to <see cref="CodeActionOptionsProvider"/> here since the code action implementation is also used to implement <see cref="IImplementInterfaceService "/>.
/// TODO: remove - see https://github.com/dotnet/roslyn/issues/60990.
/// </summary>
public async ValueTask<AccessibilityModifiersRequired> GetAccessibilityModifiersRequiredAsync(Document document, CancellationToken cancellationToken)
{
var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
var configOptions = document.Project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(syntaxTree);
if (configOptions.TryGetEditorConfigOption<CodeStyleOption2<AccessibilityModifiersRequired>>(CodeStyleOptions2.AccessibilityModifiersRequired, out var value))
{
return value.Value;
}
var fallbackFormattingOptions = await ((OptionsProvider<SyntaxFormattingOptions>)Options.FallbackOptions).GetOptionsAsync(document.Project.Services, cancellationToken).ConfigureAwait(false);
return fallbackFormattingOptions.AccessibilityModifiersRequired;
}
private async Task<IFieldSymbol> CreateDisposedValueFieldAsync(
Document document,
INamedTypeSymbol containingType,
CancellationToken cancellationToken)
{
var rule = await document.GetApplicableNamingRuleAsync(
SymbolKind.Field, Accessibility.Private, Options.FallbackOptions, cancellationToken).ConfigureAwait(false);
var requireAccessiblity = await GetAccessibilityModifiersRequiredAsync(document, cancellationToken).ConfigureAwait(false);
var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
var boolType = compilation.GetSpecialType(SpecialType.System_Boolean);
var accessibilityLevel = requireAccessiblity is AccessibilityModifiersRequired.Never or AccessibilityModifiersRequired.OmitIfDefault
? Accessibility.NotApplicable
: Accessibility.Private;
var uniqueName = GenerateUniqueNameForDisposedValueField(containingType, rule);
return CodeGenerationSymbolFactory.CreateFieldSymbol(
default,
accessibilityLevel,
DeclarationModifiers.None,
boolType, uniqueName);
}
private static string GenerateUniqueNameForDisposedValueField(INamedTypeSymbol containingType, NamingRule rule)
{
// Determine an appropriate name to call the new field.
var baseName = rule.NamingStyle.CreateName(s_disposedValueNameParts);
// Ensure that the name is unique in the containing type so we
// don't stomp on an existing member.
var uniqueName = NameGenerator.GenerateUniqueName(
baseName, n => containingType.GetMembers(n).IsEmpty);
return uniqueName;
}
}
}
}
|