Skip to content
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 69 commits into from
Apr 6, 2022
Merged
Show file tree
Hide file tree
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 Mar 25, 2022
6819187
Extract out helper
CyrusNajmabadi Mar 25, 2022
0d141dd
Add tests
CyrusNajmabadi Mar 25, 2022
f671beb
Add tsts
CyrusNajmabadi Mar 25, 2022
6ecd6f1
Update src/Analyzers/CSharp/Analyzers/CSharpAnalyzers.projitems
CyrusNajmabadi Mar 25, 2022
18b531a
Simplify
CyrusNajmabadi Mar 25, 2022
7039b08
Merge branch 'programMain' of https://github.com/CyrusNajmabadi/rosly…
CyrusNajmabadi Mar 25, 2022
4eef1a0
Update src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertToProgram…
CyrusNajmabadi Mar 25, 2022
b5df649
Fix suppression logic
CyrusNajmabadi Mar 25, 2022
3d15c8c
Fix suppression logic
CyrusNajmabadi Mar 25, 2022
e04bbf0
Add refactoring tests
CyrusNajmabadi Mar 25, 2022
c251ca8
use existing accessibility
CyrusNajmabadi Mar 25, 2022
f9dfa65
Add using
CyrusNajmabadi Mar 25, 2022
8232b92
Simplify
CyrusNajmabadi Mar 25, 2022
f4b6b09
Fix suppression
CyrusNajmabadi Mar 25, 2022
b6d5441
Add ui
CyrusNajmabadi Mar 25, 2022
deafa3b
Add automation object
CyrusNajmabadi Mar 25, 2022
8663fa7
Fixup tests
CyrusNajmabadi Mar 25, 2022
3ea54d0
Add analyzer for other direction
CyrusNajmabadi Mar 25, 2022
ea934e9
Increase checks
CyrusNajmabadi Mar 25, 2022
67a81a2
Add fixer
CyrusNajmabadi Mar 25, 2022
d6fbcfc
tests
CyrusNajmabadi Mar 25, 2022
f529beb
tests
CyrusNajmabadi Mar 25, 2022
c7feef0
tests
CyrusNajmabadi Mar 25, 2022
b12b4ae
tests
CyrusNajmabadi Mar 25, 2022
45c7780
tests
CyrusNajmabadi Mar 25, 2022
a7d6369
Fixup
CyrusNajmabadi Mar 25, 2022
f6d9923
More tests
CyrusNajmabadi Mar 25, 2022
c729d25
Extract helpers
CyrusNajmabadi Mar 25, 2022
de1fe3f
In progress
CyrusNajmabadi Mar 25, 2022
9f9066a
Finish refactoring
CyrusNajmabadi Mar 25, 2022
7853415
Fixup downstream
CyrusNajmabadi Mar 25, 2022
76773fb
Don't do analysis in non-application proects
CyrusNajmabadi Mar 26, 2022
bf742db
cleanup
CyrusNajmabadi Mar 26, 2022
489f0bb
Flip
CyrusNajmabadi Mar 26, 2022
b78c8ff
Update tests
CyrusNajmabadi Mar 26, 2022
b408e4f
Break apart
CyrusNajmabadi Mar 26, 2022
882b1bd
Simplify
CyrusNajmabadi Mar 26, 2022
0714d3c
Add comments
CyrusNajmabadi Mar 26, 2022
15cbb00
add comment
CyrusNajmabadi Mar 26, 2022
49795f6
Update tests
CyrusNajmabadi Mar 26, 2022
6da5e10
Update comment
CyrusNajmabadi Mar 26, 2022
b9abe2a
fix export name
CyrusNajmabadi Mar 26, 2022
f2baac2
fix test
CyrusNajmabadi Mar 26, 2022
66059ad
fix test
CyrusNajmabadi Mar 26, 2022
5f352c7
make sure we use predefined type
CyrusNajmabadi Mar 26, 2022
3c96d55
Update src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertProgramAn…
CyrusNajmabadi Mar 27, 2022
cc5b6a2
Update src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertProgramAn…
CyrusNajmabadi Mar 27, 2022
b8f5617
Merge remote-tracking branch 'upstream/main' into programMain
CyrusNajmabadi Mar 27, 2022
40a8994
Allow unsafe
CyrusNajmabadi Mar 27, 2022
42176bb
Merge branch 'programMain' of https://github.com/CyrusNajmabadi/rosly…
CyrusNajmabadi Mar 27, 2022
d4da759
Add tests
CyrusNajmabadi Mar 27, 2022
8b9e759
Add comment
CyrusNajmabadi Mar 27, 2022
e962abc
Simplify
CyrusNajmabadi Mar 28, 2022
4a0af65
Update src/Features/CSharp/Portable/ConvertProgram/ConvertProgramTran…
CyrusNajmabadi Mar 28, 2022
595cb93
Move comment
CyrusNajmabadi Mar 28, 2022
eca1406
Emit fields first
CyrusNajmabadi Mar 28, 2022
80a9b66
Initialize locals
CyrusNajmabadi Mar 28, 2022
b350fb5
Simplify
CyrusNajmabadi Mar 28, 2022
8ab0abe
Update comment
CyrusNajmabadi Mar 28, 2022
1c443e8
Simplufy
CyrusNajmabadi Mar 28, 2022
1727760
Add extension
CyrusNajmabadi Mar 28, 2022
806ebea
Add support/tests for file-scoped-namespaces
CyrusNajmabadi Mar 28, 2022
1b4421b
abnners
CyrusNajmabadi Mar 28, 2022
8f208be
Simplify
CyrusNajmabadi Mar 28, 2022
42712c6
Add header tests
CyrusNajmabadi Mar 28, 2022
62229fe
fix
CyrusNajmabadi Mar 28, 2022
c284334
revert
CyrusNajmabadi Mar 28, 2022
2ac8424
Simplify logic
CyrusNajmabadi Mar 28, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Analyzers/CSharp/Analyzers/CSharpAnalyzers.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
<Compile Include="$(MSBuildThisFileDirectory)ConvertNamespace\ConvertNamespaceAnalysis.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ConvertNamespace\ConvertToBlockScopedNamespaceDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ConvertNamespace\ConvertToFileScopedNamespaceDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ConvertProgram\ConvertProgramAnalysis_TopLevelStatements.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ConvertProgram\ConvertProgramAnalysis_ProgramMain.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ConvertProgram\ConvertToTopLevelStatementsDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ConvertTypeofToNameof\CSharpConvertTypeOfToNameOfDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)NewLines\ConsecutiveBracePlacement\ConsecutiveBracePlacementDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)NewLines\ConsecutiveStatementPlacement\CSharpConsecutiveStatementPlacementDiagnosticAnalyzer.cs" />
Expand Down Expand Up @@ -61,6 +64,7 @@
<Compile Include="$(MSBuildThisFileDirectory)SimplifyLinqExpression\CSharpSimplifyLinqExpressionDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SimplifyPropertyPattern\CSharpSimplifyPropertyPatternDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SimplifyPropertyPattern\SimplifyPropertyPatternHelpers.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ConvertProgram\ConvertToProgramMainDiagnosticAnalyzer.cs" />
Copy link
Member

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?

<Compile Include="$(MSBuildThisFileDirectory)UseAutoProperty\CSharpUseAutoPropertyAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCoalesceExpression\CSharpUseCoalesceExpressionDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCoalesceExpression\CSharpUseCoalesceExpressionForNullableDiagnosticAnalyzer.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -358,4 +358,11 @@
<data name="Use_is_object_check" xml:space="preserve">
<value>Use 'is object' check</value>
</data>
<data name="Convert_to_Program_Main_style_program" xml:space="preserve">
<value>Convert to 'Program.Main' style program</value>
<comment>{Locked="Program.Main"} this is the C# syntax we are going to generate</comment>
</data>
<data name="Convert_to_top_level_statements" xml:space="preserve">
<value>Convert to top-level statements</value>
</data>
</root>
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this check if there is already a Program.Main method in the compilation too?

}

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