| File: ConvertProgram\ConvertProgramTransform_ProgramMain.cs | Web Access |
| Project: src\roslyn\src\Features\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.Features) |
// 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.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeStyle; using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Simplification; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.ConvertProgram; using static CSharpSyntaxTokens; using static SyntaxFactory; internal static partial class ConvertProgramTransform { public static async Task<Document> ConvertToProgramMainAsync(Document document, AccessibilityModifiersRequired accessibilityModifiersRequired, CancellationToken cancellationToken) { // While the analyze and refactoring check ensure we're in a well formed state for their needs, the 'new // template' code just calls directly into this if the user prefers Program.Main. So check and make sure // this is actually something we can convert before proceeding. var root = (CompilationUnitSyntax)await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); if (root.IsTopLevelProgram()) { var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); var mainMethod = compilation.GetTopLevelStatementsMethod(); if (mainMethod is not null) { var oldClassDeclaration = root.Members.OfType<ClassDeclarationSyntax>().FirstOrDefault(IsProgramClass); var classDeclaration = await GenerateProgramClassAsync( document, oldClassDeclaration, mainMethod, accessibilityModifiersRequired, cancellationToken).ConfigureAwait(false); var newRoot = root.RemoveNodes(root.Members.OfType<GlobalStatementSyntax>().Skip(1), SyntaxGenerator.DefaultRemoveOptions); if (oldClassDeclaration is not null) { Contract.ThrowIfNull(newRoot); newRoot = newRoot.RemoveNode(oldClassDeclaration, SyntaxGenerator.DefaultRemoveOptions); } Contract.ThrowIfNull(newRoot); var firstGlobalStatement = newRoot.Members.OfType<GlobalStatementSyntax>().Single(); newRoot = newRoot.ReplaceNode(firstGlobalStatement, classDeclaration); return document.WithSyntaxRoot(newRoot); } } return document; } private static bool IsProgramClass(ClassDeclarationSyntax declaration) { return declaration.Identifier.ValueText == WellKnownMemberNames.TopLevelStatementsEntryPointTypeName && declaration.Modifiers.Any(SyntaxKind.PartialKeyword); } private static async Task<ClassDeclarationSyntax> GenerateProgramClassAsync( Document document, ClassDeclarationSyntax? oldClassDeclaration, IMethodSymbol mainMethod, AccessibilityModifiersRequired accessibilityModifiersRequired, CancellationToken cancellationToken) { var programType = mainMethod.ContainingType; // Respect user settings on if they want explicit or implicit accessibility modifiers. var useDeclaredAccessibility = accessibilityModifiersRequired is AccessibilityModifiersRequired.ForNonInterfaceMembers or AccessibilityModifiersRequired.Always; var root = (CompilationUnitSyntax)await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var generator = document.GetRequiredLanguageService<SyntaxGenerator>(); // See if we have an existing part in another file. If so, we'll have to generate our declaration as partial. var hasExistingPart = programType.DeclaringSyntaxReferences.Any(static (d, cancellationToken) => d.GetSyntax(cancellationToken) is TypeDeclarationSyntax, cancellationToken); var method = (MethodDeclarationSyntax)generator.MethodDeclaration( mainMethod, WellKnownMemberNames.EntryPointMethodName, GenerateProgramMainStatements(root, out var leadingTrivia)); method = method.WithReturnType(method.ReturnType.WithAdditionalAnnotations(Simplifier.AddImportsAnnotation)); method = (MethodDeclarationSyntax)generator.WithAccessibility( method, useDeclaredAccessibility ? mainMethod.DeclaredAccessibility : Accessibility.NotApplicable); // Workaround for simplification not being ready when we generate a new file. Substitute System.String[] // with string[]. if (method.ParameterList.Parameters is [{ Type: ArrayTypeSyntax arrayType }]) method = method.ReplaceNode(arrayType.ElementType, PredefinedType(StringKeyword)); if (oldClassDeclaration is null) { // If we dodn't have any suitable class declaration in the same file then generate it return FixupComments((ClassDeclarationSyntax)generator.ClassDeclaration( WellKnownMemberNames.TopLevelStatementsEntryPointTypeName, accessibility: useDeclaredAccessibility ? programType.DeclaredAccessibility : Accessibility.NotApplicable, modifiers: hasExistingPart ? DeclarationModifiers.Partial : DeclarationModifiers.None, members: [method]).WithLeadingTrivia(leadingTrivia)); } else { // Otherwise just add new member and process leading trivia // Old class declaration is below top-level statements and is probably separated from them with a blank line (or several ones). // So we want to remove all leading line to make class declaration begin from the first line of the file after applying refactoring var oldTriviaWithoutBlankLines = oldClassDeclaration.GetLeadingTrivia().WithoutLeadingBlankLines(); return oldClassDeclaration.WithMembers(oldClassDeclaration.Members.Add(method)) .WithLeadingTrivia(oldTriviaWithoutBlankLines.Union(leadingTrivia)); } } private static ImmutableArray<StatementSyntax> GenerateProgramMainStatements( CompilationUnitSyntax root, out SyntaxTriviaList triviaToMove) { using var _ = ArrayBuilder<StatementSyntax>.GetInstance(out var statements); triviaToMove = default; var first = true; foreach (var globalStatement in root.Members.OfType<GlobalStatementSyntax>()) { // Remove leading trivia from first statement. We'll move it to the Program type. Any directly attached // comments though stay attached to the first statement. var statement = globalStatement.Statement.WithAdditionalAnnotations(Formatter.Annotation); if (first) { first = false; triviaToMove = statement.GetLeadingTrivia(); while (triviaToMove is [.., SyntaxTrivia(SyntaxKind.SingleLineCommentTrivia), SyntaxTrivia(SyntaxKind.EndOfLineTrivia)]) triviaToMove = [.. triviaToMove.Take(triviaToMove.Count - 2)]; var commentsToPreserve = TriviaList(statement.GetLeadingTrivia().Skip(triviaToMove.Count)); statements.Add(FixupComments(statement.WithLeadingTrivia(commentsToPreserve))); } else { statements.Add(statement); } } return statements.ToImmutableAndClear(); } private static TSyntaxNode FixupComments<TSyntaxNode>(TSyntaxNode node) where TSyntaxNode : SyntaxNode { // Remove comment explaining top level statements as it isn't relevant if the user switches back to full // Program.Main form. var leadingTrivia = node.GetLeadingTrivia(); var comment = leadingTrivia.FirstOrNull( c => c.Kind() is SyntaxKind.SingleLineCommentTrivia && c.ToString().Contains("https://aka.ms/new-console-template")); if (comment == null) return node; var commentIndex = leadingTrivia.IndexOf(comment.Value); leadingTrivia = leadingTrivia.RemoveAt(commentIndex); while (commentIndex < leadingTrivia.Count && leadingTrivia[commentIndex].Kind() is SyntaxKind.EndOfLineTrivia) leadingTrivia = leadingTrivia.RemoveAt(commentIndex); return node.WithLeadingTrivia(leadingTrivia); } }