File: src\roslyn\src\Analyzers\CSharp\Analyzers\ConvertProgram\ConvertProgramAnalysis_TopLevelStatements.cs
Web Access
Project: 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;
    }
}