Skip to content

Commit a96acf3

Browse files
authored
fix and add tests for quoted property names and enhance symbol resolution (#18509)
## Description Added support for resolving symbols when hovering over quoted property names in Bicep files. Previously, when users used "string" literals as property keys (e.g., `'propertyName': value`), hovering over the quoted property name would not display type information or descriptions. This fix extends the `TryGetSymbolInfo` method in `SymbolHelper.cs` to handle `StringSyntax` nodes that serve as keys in both `ObjectPropertySyntax` and `ObjectTypePropertySyntax` contexts. ## Checklist - [x] I have read and adhere to the [contribution guide](https://github.com/Azure/bicep/blob/main/CONTRIBUTING.md). ###### Microsoft Reviewers: [Open in CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/Azure/bicep/pull/18509)
1 parent f24940b commit a96acf3

File tree

3 files changed

+172
-3
lines changed

3 files changed

+172
-3
lines changed

src/Bicep.Core.UnitTests/Semantics/SymbolContextTests.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33
using System.Collections.Immutable;
4+
using Bicep.Core.Navigation;
5+
using Bicep.Core.Parsing;
46
using Bicep.Core.Semantics;
57
using Bicep.Core.Syntax;
68
using Bicep.Core.UnitTests.Utils;
@@ -34,5 +36,69 @@ public void LockedModeShouldBlockAccess()
3436
context.TypeManager.Should().NotBeNull();
3537
context.Binder.Should().NotBeNull();
3638
}
39+
40+
[TestMethod]
41+
public void TestHoverOnQuotedPropertyReturnsPropertySymbol()
42+
{
43+
var text = @"
44+
resource storage 'Microsoft.Storage/storageAccounts@2021-02-01' = {
45+
name: 'mystorageaccount'
46+
location: 'eastus'
47+
sku: {
48+
'name': 'Standard_LRS'
49+
}
50+
}
51+
";
52+
var skuIndex = text.IndexOf("sku:");
53+
var namePropertyIndex = text.IndexOf("'name'", skuIndex);
54+
var cursor = namePropertyIndex + 2;
55+
56+
var compilation = CompilationHelper.Compile(text).Compilation;
57+
var model = compilation.GetEntrypointSemanticModel();
58+
59+
var programSyntax = model.SourceFile.ProgramSyntax;
60+
61+
var binder = model.Binder;
62+
var node = programSyntax.TryFindMostSpecificNodeInclusive(
63+
cursor,
64+
n => n is not IdentifierSyntax &&
65+
n is not Token &&
66+
n is not AliasAsClauseSyntax &&
67+
!IsPropertyKeyString(n));
68+
69+
if (node is StringSyntax stringSyntax && TryGetPropertyNode(stringSyntax, out var propertyNode))
70+
{
71+
node = propertyNode;
72+
}
73+
74+
bool IsPropertyKeyString(SyntaxBase syntaxBase) =>
75+
syntaxBase is StringSyntax stringSyntax && TryGetPropertyNode(stringSyntax, out _);
76+
77+
bool TryGetPropertyNode(StringSyntax stringSyntax, out SyntaxBase? propertyNode)
78+
{
79+
var parent = binder.GetParent(stringSyntax);
80+
if (parent is ObjectPropertySyntax objectProperty && objectProperty.Key == stringSyntax)
81+
{
82+
propertyNode = objectProperty;
83+
return true;
84+
}
85+
86+
if (parent is ObjectTypePropertySyntax objectTypeProperty && objectTypeProperty.Key == stringSyntax)
87+
{
88+
propertyNode = objectTypeProperty;
89+
return true;
90+
}
91+
92+
propertyNode = null;
93+
return false;
94+
}
95+
96+
node.Should().NotBeNull($"cursor at position {cursor} should find a node");
97+
var symbol = model.GetSymbolInfo(node!);
98+
99+
symbol.Should().NotBeNull("hovering over a quoted property name should return a symbol");
100+
symbol.Should().BeOfType<PropertySymbol>("quoted property name should resolve to PropertySymbol");
101+
symbol!.Name.Should().Be("name");
102+
}
37103
}
38104
}

src/Bicep.LangServer.IntegrationTests/HoverTests.cs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,6 +1145,89 @@ public async Task Compiled_json_contains_trimmed_descriptions_in_hovers()
11451145
""");
11461146
}
11471147

1148+
[TestMethod]
1149+
public async Task Hovers_are_displayed_when_hovering_over_quoted_object_property_names()
1150+
{
1151+
// https://github.com/Azure/bicep/issues/18411
1152+
var (text, cursors) = ParserHelper.GetFileWithCursors("""
1153+
type foo = {
1154+
@description('A string property')
1155+
stringProp: string
1156+
@description('An int property')
1157+
intProp: int
1158+
}
1159+
param myParam foo = {
1160+
'str|ingProp': 'hello'
1161+
'intP|rop': 42
1162+
}
1163+
""", '|');
1164+
1165+
var bicepFile = new LanguageClientFile($"file:///{TestContext.TestName}-path/to/main.bicep", text);
1166+
1167+
var helper = await ServerWithBuiltInTypes.GetAsync();
1168+
await helper.OpenFileOnceAsync(TestContext, text, bicepFile.Uri);
1169+
1170+
var hovers = await RequestHovers(helper.Client, bicepFile, cursors);
1171+
1172+
hovers.Should().AllSatisfy(h => h.Should().NotBeNull());
1173+
hovers.Should().SatisfyRespectively(
1174+
h => h!.Contents.MarkupContent!.Value.Should().Contain("stringProp: string").And.Contain("A string property"),
1175+
h => h!.Contents.MarkupContent!.Value.Should().Contain("intProp: int").And.Contain("An int property")
1176+
);
1177+
}
1178+
1179+
[TestMethod]
1180+
public async Task Hovers_are_displayed_when_hovering_over_quoted_resource_property_names()
1181+
{
1182+
// https://github.com/Azure/bicep/issues/18411
1183+
var (text, cursors) = ParserHelper.GetFileWithCursors("""
1184+
resource storage 'Microsoft.Storage/storageAccounts@2021-02-01' = {
1185+
'na|me': 'mystorageaccount'
1186+
'loca|tion': 'eastus'
1187+
'ki|nd': 'StorageV2'
1188+
sku: {
1189+
'na|me': 'Standard_LRS'
1190+
}
1191+
}
1192+
""", '|');
1193+
1194+
var bicepFile = new LanguageClientFile($"file:///{TestContext.TestName}-path/to/main.bicep", text);
1195+
1196+
var helper = await ServerWithBuiltInTypes.GetAsync();
1197+
await helper.OpenFileOnceAsync(TestContext, text, bicepFile.Uri);
1198+
1199+
var hovers = await RequestHovers(helper.Client, bicepFile, cursors);
1200+
1201+
hovers.Should().AllSatisfy(h => h.Should().NotBeNull());
1202+
}
1203+
1204+
[TestMethod]
1205+
public async Task Hovers_are_displayed_when_hovering_over_quoted_type_property_names()
1206+
{
1207+
// https://github.com/Azure/bicep/issues/18411
1208+
var (text, cursors) = ParserHelper.GetFileWithCursors("""
1209+
type foo = {
1210+
@description('A string property')
1211+
'strin|gProp': string
1212+
@description('An int property')
1213+
'intP|rop': int
1214+
}
1215+
""", '|');
1216+
1217+
var bicepFile = new LanguageClientFile($"file:///{TestContext.TestName}-path/to/main.bicep", text);
1218+
1219+
var helper = await ServerWithBuiltInTypes.GetAsync();
1220+
await helper.OpenFileOnceAsync(TestContext, text, bicepFile.Uri);
1221+
1222+
var hovers = await RequestHovers(helper.Client, bicepFile, cursors);
1223+
1224+
hovers.Should().AllSatisfy(h => h.Should().NotBeNull());
1225+
hovers.Should().SatisfyRespectively(
1226+
h => h!.Contents.MarkupContent!.Value.Should().Contain("stringProp: string").And.Contain("A string property"),
1227+
h => h!.Contents.MarkupContent!.Value.Should().Contain("intProp: int").And.Contain("An int property")
1228+
);
1229+
}
1230+
11481231
private string GetManifestFileContents(string? documentationUri, string? description)
11491232
{
11501233
string annotations =

src/Bicep.LangServer/Providers/BicepSymbolResolver.cs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33
using Bicep.Core.Navigation;
44
using Bicep.Core.Parsing;
5+
using Bicep.Core.Semantics;
56
using Bicep.Core.Syntax;
67
using Bicep.LanguageServer.CompilationManager;
78
using Bicep.LanguageServer.Utils;
@@ -38,9 +39,28 @@ public BicepSymbolResolver(ILogger<BicepSymbolResolver> logger, ICompilationMana
3839
var semanticModel = context.Compilation.GetEntrypointSemanticModel();
3940

4041
// locate the most specific node that can be bound as a symbol
41-
var node = context.ProgramSyntax.TryFindMostSpecificNodeInclusive(
42-
offset,
43-
n => n is not IdentifierSyntax && n is not Token && n is not AliasAsClauseSyntax);
42+
var binder = semanticModel.Binder;
43+
bool Predicate(SyntaxBase node)
44+
{
45+
if (node is IdentifierSyntax or Token or AliasAsClauseSyntax)
46+
{
47+
return false;
48+
}
49+
50+
if (node is StringSyntax stringSyntax)
51+
{
52+
var parent = binder.GetParent(stringSyntax);
53+
if ((parent is ObjectPropertySyntax objectProperty && objectProperty.Key == stringSyntax) ||
54+
(parent is ObjectTypePropertySyntax objectTypeProperty && objectTypeProperty.Key == stringSyntax))
55+
{
56+
return false;
57+
}
58+
}
59+
60+
return true;
61+
}
62+
63+
var node = context.ProgramSyntax.TryFindMostSpecificNodeInclusive(offset, Predicate);
4464

4565
if (node is null)
4666
{

0 commit comments

Comments
 (0)