From 6df2d639e9ef082d221746ed71cb14eab82bdf1b Mon Sep 17 00:00:00 2001 From: Shenglong Li Date: Sat, 19 Mar 2022 00:34:16 -0700 Subject: [PATCH] Property value flag should flow through ternary and parenthesized expressions (#6231) * Property types should flow through expressions * Update tests --- .../NoHardcodedEnvironmentUrlsRuleTests.cs | 1 - src/Bicep.Decompiler/TemplateConverter.cs | 1 - .../CompletionTests.cs | 105 +++++++++++++++--- .../Completions/BicepCompletionContext.cs | 66 +++++++++++ 4 files changed, 156 insertions(+), 17 deletions(-) diff --git a/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/NoHardcodedEnvironmentUrlsRuleTests.cs b/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/NoHardcodedEnvironmentUrlsRuleTests.cs index 2d8a4586511..1e75d502cbb 100644 --- a/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/NoHardcodedEnvironmentUrlsRuleTests.cs +++ b/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/NoHardcodedEnvironmentUrlsRuleTests.cs @@ -4,7 +4,6 @@ using System.Linq; using Azure.Deployments.Core.Extensions; using Bicep.Core.Analyzers.Linter.Rules; -using Bicep.Core.Configuration; using Microsoft.VisualStudio.TestTools.UnitTesting; // TODO: Test with different configs diff --git a/src/Bicep.Decompiler/TemplateConverter.cs b/src/Bicep.Decompiler/TemplateConverter.cs index bd311d9e873..e9d933c52c5 100644 --- a/src/Bicep.Decompiler/TemplateConverter.cs +++ b/src/Bicep.Decompiler/TemplateConverter.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; diff --git a/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs b/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs index 1e3e096d224..4376ec01a64 100644 --- a/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs @@ -1375,10 +1375,8 @@ public async Task List_functions_accepting_inputs_permit_object_key_completions( "); } - [TestMethod] - public async Task List_functions_accepting_inputs_permit_object_value_completions() - { - var fileWithCursors = @" + [DataTestMethod] + [DataRow(@" resource abc 'Test.Rp/listFuncTests@2020-01-01' existing = { name: 'abc' } @@ -1387,8 +1385,94 @@ public async Task List_functions_accepting_inputs_permit_object_value_completion withInputInputVal: 'hello' optionalLiteralVal: | }) -"; +", @" +resource abc 'Test.Rp/listFuncTests@2020-01-01' existing = { + name: 'abc' +} + +var outTest = abc.listWithInput('2020-01-01', { + withInputInputVal: 'hello' + optionalLiteralVal: 'either'| +}) +")] + [DataRow(@" +resource abc 'Test.Rp/listFuncTests@2020-01-01' existing = { + name: 'abc' +} + +var outTest = abc.listWithInput('2020-01-01', { + withInputInputVal: 'hello' + optionalLiteralVal: (|) +}) +", @" +resource abc 'Test.Rp/listFuncTests@2020-01-01' existing = { + name: 'abc' +} + +var outTest = abc.listWithInput('2020-01-01', { + withInputInputVal: 'hello' + optionalLiteralVal: ('either'|) +}) +")] + [DataRow(@" +resource abc 'Test.Rp/listFuncTests@2020-01-01' existing = { + name: 'abc' +} +var outTest = abc.listWithInput('2020-01-01', { + withInputInputVal: 'hello' + optionalLiteralVal: true ? | : 'or' +}) +", @" +resource abc 'Test.Rp/listFuncTests@2020-01-01' existing = { + name: 'abc' +} + +var outTest = abc.listWithInput('2020-01-01', { + withInputInputVal: 'hello' + optionalLiteralVal: true ? 'either'| : 'or' +}) +")] + [DataRow(@" +resource abc 'Test.Rp/listFuncTests@2020-01-01' existing = { + name: 'abc' +} + +var outTest = abc.listWithInput('2020-01-01', { + withInputInputVal: 'hello' + optionalLiteralVal: true ? 'or' : | +}) +", @" +resource abc 'Test.Rp/listFuncTests@2020-01-01' existing = { + name: 'abc' +} + +var outTest = abc.listWithInput('2020-01-01', { + withInputInputVal: 'hello' + optionalLiteralVal: true ? 'or' : 'either'| +}) +")] + [DataRow(@" +resource abc 'Test.Rp/listFuncTests@2020-01-01' existing = { + name: 'abc' +} + +var outTest = abc.listWithInput('2020-01-01', { + withInputInputVal: 'hello' + optionalLiteralVal: true ? 'or' : (true ? |) +}) +", @" +resource abc 'Test.Rp/listFuncTests@2020-01-01' existing = { + name: 'abc' +} + +var outTest = abc.listWithInput('2020-01-01', { + withInputInputVal: 'hello' + optionalLiteralVal: true ? 'or' : (true ? 'either'|) +}) +")] + public async Task List_functions_accepting_inputs_permit_object_value_completions(string fileWithCursors, string updatedFileWithCursors) + { var (file, cursors) = ParserHelper.GetFileWithCursors(fileWithCursors); var bicepFile = SourceFileFactory.CreateBicepFile(new Uri("file:///main.bicep"), file); using var helper = await LanguageServerHelper.StartServerWithTextAsync(TestContext, file, bicepFile.FileUri, creationOptions: new LanguageServer.Server.CreationOptions(NamespaceProvider: BuiltInTestTypes.Create())); @@ -1398,16 +1482,7 @@ public async Task List_functions_accepting_inputs_permit_object_value_completion completions.Should().Contain(x => x.Label == "'or'"); var updatedFile = ApplyCompletion(bicepFile, completions.Single(x => x.Label == "'either'")); - updatedFile.Should().HaveSourceText(@" -resource abc 'Test.Rp/listFuncTests@2020-01-01' existing = { - name: 'abc' -} - -var outTest = abc.listWithInput('2020-01-01', { - withInputInputVal: 'hello' - optionalLiteralVal: 'either'| -}) -"); + updatedFile.Should().HaveSourceText(updatedFileWithCursors); } [TestMethod] diff --git a/src/Bicep.LangServer/Completions/BicepCompletionContext.cs b/src/Bicep.LangServer/Completions/BicepCompletionContext.cs index 7fa63763ce0..0252634322b 100644 --- a/src/Bicep.LangServer/Completions/BicepCompletionContext.cs +++ b/src/Bicep.LangServer/Completions/BicepCompletionContext.cs @@ -176,6 +176,12 @@ public static BicepCompletionContext Create(IFeatureProvider featureProvider, Co // previous processing hasn't identified a completion context kind // check if we're inside an expression kind |= ConvertFlag(IsInnerExpressionContext(matchingNodes, offset), BicepCompletionContextKind.Expression); + + if (kind.HasFlag(BicepCompletionContextKind.Expression) && + PropertyTypeShouldFlowThrough(matchingNodes, propertyInfo, offset)) + { + kind |= BicepCompletionContextKind.PropertyValue; + } } return new BicepCompletionContext( @@ -546,6 +552,37 @@ private static BicepCompletionContextKind GetPropertyValueFlags(List return BicepCompletionContextKind.None; } + private static bool PropertyTypeShouldFlowThrough(List matchingNodes, (ObjectPropertySyntax? node, int index) propertyInfo, int offset) + { + if (propertyInfo.node is null) + { + return false; + } + + // Property types should flow through parenthesized and ternary expressions. For examples: + // { + // key: (( | )) + // } + // { + // key: conditionA ? | : false + // } + // { + // key: conditionA ? (conditionB ? true : |) : false + // } + if ((SyntaxMatcher.IsTailMatch(matchingNodes) || + SyntaxMatcher.IsTailMatch(matchingNodes) || + SyntaxMatcher.IsTailMatch(matchingNodes) || + SyntaxMatcher.IsTailMatch(matchingNodes) || + SyntaxMatcher.IsTailMatch(matchingNodes, (parenthesizedExpression, _) => + parenthesizedExpression.Expression is SkippedTriviaSyntax)) && + matchingNodes.Skip(propertyInfo.index + 1).SkipLast(1).All(node => node is TernaryOperationSyntax or ParenthesizedExpressionSyntax)) + { + return true; + } + + return false; + } + private static bool IsArrayItemContext(List matchingNodes, (ArraySyntax? node, int index) arrayInfo, int offset) { if (arrayInfo.node == null) @@ -762,6 +799,35 @@ TokenType.StringMiddlePiece when IsOffsetImmediatlyAfterNode(offset, token) => f return false; } + // var foo = true ?|: + // var foo = true ?| : + // var foo = true ? |: + // var foo = true ? | : + var isInEmptyTrueExpression = SyntaxMatcher.IsTailMatch( + matchingNodes, + ternaryOperation => + ternaryOperation.Question.GetPosition() <= offset && + ternaryOperation.Colon.GetPosition() >= offset && + ternaryOperation.TrueExpression is SkippedTriviaSyntax); + + if (isInEmptyTrueExpression) + { + return true; + } + + // var foo = true ? : | + var isInEmptyFalseExpression = SyntaxMatcher.IsTailMatch( + matchingNodes, + ternaryOperation => + ternaryOperation.Colon.GetPosition() <= offset && + ternaryOperation.FalseExpression.GetPosition() >= offset && + ternaryOperation.FalseExpression is SkippedTriviaSyntax); + + if (isInEmptyFalseExpression) + { + return true; + } + // It does not make sense to insert expressions at the cursor positions shown in the comments below. return !( //║{ ║{ ║|{| ║{ ║{