public async Task Overlapping_tokens_are_not_returned(DataSet dataSet) { var uri = DocumentUri.From($"/{dataSet.Name}"); var bicepFile = SourceFileFactory.CreateBicepFile(uri.ToUri(), dataSet.Bicep); using var helper = await LanguageServerHelper.StartServerWithTextAsync(TestContext, dataSet.Bicep, uri); var client = helper.Client; var semanticTokens = await client.TextDocument.RequestSemanticTokens(new SemanticTokensParams { TextDocument = new TextDocumentIdentifier(uri), }); var tokenSpans = CalculateTokenTextSpans(bicepFile.LineStarts, semanticTokens !.Data).ToArray(); for (var i = 1; i < tokenSpans.Length; i++) { var currentSpan = tokenSpans[i]; var prevSpan = tokenSpans[i - 1]; if (TextSpan.AreOverlapping(prevSpan, currentSpan)) { using (new AssertionScope() .WithAnnotations(bicepFile, "overlapping tokens", new[] { prevSpan, currentSpan }, _ => "here", x => x.ToRange(bicepFile.LineStarts))) { TextSpan.AreOverlapping(prevSpan, currentSpan).Should().BeFalse(); } } } }
public async Task GoToDefinitionRequestOnUnsupportedOrInvalidSyntaxNodeShouldReturnEmptyResponse(DataSet dataSet) { var uri = DocumentUri.From($"/{dataSet.Name}"); using var helper = await LanguageServerHelper.StartServerWithTextAsync(this.TestContext, dataSet.Bicep, uri); var client = helper.Client; var(compilation, _, _) = await dataSet.SetupPrerequisitesAndCreateCompilation(TestContext); var symbolTable = compilation.ReconstructSymbolTable(); var lineStarts = compilation.SourceFileGrouping.EntryPoint.LineStarts; var undeclaredSymbolBindings = symbolTable.Where(pair => pair.Value is not DeclaredSymbol and not PropertySymbol); foreach (var(syntax, _) in undeclaredSymbolBindings) { var response = await client.RequestDefinition(new DefinitionParams { TextDocument = new TextDocumentIdentifier(uri), Position = IntegrationTestHelper.GetPosition(lineStarts, syntax) }); using (new AssertionScope().WithVisualCursor(compilation.SourceFileGrouping.EntryPoint, syntax.Span)) { // go to definition on a symbol that isn't declared by the user (like error or function symbol) // should produce an empty response response.Should().BeEmpty(); } } }
public async Task VerifyDisableNextLineCodeActionInvocationFiresTelemetryEvent() { var bicepFileContents = @"param storageAccount string = 'testStorageAccount'"; var bicepFilePath = FileHelper.SaveResultFile(TestContext, "main.bicep", bicepFileContents); var documentUri = DocumentUri.FromFileSystemPath(bicepFilePath); var uri = documentUri.ToUri(); var files = new Dictionary <Uri, string> { [uri] = bicepFileContents, }; var compilation = new Compilation(BicepTestConstants.NamespaceProvider, SourceFileGroupingFactory.CreateForFiles(files, uri, BicepTestConstants.FileResolver, BicepTestConstants.BuiltInConfiguration), BicepTestConstants.BuiltInConfiguration); var diagnostics = compilation.GetEntrypointSemanticModel().GetAllDiagnostics(); var telemetryReceived = new TaskCompletionSource <BicepTelemetryEvent>(); using var helper = await LanguageServerHelper.StartServerWithClientConnectionAsync( TestContext, options => { options.OnTelemetryEvent <BicepTelemetryEvent>(telemetry => telemetryReceived.SetResult(telemetry)); }, new Server.CreationOptions(NamespaceProvider: BicepTestConstants.NamespaceProvider, FileResolver: new InMemoryFileResolver(files))); var client = helper.Client; client.TextDocument.DidOpenTextDocument(TextDocumentParamHelper.CreateDidOpenDocumentParams(documentUri, files[uri], 1)); var lineStarts = compilation.SourceFileGrouping.EntryPoint.LineStarts; var codeActions = await client.RequestCodeAction(new CodeActionParams { TextDocument = new TextDocumentIdentifier(documentUri), Range = diagnostics.First().ToRange(lineStarts) }); var disableNextLineCodeAction = codeActions.First(x => x.CodeAction !.Title == "Disable no-unused-params").CodeAction; _ = await client !.ResolveCodeAction(disableNextLineCodeAction !); Command?command = disableNextLineCodeAction !.Command; JArray? arguments = command !.Arguments; await client.Workspace.ExecuteCommand(command); var bicepTelemetryEvent = await IntegrationTestHelper.WithTimeoutAsync(telemetryReceived.Task); IDictionary <string, string> properties = new Dictionary <string, string> { { "code", "no-unused-params" } }; bicepTelemetryEvent.EventName.Should().Be(TelemetryConstants.EventNames.DisableNextLineDiagnostics); bicepTelemetryEvent.Properties.Should().Equal(properties); }
public async Task NonExistentUriShouldProvideNoSignatureHelp() { using var helper = await LanguageServerHelper.StartServerWithTextAsync(this.TestContext, string.Empty, DocumentUri.From("/fake.bicep")); var client = helper.Client; var signatureHelp = await RequestSignatureHelp(client, new Position(0, 0), DocumentUri.From("/fake2.bicep")); signatureHelp.Should().BeNull(); }
public async Task GoToDefinitionRequestOnValidSymbolReferenceShouldReturnLocationOfDeclaredSymbol(DataSet dataSet) { var(compilation, _, fileUri) = await dataSet.SetupPrerequisitesAndCreateCompilation(TestContext); var uri = DocumentUri.From(fileUri); using var helper = await LanguageServerHelper.StartServerWithTextAsync(this.TestContext, dataSet.Bicep, uri); var client = helper.Client; var symbolTable = compilation.ReconstructSymbolTable(); var lineStarts = compilation.SourceFileGrouping.EntryPoint.LineStarts; // filter out symbols that don't have locations as well as locals with invalid identifiers // (locals are special because their full span is the same as the identifier span, // which makes it impossible to go to definition on a local with invalid identifiers) var declaredSymbolBindings = symbolTable .Where(pair => pair.Value is DeclaredSymbol && (pair.Value is not LocalVariableSymbol local || local.NameSyntax.IsValid)) .Select(pair => new KeyValuePair <SyntaxBase, DeclaredSymbol>(pair.Key, (DeclaredSymbol)pair.Value)); foreach (var(syntax, symbol) in declaredSymbolBindings) { var response = await client.RequestDefinition(new DefinitionParams { TextDocument = new TextDocumentIdentifier(uri), Position = IntegrationTestHelper.GetPosition(lineStarts, syntax) }); var link = ValidateDefinitionResponse(response); // document should match the requested document link.TargetUri.Should().Be(uri); // target range should be the whole span of the symbol link.TargetRange.Should().Be(symbol.DeclaringSyntax.Span.ToRange(lineStarts)); // selection range should be the span of the identifier of the symbol link.TargetSelectionRange.Should().Be(symbol.NameSyntax.Span.ToRange(lineStarts)); if (syntax is ParameterDeclarationSyntax parameterSyntax) { // we only underline the key of the param declaration syntax link.OriginSelectionRange.Should().Be(parameterSyntax.Name.ToRange(lineStarts)); } else if (syntax is ITopLevelNamedDeclarationSyntax namedSyntax) { // Instead of underlining everything, we only underline the resource name link.OriginSelectionRange.Should().Be(namedSyntax.Name.ToRange(lineStarts)); } else { // origin selection range should be the span of the syntax node that references the symbol link.OriginSelectionRange.Should().Be(syntax.ToRange(lineStarts)); } } }
public async Task RequestDocumentFormattingShouldReturnFullRangeTextEdit() { var documentUri = DocumentUri.From("/template.bicep"); var diagnosticsReceived = new TaskCompletionSource <PublishDiagnosticsParams>(); using var helper = await LanguageServerHelper.StartServerWithClientConnectionAsync(this.TestContext, options => { options.OnPublishDiagnostics(diagnostics => { diagnosticsReceived.SetResult(diagnostics); }); }); var client = helper.Client; // client opens the document client.TextDocument.DidOpenTextDocument(TextDocumentParamHelper.CreateDidOpenDocumentParams(documentUri, @" param myParam object // line comment var myVar1 = 1 + mod(1, 2) var myVar2 = myParam.foo[1] resource myResource 'myRP/provider@2020-11-01' = { /* block comment */ name: 'test' } module myModule './module.bicep' = { name: 'test' } output myOutput string = 'value'", 0)); // client requests symbols var textEditContainer = await client.TextDocument.RequestDocumentFormatting(new DocumentFormattingParams { TextDocument = new TextDocumentIdentifier { Uri = documentUri }, Options = new FormattingOptions { TabSize = 4, InsertSpaces = true, InsertFinalNewline = true, } }); textEditContainer.Should().NotBeNull(); textEditContainer.Should().HaveCount(1); }
private static async Task RunDefinitionScenarioTest(TestContext testContext, string fileWithCursors, Action <List <LocationOrLocationLinks> > assertAction) { var(file, cursors) = ParserHelper.GetFileWithCursors(fileWithCursors); var bicepFile = SourceFileFactory.CreateBicepFile(new Uri("file:///path/to/main.bicep"), file); using var helper = await LanguageServerHelper.StartServerWithTextAsync(testContext, file, bicepFile.FileUri, creationOptions : new LanguageServer.Server.CreationOptions(NamespaceProvider: BuiltInTestTypes.Create())); var client = helper.Client; var results = await RequestDefinitions(client, bicepFile, cursors); assertAction(results); }
private async Task VerifyBicepFileOpenTelemetry( string?bicepConfigFileContents, string mainBicepFileContents, string referencedBicepFileContents, IDictionary <string, string>?linterRuleStateOnBicepFileOpenTelemetryEventProperties, IDictionary <string, string> bicepFileOpenTelemetryEventProperties) { var testOutputPath = Path.Combine(TestContext.ResultsDirectory, Guid.NewGuid().ToString()); if (bicepConfigFileContents is not null) { FileHelper.SaveResultFile(TestContext, "bicepconfig.json", bicepConfigFileContents, testOutputPath); } var fileSystemDict = new Dictionary <Uri, string>(); var mainBicepFilePath = FileHelper.SaveResultFile(TestContext, "main.bicep", mainBicepFileContents, testOutputPath); var mainUri = DocumentUri.FromFileSystemPath(mainBicepFilePath); fileSystemDict[mainUri.ToUri()] = mainBicepFileContents; var referencedBicepFilePath = FileHelper.SaveResultFile(TestContext, "groups.bicep", referencedBicepFileContents, testOutputPath); var moduleUri = DocumentUri.FromFileSystemPath(referencedBicepFilePath); fileSystemDict[moduleUri.ToUri()] = referencedBicepFileContents; var telemetryEventsListener = new MultipleMessageListener <BicepTelemetryEvent>(); using var helper = await LanguageServerHelper.StartServerWithClientConnectionAsync( TestContext, options => { options.OnTelemetryEvent <BicepTelemetryEvent>(telemetry => telemetryEventsListener.AddMessage(telemetry)); }, creationOptions : new Server.CreationOptions(FileResolver: new InMemoryFileResolver(fileSystemDict))); var client = helper.Client; client.TextDocument.DidOpenTextDocument(TextDocumentParamHelper.CreateDidOpenDocumentParams(mainUri, fileSystemDict[mainUri.ToUri()], 1)); var bicepTelemetryEvent = await telemetryEventsListener.WaitNext(); if (linterRuleStateOnBicepFileOpenTelemetryEventProperties is not null) { bicepTelemetryEvent.EventName.Should().Be(TelemetryConstants.EventNames.LinterRuleStateOnBicepFileOpen); bicepTelemetryEvent.Properties.Should().Contain(linterRuleStateOnBicepFileOpenTelemetryEventProperties); } bicepTelemetryEvent = await telemetryEventsListener.WaitNext(); bicepTelemetryEvent.EventName.Should().Be(TelemetryConstants.EventNames.BicepFileOpen); bicepTelemetryEvent.Properties.Should().Contain(bicepFileOpenTelemetryEventProperties); }
private async Task <LanguageServerHelper> StartServerWithClientConnectionAsync(MultipleMessageListener <PublishDiagnosticsParams> diagsListener) { var fileSystemDict = new Dictionary <Uri, string>(); var fileResolver = new InMemoryFileResolver(fileSystemDict); var serverOptions = new Server.CreationOptions(FileResolver: fileResolver); return(await LanguageServerHelper.StartServerWithClientConnectionAsync( TestContext, options => { options.OnPublishDiagnostics(diags => diagsListener.AddMessage(diags)); }, serverOptions)); }
public async Task GoToDefinitionOnUnboundSyntaxNodeShouldReturnEmptyResponse(DataSet dataSet) { // local function bool IsUnboundNode(IDictionary <SyntaxBase, Symbol> dictionary, SyntaxBase syntax) => dictionary.ContainsKey(syntax) == false && !(syntax is Token); var(compilation, _, fileUri) = await dataSet.SetupPrerequisitesAndCreateCompilation(TestContext); var uri = DocumentUri.From(fileUri); using var helper = await LanguageServerHelper.StartServerWithTextAsync(this.TestContext, dataSet.Bicep, uri); var client = helper.Client; var symbolTable = compilation.ReconstructSymbolTable(); var lineStarts = compilation.SourceFileGrouping.EntryPoint.LineStarts; var unboundNodes = SyntaxAggregator.Aggregate( source: compilation.SourceFileGrouping.EntryPoint.ProgramSyntax, seed: new List <SyntaxBase>(), function: (accumulated, syntax) => { if (IsUnboundNode(symbolTable, syntax) && !(syntax is ProgramSyntax)) { // only collect unbound nodes non-program nodes accumulated.Add(syntax); } return(accumulated); }, resultSelector: accumulated => accumulated, // visit children only if current node is not bound continuationFunction: (accumulated, syntax) => IsUnboundNode(symbolTable, syntax)); for (int i = 0; i < unboundNodes.Count(); i++) { var syntax = unboundNodes[i]; if (ValidUnboundNode(unboundNodes, i)) { continue; } var response = await client.RequestDefinition(new DefinitionParams { TextDocument = new TextDocumentIdentifier(uri), Position = IntegrationTestHelper.GetPosition(lineStarts, syntax) }); // go to definition on a syntax node that isn't bound to a symbol should produce an empty response response.Should().BeEmpty(); } }
public async Task FindReferencesWithoutDeclarationsShouldProduceCorrectResults(DataSet dataSet) { var(compilation, _, fileUri) = await dataSet.SetupPrerequisitesAndCreateCompilation(TestContext); var uri = DocumentUri.From(fileUri); using var helper = await LanguageServerHelper.StartServerWithTextAsync(this.TestContext, dataSet.Bicep, uri); var client = helper.Client; var symbolTable = compilation.ReconstructSymbolTable(); var lineStarts = compilation.SourceFileGrouping.EntryPoint.LineStarts; // filter out bind failures and locals with invalid identifiers // (locals are special because their span is equal to their identifier span) var filteredSymbolTable = symbolTable.Where(pair => pair.Value.Kind != SymbolKind.Error && (pair.Value is not LocalVariableSymbol local || local.NameSyntax.IsValid)); // TODO: Implement for PropertySymbol filteredSymbolTable = filteredSymbolTable.Where(pair => pair.Value is not PropertySymbol); var symbolToSyntaxLookup = filteredSymbolTable.ToLookup(pair => pair.Value, pair => pair.Key); foreach (var(syntax, symbol) in filteredSymbolTable) { var locations = await client.RequestReferences(new ReferenceParams { TextDocument = new TextDocumentIdentifier(uri), Context = new ReferenceContext { IncludeDeclaration = false }, Position = IntegrationTestHelper.GetPosition(lineStarts, syntax) }); // all URIs should be the same in the results locations.Select(r => r.Uri).Should().AllBeEquivalentTo(uri); // exclude declarations when calculating expected ranges var expectedRanges = symbolToSyntaxLookup[symbol] .Where(node => !(node is INamedDeclarationSyntax)) .Select(node => PositionHelper.GetNameRange(lineStarts, node)); using (new AssertionScope() .WithAnnotations(compilation.SourceFileGrouping.EntryPoint, "expected", expectedRanges, _ => "here", x => x) .WithAnnotations(compilation.SourceFileGrouping.EntryPoint, "actual", locations, _ => "here", x => x.Range)) { // ranges should match what we got from our own symbol table locations.Select(l => l.Range).Should().BeEquivalentTo(expectedRanges); } } }
private async Task <BicepTelemetryEvent> ResolveCompletionAsync(string text, string prefix, Position position) { var fileSystemDict = new Dictionary <Uri, string>(); var telemetryEventsListener = new MultipleMessageListener <BicepTelemetryEvent>(); using var helper = await LanguageServerHelper.StartServerWithClientConnectionAsync( TestContext, options => { options.OnTelemetryEvent <BicepTelemetryEvent>(telemetry => telemetryEventsListener.AddMessage(telemetry)); }, new Server.CreationOptions(NamespaceProvider: BicepTestConstants.NamespaceProvider, FileResolver: new InMemoryFileResolver(fileSystemDict))); var client = helper.Client; var mainUri = DocumentUri.FromFileSystemPath("/main.bicep"); fileSystemDict[mainUri.ToUri()] = text; client.TextDocument.DidOpenTextDocument(TextDocumentParamHelper.CreateDidOpenDocumentParams(mainUri, fileSystemDict[mainUri.ToUri()], 1)); var bicepTelemetryEvent = await telemetryEventsListener.WaitNext(); bicepTelemetryEvent.EventName.Should().Be(TelemetryConstants.EventNames.LinterRuleStateOnBicepFileOpen); bicepTelemetryEvent = await telemetryEventsListener.WaitNext(); bicepTelemetryEvent.EventName.Should().Be(TelemetryConstants.EventNames.BicepFileOpen); var completions = await client.RequestCompletion(new CompletionParams { TextDocument = new TextDocumentIdentifier(mainUri), Position = position, }); CompletionItem completionItem = completions.Where(x => x.Kind == CompletionItemKind.Snippet && x.Label == prefix).First(); Command? command = completionItem.Command; JArray? arguments = command !.Arguments; BicepTelemetryEvent?telemetryEvent = arguments !.First().ToObject <BicepTelemetryEvent>(); await client.ResolveCompletion(completionItem); await client.Workspace.ExecuteCommand(command); return(await telemetryEventsListener.WaitNext()); }
public async Task RequestingCodeActionWithFixableDiagnosticsShouldProduceQuickFixes(DataSet dataSet) { var(compilation, _, fileUri) = await dataSet.SetupPrerequisitesAndCreateCompilation(this.TestContext); var uri = DocumentUri.From(fileUri); // start language server using var helper = await LanguageServerHelper.StartServerWithTextAsync(this.TestContext, dataSet.Bicep, uri, creationOptions : new LanguageServer.Server.CreationOptions(FileResolver: new FileResolver())); var client = helper.Client; // construct a parallel compilation var lineStarts = compilation.SourceFileGrouping.EntryPoint.LineStarts; var fixables = compilation.GetEntrypointSemanticModel().GetAllDiagnostics().OfType <IFixable>(); foreach (IFixable fixable in fixables) { foreach (var span in GetOverlappingSpans(fixable.Span)) { CommandOrCodeActionContainer?quickFixes = await client.RequestCodeAction(new CodeActionParams { TextDocument = new TextDocumentIdentifier(uri), Range = span.ToRange(lineStarts) }); // Assert. quickFixes.Should().NotBeNull(); var quickFixList = quickFixes.Where(x => x.CodeAction?.Kind == CodeActionKind.QuickFix).ToList(); var bicepFixList = fixable.Fixes.ToList(); quickFixList.Should().HaveSameCount(bicepFixList); for (int i = 0; i < quickFixList.Count; i++) { var quickFix = quickFixList[i]; var bicepFix = bicepFixList[i]; quickFix.IsCodeAction.Should().BeTrue(); quickFix.CodeAction !.Kind.Should().Be(CodeActionKind.QuickFix); quickFix.CodeAction.Title.Should().Be(bicepFix.Description); quickFix.CodeAction.Edit !.Changes.Should().ContainKey(uri); var textEditList = quickFix.CodeAction.Edit.Changes ![uri].ToList();
public async Task ShouldProvideSignatureHelpBetweenFunctionParentheses(DataSet dataSet) { var(compilation, _, fileUri) = await dataSet.SetupPrerequisitesAndCreateCompilation(TestContext); var uri = DocumentUri.From(fileUri); using var helper = await LanguageServerHelper.StartServerWithTextAsync(this.TestContext, dataSet.Bicep, uri); var client = helper.Client; var symbolTable = compilation.ReconstructSymbolTable(); var tree = compilation.SourceFileGrouping.EntryPoint; var functionCalls = SyntaxAggregator.Aggregate( tree.ProgramSyntax, new List <FunctionCallSyntaxBase>(), (accumulated, current) => { if (current is FunctionCallSyntaxBase functionCallBase) { accumulated.Add(functionCallBase); } return(accumulated); }, accumulated => accumulated); foreach (FunctionCallSyntaxBase functionCall in functionCalls) { var expectDecorator = compilation.GetEntrypointSemanticModel().Binder.GetParent(functionCall) is DecoratorSyntax; var symbol = compilation.GetEntrypointSemanticModel().GetSymbolInfo(functionCall); // if the cursor is present immediate after the function argument opening paren, // the signature help can only show the signature of the enclosing function var startOffset = functionCall.OpenParen.GetEndPosition(); await ValidateOffset(client, uri, tree, startOffset, symbol as FunctionSymbol, expectDecorator); // if the cursor is present immediately before the function argument closing paren, // the signature help can only show the signature of the enclosing function var endOffset = functionCall.CloseParen.Span.Position; await ValidateOffset(client, uri, tree, endOffset, symbol as FunctionSymbol, expectDecorator); } }
public async Task HighlightsShouldShowAllReferencesOfTheSymbol(DataSet dataSet) { var(compilation, _, fileUri) = await dataSet.SetupPrerequisitesAndCreateCompilation(TestContext); var uri = DocumentUri.From(fileUri); using var helper = await LanguageServerHelper.StartServerWithTextAsync(this.TestContext, dataSet.Bicep, uri); var client = helper.Client; var symbolTable = compilation.ReconstructSymbolTable(); var lineStarts = compilation.SourceFileGrouping.EntryPoint.LineStarts; // filter out binding failures and locals with invalid identifiers // (locals are special because their full span is the same as the identifier span, // which makes it impossible to highlight locals with invalid identifiers) var filteredSymbolTable = symbolTable.Where(pair => pair.Value.Kind != SymbolKind.Error && (pair.Value is not LocalVariableSymbol local || local.NameSyntax.IsValid)); // TODO: Implement for PropertySymbol filteredSymbolTable = filteredSymbolTable.Where(pair => pair.Value is not PropertySymbol); var symbolToSyntaxLookup = filteredSymbolTable.ToLookup(pair => pair.Value, pair => pair.Key); foreach (var(syntax, symbol) in filteredSymbolTable) { var highlights = await client.RequestDocumentHighlight(new DocumentHighlightParams { TextDocument = new TextDocumentIdentifier(uri), Position = IntegrationTestHelper.GetPosition(lineStarts, syntax) }); // calculate expected highlights var expectedHighlights = symbolToSyntaxLookup[symbol].Select(node => CreateExpectedHighlight(lineStarts, node)); using (new AssertionScope() .WithAnnotations(compilation.SourceFileGrouping.EntryPoint, "expected", expectedHighlights, _ => "here", x => x.Range) .WithAnnotations(compilation.SourceFileGrouping.EntryPoint, "actual", highlights, _ => "here", x => x.Range)) { // ranges should match what we got from our own symbol table highlights.Should().BeEquivalentTo(expectedHighlights); } } }
public async Task RenamingIdentifierAccessOrDeclarationShouldRenameDeclarationAndAllReferences(DataSet dataSet) { var(compilation, _, fileUri) = await dataSet.SetupPrerequisitesAndCreateCompilation(TestContext); var uri = DocumentUri.From(fileUri); using var helper = await LanguageServerHelper.StartServerWithTextAsync(this.TestContext, dataSet.Bicep, uri); var client = helper.Client; var symbolTable = compilation.ReconstructSymbolTable(); var lineStarts = compilation.SourceFileGrouping.EntryPoint.LineStarts; var symbolToSyntaxLookup = symbolTable .Where(pair => pair.Value.Kind != SymbolKind.Error) .ToLookup(pair => pair.Value, pair => pair.Key); var validVariableAccessPairs = symbolTable .Where(pair => (pair.Key is VariableAccessSyntax || pair.Key is ResourceAccessSyntax || pair.Key is ITopLevelNamedDeclarationSyntax) && pair.Value.Kind != SymbolKind.Error && pair.Value.Kind != SymbolKind.Function && pair.Value.Kind != SymbolKind.Namespace // symbols whose identifiers have parse errors will have a name like <error> or <missing> && pair.Value.Name.Contains("<") == false); const string expectedNewText = "NewIdentifier"; foreach (var(syntax, symbol) in validVariableAccessPairs) { var edit = await client.RequestRename(new RenameParams { NewName = expectedNewText, TextDocument = new TextDocumentIdentifier(uri), Position = IntegrationTestHelper.GetPosition(lineStarts, syntax) }); edit.Should().NotBeNull(); edit !.DocumentChanges.Should().BeNullOrEmpty(); edit.Changes.Should().NotBeNull(); edit.Changes.Should().HaveCount(1); edit.Changes.Should().ContainKey(uri); var textEdits = edit.Changes ![uri];
public async Task Build_command_should_generate_template_with_symbolic_names_if_enabled() { var diagnosticsListener = new MultipleMessageListener <PublishDiagnosticsParams>(); var features = BicepTestConstants.CreateFeaturesProvider( TestContext, symbolicNameCodegenEnabled: true, assemblyFileVersion: BicepTestConstants.DevAssemblyFileVersion); using var helper = await LanguageServerHelper.StartServerWithClientConnectionAsync( this.TestContext, options => options.OnPublishDiagnostics(diagnosticsParams => diagnosticsListener.AddMessage(diagnosticsParams)), new LanguageServer.Server.CreationOptions( NamespaceProvider: BuiltInTestTypes.Create(), Features: features)); var client = helper.Client; var outputDirectory = FileHelper.SaveEmbeddedResourcesWithPathPrefix( TestContext, typeof(ExamplesTests).Assembly, "Bicep.Core.Samples/Resources_CRLF"); var bicepFilePath = Path.Combine(outputDirectory, "main.bicep"); var expectedJson = File.ReadAllText(Path.Combine(outputDirectory, "main.symbolicnames.json")); client.TextDocument.DidOpenTextDocument(TextDocumentParamHelper.CreateDidOpenDocumentParamsFromFile(bicepFilePath, 1)); await diagnosticsListener.WaitNext(); await client.Workspace.ExecuteCommand(new Command { Name = "build", Arguments = new JArray { bicepFilePath, } }); var buildCommandOutput = File.ReadAllText(Path.ChangeExtension(bicepFilePath, ".json")); buildCommandOutput.Should().BeEquivalentToIgnoringNewlines(expectedJson); }
public async Task NonFunctionCallSyntaxShouldProvideNoSignatureHelp(DataSet dataSet) { var(compilation, _, fileUri) = await dataSet.SetupPrerequisitesAndCreateCompilation(TestContext); var uri = DocumentUri.From(fileUri); using var helper = await LanguageServerHelper.StartServerWithTextAsync(this.TestContext, dataSet.Bicep, uri); var client = helper.Client; var bicepFile = compilation.SourceFileGrouping.EntryPoint; var nonFunctions = SyntaxAggregator.Aggregate( bicepFile.ProgramSyntax, new List <SyntaxBase>(), (accumulated, current) => { if (current is not FunctionCallSyntaxBase) { accumulated.Add(current); } return(accumulated); }, accumulated => accumulated, // requesting signature help on non-function nodes that are placed inside function call nodes will produce signature help // since we don't want that, stop the visitor from visiting inner nodes when a function call is encountered (accumulated, current) => current is not FunctionCallSyntaxBase); foreach (var nonFunction in nonFunctions) { using (new AssertionScope().WithVisualCursor(bicepFile, nonFunction.Span.ToZeroLengthSpan())) { var position = PositionHelper.GetPosition(bicepFile.LineStarts, nonFunction.Span.Position); var signatureHelp = await RequestSignatureHelp(client, position, uri); signatureHelp.Should().BeNull(); } } }
private async Task <BicepTelemetryEvent> ResolveCompletionAsync(string text, string prefix, Position position) { var fileSystemDict = new Dictionary <Uri, string>(); var telemetryReceived = new TaskCompletionSource <BicepTelemetryEvent>(); using var helper = await LanguageServerHelper.StartServerWithClientConnectionAsync( TestContext, options => { options.OnTelemetryEvent <BicepTelemetryEvent>(telemetry => telemetryReceived.SetResult(telemetry)); }, new LanguageServer.Server.CreationOptions(NamespaceProvider: BicepTestConstants.NamespaceProvider, FileResolver: new InMemoryFileResolver(fileSystemDict))); var client = helper.Client; var mainUri = DocumentUri.FromFileSystemPath("/main.bicep"); fileSystemDict[mainUri.ToUri()] = text; client.TextDocument.DidOpenTextDocument(TextDocumentParamHelper.CreateDidOpenDocumentParams(mainUri, fileSystemDict[mainUri.ToUri()], 1)); var completions = await client.RequestCompletion(new CompletionParams { TextDocument = new TextDocumentIdentifier(mainUri), Position = position, }); CompletionItem completionItem = completions.Where(x => x.Kind == CompletionItemKind.Snippet && x.Label == prefix).First(); Command? command = completionItem.Command; JArray? arguments = command !.Arguments; BicepTelemetryEvent?telemetryEvent = arguments !.First().ToObject <BicepTelemetryEvent>(); await client.ResolveCompletion(completionItem); await client.Workspace.ExecuteCommand(command); return(await IntegrationTestHelper.WithTimeoutAsync(telemetryReceived.Task)); }
public static async Task <LanguageServerHelper> StartServerWithTextAsync(TestContext testContext, string text, DocumentUri documentUri, Action <LanguageClientOptions>?onClientOptions = null, Server.CreationOptions?creationOptions = null) { var diagnosticsPublished = new TaskCompletionSource <PublishDiagnosticsParams>(); creationOptions ??= new Server.CreationOptions(); creationOptions = creationOptions with { FileResolver = creationOptions.FileResolver ?? new InMemoryFileResolver(new Dictionary <Uri, string> { [documentUri.ToUri()] = text, }), ModuleRestoreScheduler = creationOptions.ModuleRestoreScheduler ?? BicepTestConstants.ModuleRestoreScheduler }; var helper = await LanguageServerHelper.StartServerWithClientConnectionAsync( testContext, options => { onClientOptions?.Invoke(options); options.OnPublishDiagnostics(p => { testContext.WriteLine($"Received {p.Diagnostics.Count()} diagnostic(s)."); diagnosticsPublished.SetResult(p); }); }, creationOptions); // send open document notification helper.Client.DidOpenTextDocument(TextDocumentParamHelper.CreateDidOpenDocumentParams(documentUri, text, 0)); testContext.WriteLine($"Opened file {documentUri}."); // notifications don't produce responses, // but our server should send us diagnostics when it receives the notification await IntegrationTestHelper.WithTimeoutAsync(diagnosticsPublished.Task); return(helper); }
public static async Task <MultiFileLanguageServerHelper> StartLanguageServer(TestContext testContext, Server.CreationOptions?creationOptions = null) { var notificationRouter = new ConcurrentDictionary <DocumentUri, TaskCompletionSource <PublishDiagnosticsParams> >(); var helper = await LanguageServerHelper.StartServerWithClientConnectionAsync( testContext, onClientOptions : options => { options.OnPublishDiagnostics(p => { testContext.WriteLine($"Received {p.Diagnostics.Count()} diagnostic(s)."); if (notificationRouter.TryGetValue(p.Uri, out var completionSource)) { completionSource.SetResult(p); return; } throw new AssertFailedException($"Task completion source was not registered for document uri '{p.Uri}'."); }); }, creationOptions : creationOptions); return(new(helper.Server, helper.Client, notificationRouter)); }
public async Task HoveringOverSymbolReferencesAndDeclarationsShouldProduceHovers(DataSet dataSet) { var(compilation, _, fileUri) = await dataSet.SetupPrerequisitesAndCreateCompilation(TestContext); var uri = DocumentUri.From(fileUri); using var helper = await LanguageServerHelper.StartServerWithTextAsync(this.TestContext, dataSet.Bicep, uri, creationOptions : new LanguageServer.Server.CreationOptions(NamespaceProvider: BicepTestConstants.NamespaceProvider, FileResolver: BicepTestConstants.FileResolver)); var client = helper.Client; var symbolTable = compilation.ReconstructSymbolTable(); var lineStarts = compilation.SourceFileGrouping.EntryPoint.LineStarts; var symbolReferences = SyntaxAggregator.Aggregate( compilation.SourceFileGrouping.EntryPoint.ProgramSyntax, new List <SyntaxBase>(), (accumulated, node) => { if (node is ISymbolReference || node is ITopLevelNamedDeclarationSyntax) { accumulated.Add(node); } return(accumulated); }, accumulated => accumulated); foreach (var symbolReference in symbolReferences) { // by default, request a hover on the first character of the syntax, but for certain syntaxes, this doesn't make sense. // for example on an instance function call 'az.resourceGroup()', it only makes sense to request a hover on the 3rd character. var nodeForHover = symbolReference switch { ITopLevelDeclarationSyntax d => d.Keyword, ResourceAccessSyntax r => r.ResourceName, FunctionCallSyntaxBase f => f.Name, _ => symbolReference, }; var hover = await client.RequestHover(new HoverParams { TextDocument = new TextDocumentIdentifier(uri), Position = TextCoordinateConverter.GetPosition(lineStarts, nodeForHover.Span.Position) }); // fancy method to give us some annotated source code to look at if any assertions fail :) using (new AssertionScope().WithVisualCursor(compilation.SourceFileGrouping.EntryPoint, nodeForHover.Span.ToZeroLengthSpan())) { if (!symbolTable.TryGetValue(symbolReference, out var symbol)) { if (symbolReference is InstanceFunctionCallSyntax && compilation.GetEntrypointSemanticModel().GetSymbolInfo(symbolReference) is FunctionSymbol ifcSymbol) { ValidateHover(hover, ifcSymbol); break; } // symbol ref not bound to a symbol hover.Should().BeNull(); continue; } switch (symbol !.Kind) {
public async Task DidOpenTextDocument_should_trigger_PublishDiagnostics() { var documentUri = DocumentUri.From("/template.bicep"); var diagsReceived = new TaskCompletionSource <PublishDiagnosticsParams>(); using var helper = await LanguageServerHelper.StartServerWithClientConnectionAsync(this.TestContext, options => { options.OnPublishDiagnostics(diags => { diagsReceived.SetResult(diags); }); }); var client = helper.Client; // open document client.TextDocument.DidOpenTextDocument(TextDocumentParamHelper.CreateDidOpenDocumentParams(documentUri, @" param myParam string = 2 resource myRes 'invalidFormat' = { } randomToken ", 1)); var response = await IntegrationTestHelper.WithTimeoutAsync(diagsReceived.Task); response.Diagnostics.Should().SatisfyRespectively( d => { d.Range.Should().HaveRange((1, 6), (1, 13)); // note documentation pretty printing moves Uri to code for output d.Should().HaveCodeAndSeverity(new NoUnusedParametersRule().Uri !.AbsoluteUri, DiagnosticSeverity.Warning); }, d => { d.Range.Should().HaveRange((1, 23), (1, 24)); d.Should().HaveCodeAndSeverity("BCP027", DiagnosticSeverity.Error); }, d => { d.Range.Should().HaveRange((2, 15), (2, 30)); d.Should().HaveCodeAndSeverity("BCP029", DiagnosticSeverity.Error); }, d => { d.Range.Should().HaveRange((5, 0), (5, 11)); d.Should().HaveCodeAndSeverity("BCP007", DiagnosticSeverity.Error); } ); // change document diagsReceived = new TaskCompletionSource <PublishDiagnosticsParams>(); client.TextDocument.DidChangeTextDocument(TextDocumentParamHelper.CreateDidChangeTextDocumentParams(documentUri, @" param myParam string = 'fixed!' resource myRes 'invalidFormat' = { } randomToken ", 2)); response = await IntegrationTestHelper.WithTimeoutAsync(diagsReceived.Task); response.Diagnostics.Should().SatisfyRespectively( d => { d.Range.Should().HaveRange((1, 6), (1, 13)); // documentation provided with linter sets code to uri for pretty link print outs d.Should().HaveCodeAndSeverity(new NoUnusedParametersRule().Uri !.AbsoluteUri, DiagnosticSeverity.Warning); }, d => { d.Range.Should().HaveRange((2, 15), (2, 30)); d.Should().HaveCodeAndSeverity("BCP029", DiagnosticSeverity.Error); }, d => { d.Range.Should().HaveRange((5, 0), (5, 11)); d.Should().HaveCodeAndSeverity("BCP007", DiagnosticSeverity.Error); } ); // close document diagsReceived = new TaskCompletionSource <PublishDiagnosticsParams>(); client.TextDocument.DidCloseTextDocument(TextDocumentParamHelper.CreateDidCloseTextDocumentParams(documentUri, 3)); response = await IntegrationTestHelper.WithTimeoutAsync(diagsReceived.Task); response.Diagnostics.Should().BeEmpty(); }
public async Task BicepFileOpen_ShouldFireTelemetryEvent() { var testOutputPath = Path.Combine(TestContext.ResultsDirectory, Guid.NewGuid().ToString()); var bicepConfigFileContents = @"{ ""analyzers"": { ""core"": { ""verbose"": false, ""enabled"": true, ""rules"": { ""no-unused-params"": { ""level"": ""warning"" }, ""no-unused-vars"": { ""level"": ""info"" } } } } }"; FileHelper.SaveResultFile(TestContext, "bicepconfig.json", bicepConfigFileContents, testOutputPath); var fileSystemDict = new Dictionary <Uri, string>(); var mainBicepFileContents = @"param appInsightsName string = 'testAppInsightsName' var deployGroups = true resource applicationInsights 'Microsoft.Insights/components@2015-05-01' = { name: appInsightsName location: resourceGroup().location kind: 'web' properties: { Application_Type: 'web' } resource favorites 'favorites@2015-05-01'{ name: 'testName' } } module apimGroups 'groups.bicep' = if (deployGroups) { name: 'apimGroups' } param location string = 'testLocation' #disable-next-line"; var mainBicepFilePath = FileHelper.SaveResultFile(TestContext, "main.bicep", mainBicepFileContents, testOutputPath); var mainUri = DocumentUri.FromFileSystemPath(mainBicepFilePath); fileSystemDict[mainUri.ToUri()] = mainBicepFileContents; var referencedBicepFileContents = @"resource parentAPIM 'Microsoft.ApiManagement/service@2019-01-01' existing = { name: 'testApimInstanceName' } resource apimGroup 'Microsoft.ApiManagement/service/groups@2020-06-01-preview' = { name: 'apiManagement/groups' } param storageAccount string = 'testStorageAccount' param location string = 'testLocation' var useDefaultSettings = true"; var referencedBicepFilePath = FileHelper.SaveResultFile(TestContext, "groups.bicep", referencedBicepFileContents, testOutputPath); var moduleUri = DocumentUri.FromFileSystemPath(referencedBicepFilePath); fileSystemDict[moduleUri.ToUri()] = referencedBicepFileContents; var telemetryEventsListener = new MultipleMessageListener <BicepTelemetryEvent>(); using var helper = await LanguageServerHelper.StartServerWithClientConnectionAsync( TestContext, options => { options.OnTelemetryEvent <BicepTelemetryEvent>(telemetry => telemetryEventsListener.AddMessage(telemetry)); }, creationOptions : new Server.CreationOptions(FileResolver: new InMemoryFileResolver(fileSystemDict))); var client = helper.Client; client.TextDocument.DidOpenTextDocument(TextDocumentParamHelper.CreateDidOpenDocumentParams(mainUri, fileSystemDict[mainUri.ToUri()], 1)); var bicepTelemetryEvent = await telemetryEventsListener.WaitNext(); IDictionary <string, string> properties = new Dictionary <string, string> { { "enabled", "true" }, { "simplify-interpolation", "warning" }, { "no-unused-vars", "info" }, { "no-hardcoded-env-urls", "warning" }, { "no-unused-params", "warning" }, { "prefer-interpolation", "warning" }, { "protect-commandtoexecute-secrets", "warning" }, { "no-unnecessary-dependson", "warning" }, { "adminusername-should-not-be-literal", "warning" }, { "use-stable-vm-image", "warning" }, { "secure-parameter-default", "warning" }, { "outputs-should-not-contain-secrets", "warning" }, }; bicepTelemetryEvent.EventName.Should().Be(TelemetryConstants.EventNames.LinterRuleStateOnBicepFileOpen); bicepTelemetryEvent.Properties.Should().Contain(properties); properties = new Dictionary <string, string> { { "modules", "1" }, { "parameters", "2" }, { "resources", "2" }, { "variables", "1" }, { "fileSizeInBytes", "488" }, { "lineCount", "23" }, { "errors", "2" }, { "warnings", "1" }, { "modulesInReferencedFiles", "0" }, { "parentResourcesInReferencedFiles", "2" }, { "parametersInReferencedFiles", "2" }, { "variablesInReferencedFiles", "1" } }; bicepTelemetryEvent = await telemetryEventsListener.WaitNext(); bicepTelemetryEvent.EventName.Should().Be(TelemetryConstants.EventNames.BicepFileOpen); bicepTelemetryEvent.Properties.Should().Contain(properties); }
public async Task RequestDeploymentGraphShouldReturnDeploymentGraph() { var diagnosticsListener = new MultipleMessageListener <PublishDiagnosticsParams>(); var fileSystemDict = new Dictionary <Uri, string>(); using var helper = await LanguageServerHelper.StartServerWithClientConnectionAsync( this.TestContext, options => options.OnPublishDiagnostics(diagnosticsParams => diagnosticsListener.AddMessage(diagnosticsParams)), new LanguageServer.Server.CreationOptions(NamespaceProvider: BuiltInTestTypes.Create(), FileResolver: new InMemoryFileResolver(fileSystemDict))); var client = helper.Client; var mainUri = DocumentUri.FromFileSystemPath("/main.bicep"); fileSystemDict[mainUri.ToUri()] = @" resource res1 'Test.Rp/basicTests@2020-01-01' = { name: 'res1' } resource res2 'Test.Rp/readWriteTests@2020-01-01' = { name: 'res2' properites: { readwrite: mod1.outputs.output1 } } resource unknownRes = { } module mod1 './modules/module1.bicep' = { name: 'mod1' } module mod2 './modules/module2.bicep' = { name: 'mod2' } module nonExistingMod './path/to/nonExistingModule.bicep' = { } "; var module1Uri = DocumentUri.FromFileSystemPath("/modules/module1.bicep"); fileSystemDict[module1Uri.ToUri()] = @" resource res3 'Test.Rp/basicTests@2020-01-01' = { name: 'res3' } output output1 int = 123 "; var module2Uri = DocumentUri.FromFileSystemPath("/modules/module2.bicep"); fileSystemDict[module2Uri.ToUri()] = @" resource res4 'Test.Rp/basicTests@2020-01-01' = { name: 'res4' } module nestedMod './nestedModules/nestedModule.bicep' = [for x in []: { name: 'nestedMod' dependsOn: [ res4 ] }] "; var nestedModuleUri = DocumentUri.FromFileSystemPath("/modules/nestedModules/nestedModule.bicep"); fileSystemDict[nestedModuleUri.ToUri()] = @" resource res5 'Test.Rp/basicTests@2020-01-01' = { name: 'res5' } "; client.TextDocument.DidOpenTextDocument(TextDocumentParamHelper.CreateDidOpenDocumentParams(mainUri, fileSystemDict[mainUri.ToUri()], 1)); await diagnosticsListener.WaitNext(); var deploymentGraph = await client.SendRequest(new BicepDeploymentGraphParams(new TextDocumentIdentifier(mainUri)), default); deploymentGraph.Should().NotBeNull(); deploymentGraph !.Nodes.Should().Equal( new BicepDeploymentGraphNode("mod1", "<module>", false, CreateTextRange(15, 0, 17, 1), true, false, Path.GetFullPath(mainUri.GetFileSystemPath())), new BicepDeploymentGraphNode("mod1::res3", "Test.Rp/basicTests", false, CreateTextRange(1, 0, 3, 1), false, false, Path.GetFullPath(module1Uri.GetFileSystemPath())), new BicepDeploymentGraphNode("mod2", "<module>", false, CreateTextRange(19, 0, 21, 1), true, false, Path.GetFullPath(mainUri.GetFileSystemPath())), new BicepDeploymentGraphNode("mod2::nestedMod", "<module>", true, CreateTextRange(5, 0, 10, 2), true, false, Path.GetFullPath(module2Uri.GetFileSystemPath())), new BicepDeploymentGraphNode("mod2::nestedMod::res5", "Test.Rp/basicTests", false, CreateTextRange(1, 0, 3, 1), false, false, Path.GetFullPath(nestedModuleUri.GetFileSystemPath())), new BicepDeploymentGraphNode("mod2::res4", "Test.Rp/basicTests", false, CreateTextRange(1, 0, 3, 1), false, false, Path.GetFullPath(module2Uri.GetFileSystemPath())), new BicepDeploymentGraphNode("nonExistingMod", "<module>", false, CreateTextRange(23, 0, 24, 1), false, true, Path.GetFullPath(mainUri.GetFileSystemPath())), new BicepDeploymentGraphNode("res1", "Test.Rp/basicTests", false, CreateTextRange(1, 0, 3, 1), false, false, Path.GetFullPath(mainUri.GetFileSystemPath())), new BicepDeploymentGraphNode("res2", "Test.Rp/readWriteTests", false, CreateTextRange(5, 0, 10, 1), false, true, Path.GetFullPath(mainUri.GetFileSystemPath()))); deploymentGraph !.Edges.Should().Equal( new BicepDeploymentGraphEdge("mod2::nestedMod", "mod2::res4"), new BicepDeploymentGraphEdge("res2", "mod1")); deploymentGraph !.ErrorCount.Should().Be(7); }
public async Task RequestDocumentSymbol_should_return_full_symbol_list() { var documentUri = DocumentUri.From("/template.bicep"); var diagsReceived = new TaskCompletionSource <PublishDiagnosticsParams>(); using var helper = await LanguageServerHelper.StartServerWithClientConnectionAsync(this.TestContext, options => { options.OnPublishDiagnostics(diags => { diagsReceived.SetResult(diags); }); }); var client = helper.Client; // client opens the document client.TextDocument.DidOpenTextDocument(TextDocumentParamHelper.CreateDidOpenDocumentParams(documentUri, @" param myParam string = 'test' resource myRes 'myRp/provider@2019-01-01' = { name: 'test' } module myMod './module.bicep' = { name: 'test' } output myOutput string = 'myOutput' ", 0)); // client requests symbols var symbols = await client.TextDocument.RequestDocumentSymbol(new DocumentSymbolParams { TextDocument = new TextDocumentIdentifier { Uri = documentUri, }, }); symbols.Should().SatisfyRespectively( x => { x.DocumentSymbol !.Name.Should().Be("myParam"); x.DocumentSymbol.Kind.Should().Be(SymbolKind.Field); }, x => { x.DocumentSymbol !.Name.Should().Be("myRes"); x.DocumentSymbol.Kind.Should().Be(SymbolKind.Object); }, x => { x.DocumentSymbol !.Name.Should().Be("myMod"); x.DocumentSymbol.Kind.Should().Be(SymbolKind.Module); }, x => { x.DocumentSymbol !.Name.Should().Be("myOutput"); x.DocumentSymbol.Kind.Should().Be(SymbolKind.Interface); } ); // client deletes the output and renames the resource client.TextDocument.DidChangeTextDocument(TextDocumentParamHelper.CreateDidChangeTextDocumentParams(documentUri, @" param myParam string = 'test' resource myRenamedRes 'myRp/provider@2019-01-01' = { name: 'test' } module myMod './module.bicep' = { name: 'test' } ", 1)); // client requests symbols symbols = await client.TextDocument.RequestDocumentSymbol(new DocumentSymbolParams { TextDocument = new TextDocumentIdentifier { Uri = documentUri, }, }); symbols.Should().SatisfyRespectively( x => { x.DocumentSymbol !.Name.Should().Be("myParam"); x.DocumentSymbol.Kind.Should().Be(SymbolKind.Field); }, x => { x.DocumentSymbol !.Name.Should().Be("myRenamedRes"); x.DocumentSymbol.Kind.Should().Be(SymbolKind.Object); }, x => { x.DocumentSymbol !.Name.Should().Be("myMod"); x.DocumentSymbol.Kind.Should().Be(SymbolKind.Module); } ); }