From 6d2e95f7f8a5f4e614bb7e7481fcd8c7cc691218 Mon Sep 17 00:00:00 2001 From: Engin Polat <118744+polatengin@users.noreply.github.com> Date: Sat, 14 Mar 2026 08:59:34 -0700 Subject: [PATCH] improve inline type property decorator completion logic and add tests --- .../CompletionTests.cs | 46 ++++++++++++++++++ .../Completions/BicepCompletionProvider.cs | 47 ++++++++++++------- 2 files changed, 77 insertions(+), 16 deletions(-) diff --git a/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs b/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs index 415c22a59e9..e3f13711c92 100644 --- a/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs @@ -2117,6 +2117,52 @@ public async Task TypePropertyDecoratorsShouldAlignWithPropertyType() completions.Should().NotContain(x => x.Label == "discriminator"); } + [TestMethod] + public async Task InlineTypePropertyDecoratorsShouldAlignWithPropertyType() + { + var fileWithCursors = """ + param clusterSettings { + @| + name: string + } + """; + var (text, cursor) = ParserHelper.GetFileWithSingleCursor(fileWithCursors, '|'); + var file = await new ServerRequestHelper(TestContext, DefaultServer).OpenFile(text); + var completions = await file.RequestAndResolveCompletions(cursor); + + completions.Should().Contain(x => x.Label == "description"); + completions.Should().Contain(x => x.Label == "metadata"); + completions.Should().Contain(x => x.Label == "minLength"); + completions.Should().Contain(x => x.Label == "maxLength"); + + completions.Should().NotContain(x => x.Label == "allowed"); + completions.Should().NotContain(x => x.Label == "sealed"); + completions.Should().NotContain(x => x.Label == "discriminator"); + } + + [TestMethod] + public async Task QualifiedInlineTypePropertyDecoratorsShouldAlignWithPropertyType() + { + var fileWithCursors = """ + param clusterSettings { + @sys.| + name: string + } + """; + var (text, cursor) = ParserHelper.GetFileWithSingleCursor(fileWithCursors, '|'); + var file = await new ServerRequestHelper(TestContext, DefaultServer).OpenFile(text); + var completions = await file.RequestAndResolveCompletions(cursor); + + completions.Should().Contain(x => x.Label == "description"); + completions.Should().Contain(x => x.Label == "metadata"); + completions.Should().Contain(x => x.Label == "minLength"); + completions.Should().Contain(x => x.Label == "maxLength"); + + completions.Should().NotContain(x => x.Label == "allowed"); + completions.Should().NotContain(x => x.Label == "sealed"); + completions.Should().NotContain(x => x.Label == "discriminator"); + } + [TestMethod] public async Task ModuleCompletionsShouldNotBeUrlEscaped() { diff --git a/src/Bicep.LangServer/Completions/BicepCompletionProvider.cs b/src/Bicep.LangServer/Completions/BicepCompletionProvider.cs index 14e49431db3..e97db2b723c 100644 --- a/src/Bicep.LangServer/Completions/BicepCompletionProvider.cs +++ b/src/Bicep.LangServer/Completions/BicepCompletionProvider.cs @@ -1020,6 +1020,9 @@ private static IEnumerable GetAccessibleSymbolCompletions(Semant var enclosingDeclarationSymbol = context.EnclosingDeclaration == null ? null : model.GetSymbolInfo(context.EnclosingDeclaration); + var decoratorTargetType = context.EnclosingDecorable is null + ? null + : model.GetDeclaredType(context.EnclosingDecorable); // local function void AddSymbolCompletions(IDictionary result, IEnumerable symbols) @@ -1046,9 +1049,7 @@ IEnumerable GetAccessibleDecoratorFunctionsWithCache(NamespaceTy return result; } - result = GetAccessibleDecoratorFunctions(namespaceType, - context.EnclosingDecorable is null ? null : model.GetDeclaredType(context.EnclosingDecorable), - enclosingDeclarationSymbol); + result = GetAccessibleDecoratorFunctions(namespaceType, context.EnclosingDecorable, decoratorTargetType, enclosingDeclarationSymbol); accessibleDecoratorFunctionsCache.Add(namespaceType, result); return result; @@ -1136,7 +1137,7 @@ IEnumerable GetAccessibleDecoratorFunctionsWithCache(NamespaceTy return completions.Values; } - private static IEnumerable GetAccessibleDecoratorFunctions(NamespaceType namespaceType, TypeSymbol? targetType, Symbol? enclosingDeclarationSymbol) + private static IEnumerable GetAccessibleDecoratorFunctions(NamespaceType namespaceType, SyntaxBase? enclosingDecorable, TypeSymbol? targetType, Symbol? enclosingDeclarationSymbol) { // Local function. IEnumerable GetAccessible(IEnumerable symbols, TypeSymbol targetType, FunctionFlags flags) => @@ -1146,25 +1147,40 @@ IEnumerable GetAccessible(IEnumerable symbols, T var knownDecoratorFunctions = namespaceType.DecoratorResolver.GetKnownDecoratorFunctions().Values; - return enclosingDeclarationSymbol switch + return GetDecoratorCompletionTarget(enclosingDecorable, targetType, enclosingDeclarationSymbol) switch { - MetadataSymbol metadataSymbol => GetAccessible(knownDecoratorFunctions, metadataSymbol.Type, FunctionFlags.MetadataDecorator), - ParameterSymbol parameterSymbol => GetAccessible(knownDecoratorFunctions, parameterSymbol.Type, FunctionFlags.ParameterDecorator), - TypeAliasSymbol declaredTypeSymbol when targetType is not null => GetAccessible(knownDecoratorFunctions, (targetType as TypeType)?.Unwrapped ?? targetType, FunctionFlags.TypeDecorator), - VariableSymbol variableSymbol => GetAccessible(knownDecoratorFunctions, variableSymbol.Type, FunctionFlags.VariableDecorator), - DeclaredFunctionSymbol functionSymbol => GetAccessible(knownDecoratorFunctions, functionSymbol.Type, FunctionFlags.FunctionDecorator), - ResourceSymbol resourceSymbol => GetAccessible(knownDecoratorFunctions, resourceSymbol.Type, FunctionFlags.ResourceDecorator), - ModuleSymbol moduleSymbol => GetAccessible(knownDecoratorFunctions, moduleSymbol.Type, FunctionFlags.ModuleDecorator), - OutputSymbol outputSymbol => GetAccessible(knownDecoratorFunctions, outputSymbol.Type, FunctionFlags.OutputDecorator), + { } decoratorTarget => GetAccessible(knownDecoratorFunctions, decoratorTarget.targetType, decoratorTarget.flags), /* * The decorator is dangling if enclosingDeclarationSymbol is null. Return all decorator factory functions since * we don't know which kind of declaration it will attach to. */ - null => knownDecoratorFunctions, + null when enclosingDeclarationSymbol is null => knownDecoratorFunctions, _ => [] }; } + private static (TypeSymbol targetType, FunctionFlags flags)? GetDecoratorCompletionTarget(SyntaxBase? enclosingDecorable, TypeSymbol? targetType, Symbol? enclosingDeclarationSymbol) + { + if (enclosingDecorable is TypeDeclarationSyntax or ObjectTypePropertySyntax or ObjectTypeAdditionalPropertiesSyntax or TupleTypeItemSyntax && targetType is not null) + { + return ((targetType as TypeType)?.Unwrapped ?? targetType, FunctionFlags.TypeDecorator); + } + + return enclosingDeclarationSymbol switch + { + MetadataSymbol metadataSymbol => (metadataSymbol.Type, FunctionFlags.MetadataDecorator), + ParameterSymbol parameterSymbol => (parameterSymbol.Type, FunctionFlags.ParameterDecorator), + TypeAliasSymbol when targetType is not null => ((targetType as TypeType)?.Unwrapped ?? targetType, FunctionFlags.TypeDecorator), + VariableSymbol variableSymbol => (variableSymbol.Type, FunctionFlags.VariableDecorator), + DeclaredFunctionSymbol functionSymbol => (functionSymbol.Type, FunctionFlags.FunctionDecorator), + ResourceSymbol resourceSymbol => (resourceSymbol.Type, FunctionFlags.ResourceDecorator), + ModuleSymbol moduleSymbol => (moduleSymbol.Type, FunctionFlags.ModuleDecorator), + ExtensionNamespaceSymbol extensionSymbol => (extensionSymbol.DeclaredType, FunctionFlags.ExtensionDecorator), + OutputSymbol outputSymbol => (outputSymbol.Type, FunctionFlags.OutputDecorator), + _ => null, + }; + } + private static IEnumerable GetMemberAccessCompletions(Compilation compilation, BicepCompletionContext context) { if (!context.Kind.HasFlag(BicepCompletionContextKind.MemberAccess) || context.PropertyAccess == null) @@ -1180,8 +1196,7 @@ private static IEnumerable GetMemberAccessCompletions(Compilatio var enclosingDeclarationSymbol = context.EnclosingDeclaration is null ? null : model.GetSymbolInfo(context.EnclosingDeclaration); var decoratorTargetType = context.EnclosingDecorable is null ? null : model.GetDeclaredType(context.EnclosingDecorable); - return GetAccessibleDecoratorFunctions(namespaceType, decoratorTargetType, enclosingDeclarationSymbol) - .Select(symbol => CreateSymbolCompletion(symbol, context.ReplacementRange, model)); + return GetAccessibleDecoratorFunctions(namespaceType, context.EnclosingDecorable, decoratorTargetType, enclosingDeclarationSymbol).Select(symbol => CreateSymbolCompletion(symbol, context.ReplacementRange, model)); } if (declaredType is not null && TypeHelper.TryRemoveNullability(declaredType) is TypeSymbol nonNullable)