File: ConvertProgramAnalysis_TopLevelStatements.cs
Web Access
Project: ..\..\..\src\CodeStyle\CSharp\Analyzers\Microsoft.CodeAnalysis.CSharp.CodeStyle.csproj (Microsoft.CodeAnalysis.CSharp.CodeStyle)
// 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.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.Analyzers.ConvertProgram
{
    internal static partial class ConvertProgramAnalysis
    {
        public static bool CanOfferUseTopLevelStatements(CodeStyleOption2<bool> option, bool forAnalyzer)
        {
            var userPrefersTopLevelStatements = option.Value == true;
            var analyzerDisabled = option.Notification.Severity == ReportDiagnostic.Suppress;
            var forRefactoring = !forAnalyzer;
 
            // If the user likes top level statements, then we offer to convert to them from the diagnostic analyzer.
            // If the user prefers Program.Main then we offer to use top-level-statements from the refactoring provider.
            // If the analyzer is disabled completely, the refactoring is enabled in both directions.
            var canOffer = userPrefersTopLevelStatements == forAnalyzer || (forRefactoring && analyzerDisabled);
            return canOffer;
        }
 
        public static Location GetUseTopLevelStatementsDiagnosticLocation(MethodDeclarationSyntax methodDeclaration, bool isHidden)
        {
            // if the diagnostic is hidden, show it anywhere on the main method. Otherwise, just put the diagnostic on
            // the the 'Main' identifier.
            return isHidden ? methodDeclaration.GetLocation() : methodDeclaration.Identifier.GetLocation();
        }
 
        public static string? GetMainTypeName(Compilation compilation)
        {
            var mainTypeFullName = compilation.Options.MainTypeName;
            var mainTypeName = mainTypeFullName?.Split('.').Last();
            return mainTypeName;
        }
 
        public static bool IsProgramMainMethod(
            SemanticModel semanticModel,
            MethodDeclarationSyntax methodDeclaration,
            string? mainTypeName,
            CancellationToken cancellationToken,
            out bool canConvertToTopLevelStatements)
        {
            canConvertToTopLevelStatements = false;
 
            // Quick syntactic checks to allow us to avoid most methods.  We basically filter out anything that isn't
            // `static Main` immediately.
            //
            // For simplicity, we require the method to have a body so that we don't have to care about
            // expression-bodied members later.
            if (!methodDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword) ||
                methodDeclaration.TypeParameterList is not null ||
                methodDeclaration.Identifier.ValueText != WellKnownMemberNames.EntryPointMethodName ||
                methodDeclaration.Parent is not TypeDeclarationSyntax containingTypeDeclaration ||
                methodDeclaration.Body == null)
            {
                return false;
            }
 
            // If the compilation options specified a type name that Main should be found in, then do a quick check that
            // our containing type matches that.
            if (mainTypeName != null && containingTypeDeclaration.Identifier.ValueText != mainTypeName)
                return false;
 
            // If the user renamed the 'args' parameter, we can't convert to top level statements.
            if (methodDeclaration.ParameterList.Parameters is [{ Identifier.ValueText: not "args" }])
                return false;
 
            // Found a suitable candidate.  See if this matches the entrypoint the compiler has actually chosen.
            var entryPointMethod = semanticModel.Compilation.GetEntryPoint(cancellationToken);
            if (entryPointMethod == null)
                return false;
 
            var thisMethod = semanticModel.GetDeclaredSymbol(methodDeclaration);
            if (!entryPointMethod.Equals(thisMethod))
                return false;
 
            // We found the entrypoint.  However, we can only effectively convert this to top-level-statements
            // if the existing type is amenable to that.
            canConvertToTopLevelStatements = TypeCanBeConverted(entryPointMethod.ContainingType, containingTypeDeclaration);
            return true;
        }
 
        private static bool TypeCanBeConverted(INamedTypeSymbol containingType, TypeDeclarationSyntax typeDeclaration)
        {
            // Can't convert if our Program type derives or implements anything special.
            if (containingType.BaseType?.SpecialType != SpecialType.System_Object)
                return false;
 
            if (containingType.AllInterfaces.Length > 0)
                return false;
 
            // Too complex to convert many parts to top-level statements.  Just bail on this for now.
            if (containingType.DeclaringSyntaxReferences.Length > 1)
                return false;
 
            // Too complex to support converting a nested type.
            if (containingType.ContainingType != null)
                return false;
 
            // If the type wasn't internal it might have been public and something outside this assembly might be using it.
            if (containingType.DeclaredAccessibility == Accessibility.Public)
                return false;
 
            // type can't be converted with attributes.
            if (typeDeclaration.AttributeLists.Count > 0)
                return false;
 
            // can't convert doc comments to top level statements.
            if (typeDeclaration.GetLeadingTrivia().Any(t => t.IsDocComment()))
                return false;
 
            // All the members of the type need to be private/static.  And we can only have fields or methods. that's to
            // ensure that no one else was calling into this type, and that we can convert everything in the type to
            // either locals or local-functions.
 
            foreach (var member in typeDeclaration.Members)
            {
                // method can't be converted with attributes.  While a local function could support it, it would likely
                // change the meaning of the program if reflection is being used to try to find this method.
                if (member.AttributeLists.Count > 0)
                    return false;
 
                // if not private, can't convert as something may be referencing it.
                if (member.Modifiers.Any(m => m.Kind() is SyntaxKind.PublicKeyword or SyntaxKind.ProtectedKeyword or SyntaxKind.InternalKeyword))
                    return false;
 
                if (!member.Modifiers.Any(SyntaxKind.StaticKeyword))
                    return false;
 
                if (member is not FieldDeclarationSyntax and not MethodDeclarationSyntax)
                    return false;
 
                // if a method, it has to actually have a body so we can convert it to a local function.
                if (member is MethodDeclarationSyntax { Body: null, ExpressionBody: null })
                    return false;
 
                // can't convert doc comments to top level statements.
                if (member.GetLeadingTrivia().Any(t => t.IsDocComment()))
                    return false;
            }
 
            return true;
        }
    }
}