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

Display function overload description on hover #4669

Merged
merged 3 commits into from
Sep 30, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
58 changes: 51 additions & 7 deletions src/Bicep.LangServer.IntegrationTests/HoverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ node is not ITopLevelNamedDeclarationSyntax &&
}


[DataTestMethod]
[TestMethod]
public async Task PropertyHovers_are_displayed_on_properties()
{
var (file, cursors) = ParserHelper.GetFileWithCursors(@"
Expand Down Expand Up @@ -202,7 +202,7 @@ public async Task PropertyHovers_are_displayed_on_properties()
}


[DataTestMethod]
[TestMethod]
public async Task PropertyHovers_are_displayed_on_properties_with_loops()
{
var (file, cursors) = ParserHelper.GetFileWithCursors(@"
Expand Down Expand Up @@ -233,7 +233,7 @@ public async Task PropertyHovers_are_displayed_on_properties_with_loops()
}


[DataTestMethod]
[TestMethod]
public async Task PropertyHovers_are_displayed_on_properties_with_conditions()
{
var (file, cursors) = ParserHelper.GetFileWithCursors(@"
Expand Down Expand Up @@ -263,7 +263,7 @@ public async Task PropertyHovers_are_displayed_on_properties_with_conditions()
h => h!.Contents.MarkupContent!.Value.Should().Be("```bicep\nreadonly: string\n```\nThis is a property which only supports reading.\n"));
}

[DataTestMethod]
[TestMethod]
public async Task Hovers_are_displayed_on_discription_decorator_objects()
{
var (file, cursors) = ParserHelper.GetFileWithCursors(@"
Expand Down Expand Up @@ -299,7 +299,7 @@ var test|Param string
h => h!.Contents.MarkupContent!.Value.Should().EndWith("```\nthis is my output\n"));
}

[DataTestMethod]
[TestMethod]
public async Task Hovers_are_displayed_on_discription_decorator_objects_across_bicep_modules()
{
var modFile = @"
Expand Down Expand Up @@ -352,7 +352,40 @@ param param1 string
h => h!.Contents.MarkupContent!.Value.Should().EndWith("```\nthis \nis \nout2\n"));
}

[DataTestMethod]
[TestMethod]
public async Task Function_hovers_include_descriptions_if_function_overload_has_been_resolved()
{
var hovers = await RequestHoversAtCursorLocations(@"
var rgFunc = resource|Group()
var nsRgFunc = az.resourceGroup|()

var concatFunc = conc|at('abc', 'def')
var nsConcatFunc = sys.c|oncat('abc', 'def')
");

hovers.Should().SatisfyRespectively(
h => h!.Contents.MarkupContent!.Value.Should().Be("```bicep\nfunction resourceGroup(): resourceGroup\n```\nReturns the current resource group scope. **This function can only be used in resourceGroup deployments.**\n"),
h => h!.Contents.MarkupContent!.Value.Should().Be("```bicep\nfunction resourceGroup(): resourceGroup\n```\nReturns the current resource group scope. **This function can only be used in resourceGroup deployments.**\n"),
h => h!.Contents.MarkupContent!.Value.Should().Be("```bicep\nfunction concat('abc', 'def'): string\n```\nCombines multiple string, integer, or boolean values and returns them as a concatenated string.\n"),
h => h!.Contents.MarkupContent!.Value.Should().Be("```bicep\nfunction concat('abc', 'def'): string\n```\nCombines multiple string, integer, or boolean values and returns them as a concatenated string.\n"));
}

[TestMethod]
public async Task Function_hovers_display_without_descriptions_if_function_overload_has_not_been_resolved()
{
// using the any type, we don't know which particular overload has been selected, so we cannot show an accurate description.
// this should be taken care of with https://github.com/Azure/bicep/issues/4588
var hovers = await RequestHoversAtCursorLocations(@"
var concatFunc = conc|at(any('hello'))
var nsConcatFunc = sys.conc|at(any('hello'))
");

hovers.Should().SatisfyRespectively(
h => h!.Contents.MarkupContent!.Value.Should().Be("```bicep\nfunction concat(any): any\n```\n"),
h => h!.Contents.MarkupContent!.Value.Should().Be("```bicep\nfunction concat(any): any\n```\n"));
}

[TestMethod]
public async Task Hovers_are_displayed_on_discription_decorator_objects_across_arm_modules()
{
var modFile = @"
Expand Down Expand Up @@ -416,7 +449,7 @@ param param1 string
h => h!.Contents.MarkupContent!.Value.Should().EndWith("```\nthis \nis \nout2\n"));
}

[DataTestMethod]
[TestMethod]
public async Task PropertyHovers_are_displayed_on_partial_discriminator_objects()
{
var (file, cursors) = ParserHelper.GetFileWithCursors(@"
Expand Down Expand Up @@ -514,5 +547,16 @@ private static IEnumerable<object[]> GetData()

return hovers;
}

public async Task<IEnumerable<Hover?>> RequestHoversAtCursorLocations(string fileWithCursors)
{
var (file, cursors) = ParserHelper.GetFileWithCursors(fileWithCursors);

var bicepFile = SourceFileFactory.CreateBicepFile(new Uri("file:///path/to/main.bicep"), file);
var client = await IntegrationTestHelper.StartServerWithTextAsync(this.TestContext, file, bicepFile.FileUri, creationOptions: new LanguageServer.Server.CreationOptions(NamespaceProvider: BuiltInTestTypes.Create()));


return await RequestHovers(client, bicepFile, cursors);
}
}
}
23 changes: 13 additions & 10 deletions src/Bicep.LangServer/Handlers/BicepHoverHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using OmniSharp.Extensions.LanguageServer.Protocol.Document;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
using Bicep.Core.Semantics.Namespaces;
using Bicep.LanguageServer.CompilationManager;

namespace Bicep.LanguageServer.Handlers
{
Expand Down Expand Up @@ -96,10 +97,10 @@ public BicepHoverHandler(ISymbolResolver symbolResolver)
case BuiltInNamespaceSymbol builtInNamespace:
return CodeBlock($"{builtInNamespace.Name} namespace");

case FunctionSymbol function when result.Origin is FunctionCallSyntax functionCall:
case FunctionSymbol function when result.Origin is FunctionCallSyntaxBase functionCall:
// it's not possible for a non-function call syntax to resolve to a function symbol
// but this simplifies the checks
return CodeBlock(GetFunctionMarkdown(function, functionCall.Arguments, result.Origin, result.Context.Compilation.GetEntrypointSemanticModel()));
return GetFunctionMarkdown(function, functionCall, result.Context.Compilation.GetEntrypointSemanticModel());

case PropertySymbol property:
if (GetModuleParameterOrOutputDescription(request, result, $"{property.Name}: {property.Type}", out var codeBlock))
Expand All @@ -108,10 +109,6 @@ public BicepHoverHandler(ISymbolResolver symbolResolver)
}
return CodeBlockWithDescription($"{property.Name}: {property.Type}", property.Description);

case FunctionSymbol function when result.Origin is InstanceFunctionCallSyntax functionCall:
return CodeBlock(
GetFunctionMarkdown(function, functionCall.Arguments, result.Origin, result.Context.Compilation.GetEntrypointSemanticModel()));

case LocalVariableSymbol local:
return CodeBlock($"{local.Name}: {local.Type}");

Expand All @@ -130,15 +127,15 @@ private static string CodeBlock(string content) =>
// Markdown needs two leading whitespaces before newline to insert a line break
private static string CodeBlockWithDescription(string content, string? description) => CodeBlock(content) + (description is not null ? $"{description.Replace("\n", " \n")}\n" : string.Empty);

private static string GetFunctionMarkdown(FunctionSymbol function, ImmutableArray<FunctionArgumentSyntax> arguments, SyntaxBase functionCall, SemanticModel model)
private static string GetFunctionMarkdown(FunctionSymbol function, FunctionCallSyntaxBase functionCall, SemanticModel model)
{
var buffer = new StringBuilder();
buffer.Append($"function ");
buffer.Append(function.Name);
buffer.Append('(');

const string argumentSeparator = ", ";
foreach (FunctionArgumentSyntax argumentSyntax in arguments)
foreach (FunctionArgumentSyntax argumentSyntax in functionCall.Arguments)
{
var argumentType = model.GetTypeInfo(argumentSyntax);
buffer.Append(argumentType);
Expand All @@ -147,15 +144,21 @@ private static string GetFunctionMarkdown(FunctionSymbol function, ImmutableArra
}

// remove trailing argument separator (if any)
if (arguments.Length > 0)
if (functionCall.Arguments.Length > 0)
{
buffer.Remove(buffer.Length - argumentSeparator.Length, argumentSeparator.Length);
}

buffer.Append("): ");
buffer.Append(model.GetTypeInfo(functionCall));

return buffer.ToString();
if (model.TypeManager.GetMatchedFunctionOverload(functionCall) is {} matchedOverload)
{
return CodeBlockWithDescription(buffer.ToString(), matchedOverload.Description);
}

// TODO fall back to displaying a more generic description if unable to resolve a particular overload, once https://github.com/Azure/bicep/issues/4588 has been implemented.
return CodeBlock(buffer.ToString());
}

private static bool GetModuleParameterOrOutputDescription(HoverParams request, SymbolResolutionResult result, string content, [NotNullWhen(true)] out string? codeBlock)
Expand Down