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

Support wrapping constructs even in the presence of some recoverable errors #59385

Merged
merged 5 commits into from
Feb 11, 2022
Merged
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
Expand Up @@ -9,6 +9,7 @@
using Microsoft.CodeAnalysis.CSharp.Test.Utilities;
using Microsoft.CodeAnalysis.CSharp.Wrapping;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Wrapping
Expand Down Expand Up @@ -810,6 +811,22 @@ public C(int i,
}");
}

[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsWrapping)]
[WorkItem(38986, "https://github.com/dotnet/roslyn/issues/38986")]
public async Task TestInConstructorWithSyntaxErrorAfter()
{
await TestInRegularAndScript1Async(
@"class C {
public [||]C(int i, int j) : base(,) {
}
}",
@"class C {
public C(int i,
int j) : base(,) {
}
}");
}

[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsWrapping)]
public async Task TestInIndexer()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@ Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.Wrapping
end class")
End Function

<Fact, Trait(Traits.Feature, Traits.Features.CodeActionsWrapping)>
Public Async Function TestAvailableWithSyntaxErrorAfter() As Task
Await TestInRegularAndScript1Async(
"class C
function Goobar([||]i as integer, j as integer) as
end function
end class",
"class C
function Goobar(i as integer,
j as integer) as
end function
end class")
End Function

<Fact, Trait(Traits.Feature, Traits.Features.CodeActionsWrapping)>
Public Async Function TestMissingWithSelection() As Task
Await TestMissingAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System.Linq;
using Microsoft.CodeAnalysis.CSharp.LanguageServices;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

Expand All @@ -27,7 +26,7 @@ internal partial class CSharpArgumentWrapper
protected override SeparatedSyntaxList<ArgumentSyntax> GetListItems(BaseArgumentListSyntax listSyntax)
=> listSyntax.Arguments;

protected override BaseArgumentListSyntax TryGetApplicableList(SyntaxNode node)
protected override BaseArgumentListSyntax? TryGetApplicableList(SyntaxNode node)
=> node switch
{
InvocationExpressionSyntax invocationExpression => invocationExpression.ArgumentList,
Expand All @@ -38,18 +37,23 @@ protected override BaseArgumentListSyntax TryGetApplicableList(SyntaxNode node)
};

protected override bool PositionIsApplicable(
SyntaxNode root, int position,
SyntaxNode declaration, BaseArgumentListSyntax listSyntax)
SyntaxNode root,
int position,
SyntaxNode declaration,
bool containsSyntaxError,
BaseArgumentListSyntax listSyntax)
{
if (containsSyntaxError)
return false;

var startToken = listSyntax.GetFirstToken();

if (declaration is InvocationExpressionSyntax or
ElementAccessExpressionSyntax)
if (declaration is InvocationExpressionSyntax or ElementAccessExpressionSyntax)
{
// If we have something like Foo(...) or this.Foo(...) allow anywhere in the Foo(...)
// section.
var expr = (declaration as InvocationExpressionSyntax)?.Expression ??
(declaration as ElementAccessExpressionSyntax).Expression;
((ElementAccessExpressionSyntax)declaration).Expression;
var name = TryGetInvokedName(expr);

startToken = name == null ? listSyntax.GetFirstToken() : name.GetFirstToken();
Expand All @@ -68,40 +72,32 @@ protected override bool PositionIsApplicable(
var endToken = listSyntax.GetLastToken();
var span = TextSpan.FromBounds(startToken.SpanStart, endToken.Span.End);
if (!span.IntersectsWith(position))
{
return false;
}

// allow anywhere in the arg list, as long we don't end up walking through something
// complex like a lambda/anonymous function.
var token = root.FindToken(position);
if (token.Parent.Ancestors().Contains(listSyntax))
if (token.GetRequiredParent().Ancestors().Contains(listSyntax))
{
for (var current = token.Parent; current != listSyntax; current = current.Parent)
for (var current = token.Parent; current != listSyntax; current = current?.Parent)
{
if (CSharpSyntaxFacts.Instance.IsAnonymousFunctionExpression(current))
{
return false;
}
}
}

return true;
}

private static ExpressionSyntax TryGetInvokedName(ExpressionSyntax expr)
private static ExpressionSyntax? TryGetInvokedName(ExpressionSyntax expr)
{
// `Foo(...)`. Allow up through the 'Foo' portion
if (expr is NameSyntax name)
{
return name;
}

// `this[...]`. Allow up through the 'this' token.
if (expr is ThisExpressionSyntax or BaseExpressionSyntax)
{
return expr;
}

// expr.Foo(...) or expr?.Foo(...)
// All up through the 'Foo' portion.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,12 @@ protected override SeparatedSyntaxList<ParameterSyntax> GetListItems(BaseParamet
=> node.GetParameterList();

protected override bool PositionIsApplicable(
SyntaxNode root, int position, SyntaxNode declaration, BaseParameterListSyntax listSyntax)
SyntaxNode root, int position, SyntaxNode declaration, bool containsSyntaxError, BaseParameterListSyntax listSyntax)
{
// CSharpSyntaxGenerator.GetParameterList synthesizes a parameter list for simple-lambdas.
// In that case, we're not applicable in that list.
if (declaration.Kind() == SyntaxKind.SimpleLambdaExpression)
{
return false;
}

var generator = CSharpSyntaxGenerator.Instance;
var attributes = generator.GetAttributes(declaration);
Expand All @@ -52,7 +50,13 @@ protected override bool PositionIsApplicable(
var lastToken = listSyntax.GetLastToken();

var headerSpan = TextSpan.FromBounds(firstToken.SpanStart, lastToken.Span.End);
return headerSpan.IntersectsWith(position);
if (!headerSpan.IntersectsWith(position))
return false;

if (containsSyntaxError && ContainsOverlappingSyntaxErrror(declaration, headerSpan))
dibarbet marked this conversation as resolved.
Show resolved Hide resolved
return false;

return true;
}
}
}
21 changes: 10 additions & 11 deletions src/Features/Core/Portable/Wrapping/AbstractWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Shared.Extensions;

namespace Microsoft.CodeAnalysis.Wrapping
{
using System.Linq;
using Microsoft.CodeAnalysis.Indentation;
using Microsoft.CodeAnalysis.Text;

/// <summary>
/// Common implementation of all <see cref="ISyntaxWrapper"/>. This type takes care of a lot of common logic for
Expand All @@ -32,7 +32,8 @@ internal abstract partial class AbstractSyntaxWrapper : ISyntaxWrapper
protected AbstractSyntaxWrapper(IIndentationService indentationService)
=> IndentationService = indentationService;

public abstract Task<ICodeActionComputer> TryCreateComputerAsync(Document document, int position, SyntaxNode node, CancellationToken cancellationToken);
public abstract Task<ICodeActionComputer?> TryCreateComputerAsync(
Document document, int position, SyntaxNode node, bool containsSyntaxError, CancellationToken cancellationToken);

protected static async Task<bool> ContainsUnformattableContentAsync(
Document document, IEnumerable<SyntaxNodeOrToken> nodesAndTokens, CancellationToken cancellationToken)
Expand All @@ -46,25 +47,23 @@ protected static async Task<bool> ContainsUnformattableContentAsync(
var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
foreach (var item in nodesAndTokens)
{
if (item == null ||
item.Span.IsEmpty)
{
if (item == null || item.Span.IsEmpty)
return true;
}

var firstToken = item.IsToken ? item.AsToken() : item.AsNode().GetFirstToken();
var lastToken = item.IsToken ? item.AsToken() : item.AsNode().GetLastToken();
var firstToken = item.IsToken ? item.AsToken() : item.AsNode()!.GetFirstToken();
var lastToken = item.IsToken ? item.AsToken() : item.AsNode()!.GetLastToken();

// Note: we check if things are on the same line, even in the case of a single token.
// This is so that we don't try to wrap multiline tokens either (like a multi-line
// string).
if (!sourceText.AreOnSameLine(firstToken, lastToken))
{
return true;
}
}

return false;
}

protected static bool ContainsOverlappingSyntaxErrror(SyntaxNode declaration, TextSpan headerSpan)
=> declaration.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error && d.Location.SourceSpan.OverlapsWith(headerSpan));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.Shared.Extensions;

namespace Microsoft.CodeAnalysis.Wrapping
{
Expand All @@ -34,22 +33,15 @@ public override async Task ComputeRefactoringsAsync(CodeRefactoringContext conte
{
var (document, span, cancellationToken) = context;
if (!span.IsEmpty)
{
return;
}

var position = span.Start;
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var token = root.FindToken(position);

foreach (var node in token.Parent.AncestorsAndSelf())
foreach (var node in token.GetRequiredParent().AncestorsAndSelf())
{
// Make sure we don't have any syntax errors here. Don't want to format if we don't
// really understand what's going on.
if (node.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error))
{
return;
}
var containsSyntaxError = node.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error);

// Check if any wrapper can handle this node. If so, then we're done, otherwise
// keep walking up.
Expand All @@ -58,22 +50,23 @@ public override async Task ComputeRefactoringsAsync(CodeRefactoringContext conte
cancellationToken.ThrowIfCancellationRequested();

var computer = await wrapper.TryCreateComputerAsync(
document, position, node, cancellationToken).ConfigureAwait(false);
document, position, node, containsSyntaxError, cancellationToken).ConfigureAwait(false);

if (computer == null)
{
continue;
}

var actions = await computer.GetTopLevelCodeActionsAsync().ConfigureAwait(false);
if (actions.IsDefaultOrEmpty)
{
continue;
}

context.RegisterRefactorings(actions);
return;
}

// if we hit a syntax error and the computer couldn't handle it, then bail out. Don't want to format if
// we don't really understand what's going on.
if (containsSyntaxError)
return;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -41,19 +39,18 @@ protected AbstractBinaryExpressionWrapper(
/// </summary>
protected abstract SyntaxTriviaList GetNewLineBeforeOperatorTrivia(SyntaxTriviaList newLine);

public sealed override async Task<ICodeActionComputer> TryCreateComputerAsync(
Document document, int position, SyntaxNode node, CancellationToken cancellationToken)
public sealed override async Task<ICodeActionComputer?> TryCreateComputerAsync(
Document document, int position, SyntaxNode node, bool containsSyntaxError, CancellationToken cancellationToken)
{
if (containsSyntaxError)
return null;

if (node is not TBinaryExpressionSyntax binaryExpr)
{
return null;
}

var precedence = _precedenceService.GetPrecedenceKind(binaryExpr);
if (precedence == PrecedenceKind.Other)
{
return null;
}

// Don't process this binary expression if it's in a parent binary expr of the same or
// lower precedence. We'll just allow our caller to walk up to that and call back into
Expand Down Expand Up @@ -87,9 +84,7 @@ public sealed override async Task<ICodeActionComputer> TryCreateComputerAsync(
document, exprsAndOperators, cancellationToken).ConfigureAwait(false);

if (containsUnformattableContent)
{
return null;
}

var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
var options = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false);
Expand Down
Loading