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 'Generate additional part' refactoring #3705

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

namespace StyleCop.Analyzers.PrivateCodeFixes;

using System.IO;
using System.Text;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = nameof(GeneratePartCodeRefactoringProvider))]
internal sealed class GeneratePartCodeRefactoringProvider
: CodeRefactoringProvider
{
public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
var partialType = await context.TryGetRelevantNodeAsync<ClassDeclarationSyntax>(CSharpRefactoringHelpers.Instance).ConfigureAwait(false);
if (partialType is not { Modifiers: var modifiers }
|| !modifiers.Any(SyntaxKind.PartialKeyword))
{
return;
}

context.RegisterRefactoring(CodeAction.Create(
"Generate additional part",
async cancellationToken =>
{
var namespaceDeclaration = partialType.FirstAncestorOrSelf<BaseNamespaceDeclarationSyntax>();
if (namespaceDeclaration is null)
{
return context.Document.Project.Solution;
}

var firstUsing = namespaceDeclaration.Usings.FirstOrDefault()?.Name.ToString();

var namespaceName = namespaceDeclaration.Name.ToString();
var subNamespace = namespaceName;
var rootNamespace = context.Document.Project.DefaultNamespace;
if (!string.IsNullOrEmpty(rootNamespace) && namespaceName.StartsWith(rootNamespace + "."))
{
subNamespace = namespaceName[(rootNamespace.Length + 1)..];
}

var content = $@"// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

namespace {namespaceName};

using {firstUsing};

public partial class {partialType.Identifier.ValueText} {partialType.BaseList}
{{
}}
";

var fileName = partialType.Identifier.ValueText + ".cs";
var directory = Path.GetDirectoryName(context.Document.Project.FilePath)!;
var existingText = await context.Document.GetTextAsync(cancellationToken).ConfigureAwait(false);

var addedDocument = context.Document.Project.AddDocument(
fileName,
SourceText.From(content, new UTF8Encoding(true), existingText.ChecksumAlgorithm),
folders: subNamespace.Split('.'),
filePath: Path.Combine(directory, Path.Combine(subNamespace.Split('.')), fileName));

return addedDocument.Project.Solution;
},
nameof(GeneratePartCodeRefactoringProvider)));
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// <auto-generated/>
// https://raw.githubusercontent.com/dotnet/roslyn-analyzers/84fb81c27e0554eadf6b12f97eb52c7cd2803c7e/src/Utilities/Refactoring/AbstractSyntaxFacts.cs

#nullable enable

// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;

namespace Analyzer.Utilities
{
internal abstract class AbstractSyntaxFacts
{
public abstract ISyntaxKinds SyntaxKinds { get; }

public bool IsOnHeader(SyntaxNode root, int position, SyntaxNode ownerOfHeader, SyntaxNodeOrToken lastTokenOrNodeOfHeader)
=> IsOnHeader(root, position, ownerOfHeader, lastTokenOrNodeOfHeader, ImmutableArray<SyntaxNode>.Empty);

public bool IsOnHeader<THoleSyntax>(
SyntaxNode root,
int position,
SyntaxNode ownerOfHeader,
SyntaxNodeOrToken lastTokenOrNodeOfHeader,
ImmutableArray<THoleSyntax> holes)
where THoleSyntax : SyntaxNode
{
Debug.Assert(ownerOfHeader.FullSpan.Contains(lastTokenOrNodeOfHeader.Span));

var headerSpan = TextSpan.FromBounds(
start: GetStartOfNodeExcludingAttributes(root, ownerOfHeader),
end: lastTokenOrNodeOfHeader.FullSpan.End);

// Is in header check is inclusive, being on the end edge of an header still counts
if (!headerSpan.IntersectsWith(position))
{
return false;
}

// Holes are exclusive:
// To be consistent with other 'being on the edge' of Tokens/Nodes a position is
// in a hole (not in a header) only if it's inside _inside_ a hole, not only on the edge.
if (holes.Any(h => h.Span.Contains(position) && position > h.Span.Start))
{
return false;
}

return true;
}

/// <summary>
/// Tries to get an ancestor of a Token on current position or of Token directly to left:
/// e.g.: tokenWithWantedAncestor[||]tokenWithoutWantedAncestor
/// </summary>
protected TNode? TryGetAncestorForLocation<TNode>(SyntaxNode root, int position)
where TNode : SyntaxNode
{
var tokenToRightOrIn = root.FindToken(position);
var nodeToRightOrIn = tokenToRightOrIn.GetAncestor<TNode>();
if (nodeToRightOrIn != null)
{
return nodeToRightOrIn;
}

// not at the beginning of a Token -> no (different) token to the left
if (tokenToRightOrIn.FullSpan.Start != position && tokenToRightOrIn.RawKind != SyntaxKinds.EndOfFileToken)
{
return null;
}

return tokenToRightOrIn.GetPreviousToken().GetAncestor<TNode>();
}

protected int GetStartOfNodeExcludingAttributes(SyntaxNode root, SyntaxNode node)
{
var attributeList = GetAttributeLists(node);
if (attributeList.Any())
{
var endOfAttributeLists = attributeList.Last().Span.End;
var afterAttributesToken = root.FindTokenOnRightOfPosition(endOfAttributeLists);

return Math.Min(afterAttributesToken.Span.Start, node.Span.End);
}

return node.SpanStart;
}

public abstract SyntaxList<SyntaxNode> GetAttributeLists(SyntaxNode node);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// <auto-generated/>
// https://raw.githubusercontent.com/dotnet/roslyn-analyzers/84fb81c27e0554eadf6b12f97eb52c7cd2803c7e/src/Utilities/Refactoring.CSharp/CSharpRefactoringHelpers.cs

#nullable enable

// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Analyzer.Utilities
{
internal sealed class CSharpRefactoringHelpers : AbstractRefactoringHelpers<ExpressionSyntax, ArgumentSyntax, ExpressionStatementSyntax>
{
public static CSharpRefactoringHelpers Instance { get; } = new CSharpRefactoringHelpers();

private CSharpRefactoringHelpers()
{
}

protected override ISyntaxFacts SyntaxFacts => CSharpSyntaxFacts.Instance;

protected override IEnumerable<SyntaxNode> ExtractNodesSimple(SyntaxNode? node, ISyntaxFacts syntaxFacts)
{
if (node == null)
{
yield break;
}

foreach (var extractedNode in base.ExtractNodesSimple(node, syntaxFacts))
{
yield return extractedNode;
}

// `var a = b;`
// -> `var a = b`;
if (node is LocalDeclarationStatementSyntax localDeclaration)
{
yield return localDeclaration.Declaration;
}

// var `a = b`;
if (node is VariableDeclaratorSyntax declarator)
{
var declaration = declarator.Parent;
if (declaration?.Parent is LocalDeclarationStatementSyntax localDeclarationStatement)
{
var variables = syntaxFacts.GetVariablesOfLocalDeclarationStatement(localDeclarationStatement);
if (variables.Count == 1)
{
// -> `var a = b`;
yield return declaration;

// -> `var a = b;`
yield return localDeclarationStatement;
}
}
}
}
}
}
Loading