-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add feature to convert from top-level-statements to Program.Main form #60383
Merged
Merged
Changes from all commits
Commits
Show all changes
69 commits
Select commit
Hold shift + click to select a range
d9825ad
Add feature to convert from top-level-statements to Program.Main form
CyrusNajmabadi 6819187
Extract out helper
CyrusNajmabadi 0d141dd
Add tests
CyrusNajmabadi f671beb
Add tsts
CyrusNajmabadi 6ecd6f1
Update src/Analyzers/CSharp/Analyzers/CSharpAnalyzers.projitems
CyrusNajmabadi 18b531a
Simplify
CyrusNajmabadi 7039b08
Merge branch 'programMain' of https://github.com/CyrusNajmabadi/rosly…
CyrusNajmabadi 4eef1a0
Update src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertToProgram…
CyrusNajmabadi b5df649
Fix suppression logic
CyrusNajmabadi 3d15c8c
Fix suppression logic
CyrusNajmabadi e04bbf0
Add refactoring tests
CyrusNajmabadi c251ca8
use existing accessibility
CyrusNajmabadi f9dfa65
Add using
CyrusNajmabadi 8232b92
Simplify
CyrusNajmabadi f4b6b09
Fix suppression
CyrusNajmabadi b6d5441
Add ui
CyrusNajmabadi deafa3b
Add automation object
CyrusNajmabadi 8663fa7
Fixup tests
CyrusNajmabadi 3ea54d0
Add analyzer for other direction
CyrusNajmabadi ea934e9
Increase checks
CyrusNajmabadi 67a81a2
Add fixer
CyrusNajmabadi d6fbcfc
tests
CyrusNajmabadi f529beb
tests
CyrusNajmabadi c7feef0
tests
CyrusNajmabadi b12b4ae
tests
CyrusNajmabadi 45c7780
tests
CyrusNajmabadi a7d6369
Fixup
CyrusNajmabadi f6d9923
More tests
CyrusNajmabadi c729d25
Extract helpers
CyrusNajmabadi de1fe3f
In progress
CyrusNajmabadi 9f9066a
Finish refactoring
CyrusNajmabadi 7853415
Fixup downstream
CyrusNajmabadi 76773fb
Don't do analysis in non-application proects
CyrusNajmabadi bf742db
cleanup
CyrusNajmabadi 489f0bb
Flip
CyrusNajmabadi b78c8ff
Update tests
CyrusNajmabadi b408e4f
Break apart
CyrusNajmabadi 882b1bd
Simplify
CyrusNajmabadi 0714d3c
Add comments
CyrusNajmabadi 15cbb00
add comment
CyrusNajmabadi 49795f6
Update tests
CyrusNajmabadi 6da5e10
Update comment
CyrusNajmabadi b9abe2a
fix export name
CyrusNajmabadi f2baac2
fix test
CyrusNajmabadi 66059ad
fix test
CyrusNajmabadi 5f352c7
make sure we use predefined type
CyrusNajmabadi 3c96d55
Update src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertProgramAn…
CyrusNajmabadi cc5b6a2
Update src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertProgramAn…
CyrusNajmabadi b8f5617
Merge remote-tracking branch 'upstream/main' into programMain
CyrusNajmabadi 40a8994
Allow unsafe
CyrusNajmabadi 42176bb
Merge branch 'programMain' of https://github.com/CyrusNajmabadi/rosly…
CyrusNajmabadi d4da759
Add tests
CyrusNajmabadi 8b9e759
Add comment
CyrusNajmabadi e962abc
Simplify
CyrusNajmabadi 4a0af65
Update src/Features/CSharp/Portable/ConvertProgram/ConvertProgramTran…
CyrusNajmabadi 595cb93
Move comment
CyrusNajmabadi eca1406
Emit fields first
CyrusNajmabadi 80a9b66
Initialize locals
CyrusNajmabadi b350fb5
Simplify
CyrusNajmabadi 8ab0abe
Update comment
CyrusNajmabadi 1c443e8
Simplufy
CyrusNajmabadi 1727760
Add extension
CyrusNajmabadi 806ebea
Add support/tests for file-scoped-namespaces
CyrusNajmabadi 1b4421b
abnners
CyrusNajmabadi 8f208be
Simplify
CyrusNajmabadi 42712c6
Add header tests
CyrusNajmabadi 62229fe
fix
CyrusNajmabadi c284334
revert
CyrusNajmabadi 2ac8424
Simplify logic
CyrusNajmabadi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
80 changes: 80 additions & 0 deletions
80
src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertProgramAnalysis_ProgramMain.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
// 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 Microsoft.CodeAnalysis.CodeStyle; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Microsoft.CodeAnalysis.Shared.Extensions; | ||
using Microsoft.CodeAnalysis.Text; | ||
using Roslyn.Utilities; | ||
using Microsoft.CodeAnalysis.CSharp.Extensions; | ||
|
||
namespace Microsoft.CodeAnalysis.CSharp.Analyzers.ConvertProgram | ||
{ | ||
internal static partial class ConvertProgramAnalysis | ||
{ | ||
public static bool IsApplication(Compilation compilation) | ||
=> IsApplication(compilation.Options); | ||
|
||
public static bool IsApplication(CompilationOptions options) | ||
=> options.OutputKind is OutputKind.ConsoleApplication or OutputKind.WindowsApplication; | ||
|
||
public static bool CanOfferUseProgramMain( | ||
CodeStyleOption2<bool> option, | ||
CompilationUnitSyntax root, | ||
Compilation compilation, | ||
bool forAnalyzer) | ||
{ | ||
// We only have to check if the first member is a global statement. Global statements following anything | ||
// else is not legal. | ||
if (!root.IsTopLevelProgram()) | ||
return false; | ||
|
||
if (!CanOfferUseProgramMain(option, forAnalyzer)) | ||
return false; | ||
|
||
// resiliency check for later on. This shouldn't happen but we don't want to crash if we are in a weird | ||
// state where we have top level statements but no 'Program' type. | ||
var programType = compilation.GetBestTypeByMetadataName(WellKnownMemberNames.TopLevelStatementsEntryPointTypeName); | ||
if (programType == null) | ||
return false; | ||
|
||
if (programType.GetMembers(WellKnownMemberNames.TopLevelStatementsEntryPointMethodName).FirstOrDefault() is not IMethodSymbol) | ||
return false; | ||
|
||
return true; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this check if there is already a |
||
} | ||
|
||
private static bool CanOfferUseProgramMain(CodeStyleOption2<bool> option, bool forAnalyzer) | ||
{ | ||
var userPrefersProgramMain = option.Value == false; | ||
var analyzerDisabled = option.Notification.Severity == ReportDiagnostic.Suppress; | ||
var forRefactoring = !forAnalyzer; | ||
|
||
// If the user likes Program.Main, then we offer to conver to Program.Main from the diagnostic analyzer. | ||
// If the user prefers Top-level-statements then we offer to use Program.Main from the refactoring provider. | ||
// If the analyzer is disabled completely, the refactoring is enabled in both directions. | ||
var canOffer = userPrefersProgramMain == forAnalyzer || (forRefactoring && analyzerDisabled); | ||
return canOffer; | ||
} | ||
|
||
public static Location GetUseProgramMainDiagnosticLocation(CompilationUnitSyntax root, bool isHidden) | ||
{ | ||
// if the diagnostic is hidden, show it anywhere from the top of the file through the end of the last global | ||
// statement. That way the user can make the change anywhere in teh top level code. Otherwise, just put | ||
// the diagnostic on the start of the first global statement. | ||
if (!isHidden) | ||
return root.Members.OfType<GlobalStatementSyntax>().First().GetFirstToken().GetLocation(); | ||
|
||
// note: the legal start has to come after any #pragma directives. We don't want this to be suppressed, but | ||
// then have the span of the diagnostic end up outside the suppression. | ||
var lastPragma = root.GetFirstToken().LeadingTrivia.LastOrDefault(t => t.Kind() is SyntaxKind.PragmaWarningDirectiveTrivia); | ||
var start = lastPragma == default ? 0 : lastPragma.FullSpan.End; | ||
|
||
return Location.Create( | ||
root.SyntaxTree, | ||
TextSpan.FromBounds(start, root.Members.OfType<GlobalStatementSyntax>().Last().FullSpan.End)); | ||
} | ||
} | ||
} |
156 changes: 156 additions & 0 deletions
156
src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertProgramAnalysis_TopLevelStatements.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
// 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) | ||
CyrusNajmabadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
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.Count == 1 && | ||
methodDeclaration.ParameterList.Parameters[0].Identifier.ValueText != "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; | ||
CyrusNajmabadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// 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; | ||
} | ||
} | ||
} |
65 changes: 65 additions & 0 deletions
65
src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertToProgramMainDiagnosticAnalyzer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
// 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 Microsoft.CodeAnalysis.CodeStyle; | ||
using Microsoft.CodeAnalysis.CSharp.Analyzers.ConvertProgram; | ||
using Microsoft.CodeAnalysis.CSharp.CodeStyle; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Microsoft.CodeAnalysis.Diagnostics; | ||
|
||
namespace Microsoft.CodeAnalysis.CSharp.TopLevelStatements | ||
{ | ||
using static ConvertProgramAnalysis; | ||
|
||
[DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
internal sealed class ConvertToProgramMainDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer | ||
{ | ||
public ConvertToProgramMainDiagnosticAnalyzer() | ||
: base( | ||
IDEDiagnosticIds.UseProgramMainId, | ||
EnforceOnBuildValues.UseProgramMain, | ||
CSharpCodeStyleOptions.PreferTopLevelStatements, | ||
LanguageNames.CSharp, | ||
new LocalizableResourceString(nameof(CSharpAnalyzersResources.Convert_to_Program_Main_style_program), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) | ||
{ | ||
} | ||
|
||
public override DiagnosticAnalyzerCategory GetAnalyzerCategory() | ||
=> DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; | ||
|
||
protected override void InitializeWorker(AnalysisContext context) | ||
{ | ||
context.RegisterCompilationStartAction(context => | ||
{ | ||
if (!IsApplication(context.Compilation)) | ||
return; | ||
|
||
context.RegisterSyntaxNodeAction(ProcessCompilationUnit, SyntaxKind.CompilationUnit); | ||
}); | ||
} | ||
|
||
private void ProcessCompilationUnit(SyntaxNodeAnalysisContext context) | ||
{ | ||
var options = context.Options; | ||
var root = (CompilationUnitSyntax)context.Node; | ||
|
||
var optionSet = options.GetAnalyzerOptionSet(root.SyntaxTree, context.CancellationToken); | ||
var option = optionSet.GetOption(CSharpCodeStyleOptions.PreferTopLevelStatements); | ||
|
||
if (!CanOfferUseProgramMain(option, root, context.Compilation, forAnalyzer: true)) | ||
return; | ||
|
||
var severity = option.Notification.Severity; | ||
|
||
context.ReportDiagnostic(DiagnosticHelper.Create( | ||
this.Descriptor, | ||
GetUseProgramMainDiagnosticLocation( | ||
root, isHidden: severity.WithDefaultSeverity(DiagnosticSeverity.Hidden) == ReportDiagnostic.Hidden), | ||
severity, | ||
ImmutableArray<Location>.Empty, | ||
ImmutableDictionary<string, string?>.Empty)); | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to preserve the alphabetical order of directory names in this file?