| 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; } }