public async Task Property_completions_include_descriptions() { var(file, cursors) = ParserHelper.GetFileWithCursors(@" resource testRes 'Test.Rp/readWriteTests@2020-01-01' = { name: 'testRes' properties: { | } } output string test = testRes.| output string test2 = testRes.properties.| "); var syntaxTree = SyntaxTree.Create(new Uri("file:///path/to/main.bicep"), file); var client = await IntegrationTestHelper.StartServerWithTextAsync(file, syntaxTree.FileUri, resourceTypeProvider : BuiltInTestTypes.Create()); var completions = await RequestCompletions(client, syntaxTree, cursors); completions.Should().SatisfyRespectively( x => x !.OrderBy(d => d.SortText).Should().SatisfyRespectively( d => d.Documentation !.MarkupContent !.Value.Should().Contain("This is a property which supports reading AND writing!"), d => d.Documentation !.MarkupContent !.Value.Should().Contain("This is a property which is required."), d => d.Documentation !.MarkupContent !.Value.Should().Contain("This is a property which only supports writing.")), x => x !.OrderBy(d => d.SortText).Should().SatisfyRespectively( d => d.Documentation !.MarkupContent !.Value.Should().Contain("apiVersion property"), d => d.Documentation !.MarkupContent !.Value.Should().Contain("id property"), d => d.Documentation !.MarkupContent !.Value.Should().Contain("name property"), d => d.Documentation !.MarkupContent !.Value.Should().Contain("properties property"), d => d.Documentation !.MarkupContent !.Value.Should().Contain("type property")), x => x !.OrderBy(d => d.SortText).Should().SatisfyRespectively( d => d.Documentation !.MarkupContent !.Value.Should().Contain("This is a property which only supports reading."), d => d.Documentation !.MarkupContent !.Value.Should().Contain("This is a property which supports reading AND writing!"), d => d.Documentation !.MarkupContent !.Value.Should().Contain("This is a property which is required."))); }
public async Task HighlightsShouldShowAllReferencesOfTheSymbol(DataSet dataSet) { var uri = DocumentUri.From($"/{dataSet.Name}"); using var client = await IntegrationTestHelper.StartServerWithTextAsync(dataSet.Bicep, uri); var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxFactory.CreateFromText(dataSet.Bicep)); var symbolTable = compilation.ReconstructSymbolTable(); var lineStarts = TextCoordinateConverter.GetLineStarts(dataSet.Bicep); var symbolToSyntaxLookup = symbolTable .Where(pair => pair.Value.Kind != SymbolKind.Error) .ToLookup(pair => pair.Value, pair => pair.Key); foreach (var(syntax, symbol) in symbolTable.Where(s => s.Value.Kind != SymbolKind.Error)) { var highlights = await client.RequestDocumentHighlight(new DocumentHighlightParams { TextDocument = new TextDocumentIdentifier(uri), Position = PositionHelper.GetPosition(lineStarts, syntax.Span.Position) }); // calculate expected highlights var expectedHighlights = symbolToSyntaxLookup[symbol].Select(node => CreateExpectedHighlight(lineStarts, node)); // ranges should match what we got from our own symbol table highlights.Should().BeEquivalentTo(expectedHighlights); } }
public async Task String_segments_do_not_return_completions() { var(file, cursors) = ParserHelper.GetFileWithCursors(@" var completeString = |'he|llo'| var interpolatedString = |'abc${|true}|de|f${|false}|gh|i'| var multilineString = |'''| hel|lo '''| "); var syntaxTree = SyntaxTree.Create(new Uri("file:///main.bicep"), file); using var client = await IntegrationTestHelper.StartServerWithTextAsync(file, syntaxTree.FileUri, resourceTypeProvider : TypeProvider); foreach (var cursor in cursors) { using (new AssertionScope().WithVisualCursor(syntaxTree, new TextSpan(cursor, 0))) { var completions = await client.RequestCompletion(new CompletionParams { TextDocument = new TextDocumentIdentifier(syntaxTree.FileUri), Position = TextCoordinateConverter.GetPosition(syntaxTree.LineStarts, cursor), }); completions.Should().BeEmpty(); } } }
public async Task Completions_are_not_offered_inside_comments() { var(file, cursors) = ParserHelper.GetFileWithCursors(@" var test = /|/ comment here| var test2 = /|* block c|omment *|/ "); var syntaxTree = SyntaxTree.Create(new Uri("file:///main.bicep"), file); using var client = await IntegrationTestHelper.StartServerWithTextAsync(file, syntaxTree.FileUri, resourceTypeProvider : TypeProvider); foreach (var cursor in cursors) { using (new AssertionScope().WithVisualCursor(syntaxTree, new TextSpan(cursor, 0))) { var completions = await client.RequestCompletion(new CompletionParams { TextDocument = new TextDocumentIdentifier(syntaxTree.FileUri), Position = TextCoordinateConverter.GetPosition(syntaxTree.LineStarts, cursor), }); completions.Should().BeEmpty(); } } }
public async Task RenamingFunctionsShouldProduceEmptyEdit(DataSet dataSet) { var uri = DocumentUri.From($"/{dataSet.Name}"); using var client = await IntegrationTestHelper.StartServerWithTextAsync(dataSet.Bicep, uri); var compilation = dataSet.CopyFilesAndCreateCompilation(TestContext, out _); var symbolTable = compilation.ReconstructSymbolTable(); var lineStarts = compilation.SyntaxTreeGrouping.EntryPoint.LineStarts; var validFunctionCallPairs = symbolTable .Where(pair => pair.Value.Kind == SymbolKind.Function) .Select(pair => pair.Key); foreach (var syntax in validFunctionCallPairs) { var edit = await client.RequestRename(new RenameParams { NewName = "NewIdentifier", TextDocument = new TextDocumentIdentifier(uri), Position = IntegrationTestHelper.GetPosition(lineStarts, syntax) }); edit.DocumentChanges.Should().BeNullOrEmpty(); edit.Changes.Should().BeNull(); } }
private async Task <string> RequestSnippetCompletion(string bicepFileName, CompletionData completionData, string placeholderFile) { var documentUri = DocumentUri.FromFileSystemPath(bicepFileName); var syntaxTree = SyntaxTree.Create(documentUri.ToUri(), placeholderFile); var client = await IntegrationTestHelper.StartServerWithTextAsync( placeholderFile, documentUri, null, TypeProvider); var cursor = placeholderFile.IndexOf("// Insert snippet here"); var completions = await client.RequestCompletion(new CompletionParams { TextDocument = documentUri, Position = TextCoordinateConverter.GetPosition(syntaxTree.LineStarts, cursor), }); var matchingSnippets = completions.Where(x => x.Kind == CompletionItemKind.Snippet && x.Label == completionData.Prefix); matchingSnippets.Should().HaveCount(1); var completion = matchingSnippets.First(); completion.TextEdit.Should().NotBeNull(); completion.TextEdit !.Range.Should().Be(new TextSpan(cursor, 0).ToRange(syntaxTree.LineStarts)); completion.TextEdit.NewText.Should().NotBeNullOrWhiteSpace(); return(completion.TextEdit.NewText); }
public async Task NonFunctionCallSyntaxShouldProvideNoSignatureHelp(DataSet dataSet) { var uri = DocumentUri.From($"/{dataSet.Name}"); using var client = await IntegrationTestHelper.StartServerWithTextAsync(dataSet.Bicep, uri); var compilation = dataSet.CopyFilesAndCreateCompilation(TestContext, out _); var tree = compilation.SyntaxTreeGrouping.EntryPoint; var nonFunctions = SyntaxAggregator.Aggregate( tree.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) { var position = PositionHelper.GetPosition(tree.LineStarts, nonFunction.Span.Position); var signatureHelp = await RequestSignatureHelp(client, position, uri); signatureHelp.Should().BeNull(); } }
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 client = await IntegrationTestHelper.StartServerWithTextAsync(TestContext, dataSet.Bicep, uri); 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 client = await IntegrationTestHelper.StartServerWithTextAsync(dataSet.Bicep, uri); var compilation = dataSet.CopyFilesAndCreateCompilation(TestContext, out _); var symbolTable = compilation.ReconstructSymbolTable(); var lineStarts = compilation.SyntaxTreeGrouping.EntryPoint.LineStarts; var undeclaredSymbolBindings = symbolTable.Where(pair => !(pair.Value is DeclaredSymbol)); foreach (var(syntax, _) in undeclaredSymbolBindings) { var response = await client.RequestDefinition(new DefinitionParams { TextDocument = new TextDocumentIdentifier(uri), Position = PositionHelper.GetPosition(lineStarts, syntax.Span.Position) }); // 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 GoToDefinitionRequestOnUnsupportedOrInvalidSyntaxNodeShouldReturnEmptyResponse(DataSet dataSet) { var uri = DocumentUri.From($"/{dataSet.Name}"); using var client = await IntegrationTestHelper.StartServerWithTextAsync(this.TestContext, dataSet.Bicep, uri); 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 RenamingFunctionsShouldProduceEmptyEdit(DataSet dataSet) { var uri = DocumentUri.From($"/{dataSet.Name}"); using var client = await IntegrationTestHelper.StartServerWithTextAsync(dataSet.Bicep, uri); var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxFactory.CreateFromText(dataSet.Bicep)); var symbolTable = compilation.ReconstructSymbolTable(); var lineStarts = TextCoordinateConverter.GetLineStarts(dataSet.Bicep); var validFunctionCallPairs = symbolTable .Where(pair => pair.Value.Kind == SymbolKind.Function) .Select(pair => pair.Key); foreach (var syntax in validFunctionCallPairs) { var edit = await client.RequestRename(new RenameParams { NewName = "NewIdentifier", TextDocument = new TextDocumentIdentifier(uri), Position = IntegrationTestHelper.GetPosition(lineStarts, syntax) }); edit.DocumentChanges.Should().BeNullOrEmpty(); edit.Changes.Should().BeNull(); } }
public async Task HoveringOverSymbolReferencesAndDeclarationsShouldProduceHovers(DataSet dataSet) { var compilation = dataSet.CopyFilesAndCreateCompilation(TestContext, out _, out var fileUri); var uri = DocumentUri.From(fileUri); var client = await IntegrationTestHelper.StartServerWithTextAsync(this.TestContext, dataSet.Bicep, uri, resourceTypeProvider : AzResourceTypeProvider.CreateWithAzTypes(), fileResolver : BicepTestConstants.FileResolver); var symbolTable = compilation.ReconstructSymbolTable(); var lineStarts = compilation.SyntaxTreeGrouping.EntryPoint.LineStarts; var symbolReferences = SyntaxAggregator.Aggregate( compilation.SyntaxTreeGrouping.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.SyntaxTreeGrouping.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 HighlightsShouldShowAllReferencesOfTheSymbol(DataSet dataSet) { var uri = DocumentUri.From($"/{dataSet.Name}"); using var client = await IntegrationTestHelper.StartServerWithTextAsync(dataSet.Bicep, uri); var compilation = dataSet.CopyFilesAndCreateCompilation(TestContext, out _); var symbolTable = compilation.ReconstructSymbolTable(); var lineStarts = compilation.SyntaxTreeGrouping.EntryPoint.LineStarts; var symbolToSyntaxLookup = symbolTable .Where(pair => pair.Value.Kind != SymbolKind.Error) .ToLookup(pair => pair.Value, pair => pair.Key); foreach (var(syntax, symbol) in symbolTable.Where(s => s.Value.Kind != SymbolKind.Error)) { 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)); // ranges should match what we got from our own symbol table highlights.Should().BeEquivalentTo(expectedHighlights); } }
public async Task VerifyNestedResourceCompletionReturnsCustomSnippetWithoutParentInformation() { string fileWithCursors = @"resource automationAccount 'Microsoft.Automation/automationAccounts@2019-06-01' = { name: 'name' location: resourceGroup().location | }"; var(file, cursors) = ParserHelper.GetFileWithCursors(fileWithCursors); var syntaxTree = SyntaxTree.Create(new Uri("file:///path/to/main.bicep"), file); var client = await IntegrationTestHelper.StartServerWithTextAsync(TestContext, file, syntaxTree.FileUri, resourceTypeProvider : TypeProvider); var completionLists = await RequestCompletions(client, syntaxTree, cursors); completionLists.Count().Should().Be(1); var snippetCompletion = completionLists.First() !.Items.Where(x => x.Kind == CompletionItemKind.Snippet && x.Label == "res-automation-cred"); snippetCompletion.Should().SatisfyRespectively( c => { c.Label.Should().Be("res-automation-cred"); c.Detail.Should().Be("Automation Credential"); c.InsertTextFormat.Should().Be(InsertTextFormat.Snippet); c.TextEdit?.TextEdit?.NewText?.Should().BeEquivalentToIgnoringNewlines(@"resource ${2:automationCredential} 'credentials@2019-06-01' = { name: ${3:'name'} properties: { userName: ${4:'userName'} password: ${5:'password'} description: ${6:'description'} } }"); }); }
public async Task HighlightsShouldShowAllReferencesOfTheSymbol(DataSet dataSet) { var uri = DocumentUri.From($"/{dataSet.Name}"); using var client = await IntegrationTestHelper.StartServerWithTextAsync(dataSet.Bicep, uri); var compilation = dataSet.CopyFilesAndCreateCompilation(TestContext, out _); var symbolTable = compilation.ReconstructSymbolTable(); var lineStarts = compilation.SyntaxTreeGrouping.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)); 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)); // ranges should match what we got from our own symbol table highlights.Should().BeEquivalentTo(expectedHighlights); } }
public async Task NonExistentUriShouldProvideNoSignatureHelp() { using var client = await IntegrationTestHelper.StartServerWithTextAsync(this.TestContext, string.Empty, DocumentUri.From("/fake.bicep")); 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 client = await IntegrationTestHelper.StartServerWithTextAsync(this.TestContext, dataSet.Bicep, uri); 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)); } } }
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); var client = await IntegrationTestHelper.StartServerWithTextAsync(testContext, file, bicepFile.FileUri, creationOptions : new LanguageServer.Server.CreationOptions(NamespaceProvider: BuiltInTestTypes.Create())); var results = await RequestDefinitions(client, bicepFile, cursors); assertAction(results); }
public async Task RequestingCodeActionWithFixableDiagnosticsShouldProduceQuickFixes(DataSet dataSet) { var uri = DocumentUri.From($"/{dataSet.Name}"); var client = await IntegrationTestHelper.StartServerWithTextAsync(dataSet.Bicep, uri); // construct a parallel compilation var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxFactory.CreateFromText(dataSet.Bicep)); var lineStarts = TextCoordinateConverter.GetLineStarts(dataSet.Bicep); var fixables = compilation.GetSemanticModel().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.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(); var replacementList = bicepFix.Replacements.ToList(); for (int j = 0; j < textEditList.Count; j++) { var textEdit = textEditList[j]; var replacement = replacementList[j]; textEdit.Range.Should().Be(replacement.ToRange(lineStarts)); textEdit.NewText.Should().Be(replacement.Text); } } } } }
public async Task HoveringOverSymbolReferencesAndDeclarationsShouldProduceHovers(DataSet dataSet) { var uri = DocumentUri.From($"/{dataSet.Name}"); var client = await IntegrationTestHelper.StartServerWithTextAsync(dataSet.Bicep, uri, resourceTypeProvider : new AzResourceTypeProvider(new TypeLoader())); // construct a parallel compilation var compilation = dataSet.CopyFilesAndCreateCompilation(TestContext, out _); var symbolTable = compilation.ReconstructSymbolTable(); var lineStarts = compilation.SyntaxTreeGrouping.EntryPoint.LineStarts; var symbolReferences = SyntaxAggregator.Aggregate( compilation.SyntaxTreeGrouping.EntryPoint.ProgramSyntax, new List <SyntaxBase>(), (accumulated, node) => { if (node is ISymbolReference || node is ITopLevelNamedDeclarationSyntax) { accumulated.Add(node); } return(accumulated); }, accumulated => accumulated); foreach (SyntaxBase symbolReference in symbolReferences) { var nodeForHover = symbolReference switch { ITopLevelDeclarationSyntax d => d.Keyword, ResourceAccessSyntax r => r.ResourceName, _ => symbolReference, }; var hover = await client.RequestHover(new HoverParams { TextDocument = new TextDocumentIdentifier(uri), Position = PositionHelper.GetPosition(lineStarts, nodeForHover.Span.Position) }); // fancy method to give us some annotated source code to look at if any assertions fail :) using (CreateAssertionScopeWithContext(compilation.SyntaxTreeGrouping.EntryPoint, hover, nodeForHover.Span.ToZeroLengthSpan())) { if (symbolTable.TryGetValue(symbolReference, out var symbol) == false) { // symbol ref not bound to a symbol hover.Should().BeNull(); continue; } switch (symbol !.Kind) {
public async Task GoToDefinitionOnUnboundSyntaxNodeShouldReturnEmptyResponse(DataSet dataSet) { // local function bool IsUnboundNode(IDictionary <SyntaxBase, Symbol> dictionary, SyntaxBase syntax) => dictionary.ContainsKey(syntax) == false && !(syntax is Token); var uri = DocumentUri.From($"/{dataSet.Name}"); using var client = await IntegrationTestHelper.StartServerWithTextAsync(dataSet.Bicep, uri); var compilation = dataSet.CopyFilesAndCreateCompilation(TestContext, out _); var symbolTable = compilation.ReconstructSymbolTable(); var lineStarts = compilation.SyntaxTreeGrouping.EntryPoint.LineStarts; var unboundNodes = SyntaxAggregator.Aggregate( source: compilation.SyntaxTreeGrouping.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)); foreach (var syntax in unboundNodes) { var offset = syntax switch { // base expression could be a variable access which is bound and will throw off the test PropertyAccessSyntax propertyAccess => propertyAccess.PropertyName.Span.Position, ArrayAccessSyntax arrayAccess => arrayAccess.OpenSquare.Span.Position, _ => syntax.Span.Position }; var response = await client.RequestDefinition(new DefinitionParams { TextDocument = new TextDocumentIdentifier(uri), Position = PositionHelper.GetPosition(lineStarts, offset) }); // 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 VerifyNestedResourceBodyCompletionReturnsSnippets() { string fileWithCursors = @"resource automationAccount 'Microsoft.Automation/automationAccounts@2019-06-01' = { name: 'name' location: resourceGroup().location resource automationCredential 'credentials@2019-06-01' = | }"; var(file, cursors) = ParserHelper.GetFileWithCursors(fileWithCursors); var syntaxTree = SyntaxTree.Create(new Uri("file:///path/to/main.bicep"), file); var client = await IntegrationTestHelper.StartServerWithTextAsync(TestContext, file, syntaxTree.FileUri, resourceTypeProvider : TypeProvider); var completionLists = await RequestCompletions(client, syntaxTree, cursors); completionLists.Count().Should().Be(1); var snippetCompletions = completionLists.First() !.Items.Where(x => x.Kind == CompletionItemKind.Snippet); snippetCompletions.Should().SatisfyRespectively( c => { c.Label.Should().Be("{}"); }, c => { c.Label.Should().Be("snippet"); }, c => { c.Label.Should().Be("required-properties"); }, c => { c.Label.Should().Be("if"); }, c => { c.Label.Should().Be("for"); }, c => { c.Label.Should().Be("for-indexed"); }, c => { c.Label.Should().Be("for-filtered"); }); }
public async Task FindReferencesWithoutDeclarationsShouldProduceCorrectResults(DataSet dataSet) { var(compilation, _, fileUri) = await dataSet.SetupPrerequisitesAndCreateCompilation(TestContext); var uri = DocumentUri.From(fileUri); using var client = await IntegrationTestHelper.StartServerWithTextAsync(this.TestContext, dataSet.Bicep, uri); 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); } } }
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 client = await IntegrationTestHelper.StartServerWithTextAsync(this.TestContext, dataSet.Bicep, uri); 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 RenamingIdentifierAccessOrDeclarationShouldRenameDeclarationAndAllReferences(DataSet dataSet) { var uri = DocumentUri.From($"/{dataSet.Name}"); using var client = await IntegrationTestHelper.StartServerWithTextAsync(dataSet.Bicep, uri); var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxFactory.CreateFromText(dataSet.Bicep)); var symbolTable = compilation.ReconstructSymbolTable(); var lineStarts = TextCoordinateConverter.GetLineStarts(dataSet.Bicep); 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 IDeclarationSyntax) && 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.DocumentChanges.Should().BeNullOrEmpty(); edit.Changes.Should().HaveCount(1); edit.Changes.Should().ContainKey(uri); var textEdits = edit.Changes[uri]; textEdits.Should().NotBeEmpty(); var expectedEdits = symbolToSyntaxLookup[symbol] .Select(node => CreateExpectedTextEdit(lineStarts, expectedNewText, node)); textEdits.Should().BeEquivalentTo(expectedEdits); } }
public async Task CompletionRequestShouldProduceExpectedCompletions(DataSet dataSet, string setName, IList <Position> positions) { var uri = DocumentUri.From($"/{dataSet.Name}"); using var client = await IntegrationTestHelper.StartServerWithTextAsync(dataSet.Bicep, uri); var intermediate = new List <(Position position, JToken actual)>(); foreach (var position in positions) { var actual = await GetActualCompletions(client, uri, position); intermediate.Add((position, actual)); } ValidateCompletions(dataSet, setName, intermediate); }
public async Task RenamingNonSymbolsShouldProduceEmptyEdit(DataSet dataSet) { // local function bool IsWrongNode(SyntaxBase node) => !(node is ISymbolReference) && !(node is IDeclarationSyntax) && !(node is Token); var uri = DocumentUri.From($"/{dataSet.Name}"); using var client = await IntegrationTestHelper.StartServerWithTextAsync(dataSet.Bicep, uri); var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxFactory.CreateFromText(dataSet.Bicep)); var symbolTable = compilation.ReconstructSymbolTable(); var lineStarts = TextCoordinateConverter.GetLineStarts(dataSet.Bicep); var symbolToSyntaxLookup = symbolTable .Where(pair => pair.Value.Kind != SymbolKind.Error) .ToLookup(pair => pair.Value, pair => pair.Key); var wrongNodes = SyntaxAggregator.Aggregate( compilation.ProgramSyntax, new List <SyntaxBase>(), (accumulated, node) => { if (IsWrongNode(node) && !(node is ProgramSyntax)) { accumulated.Add(node); } return(accumulated); }, accumulated => accumulated, (accumulated, node) => IsWrongNode(node)); foreach (var syntax in wrongNodes) { var edit = await client.RequestRename(new RenameParams { NewName = "NewIdentifier", TextDocument = new TextDocumentIdentifier(uri), Position = IntegrationTestHelper.GetPosition(lineStarts, syntax) }); edit.DocumentChanges.Should().BeNullOrEmpty(); edit.Changes.Should().BeNull(); } }
public async Task HoveringOverSymbolReferencesAndDeclarationsShouldProduceHovers(DataSet dataSet) { var uri = DocumentUri.From($"/{dataSet.Name}"); var client = await IntegrationTestHelper.StartServerWithTextAsync(dataSet.Bicep, uri); // construct a parallel compilation var compilation = dataSet.CopyFilesAndCreateCompilation(TestContext, out _); var symbolTable = compilation.ReconstructSymbolTable(); var lineStarts = compilation.SyntaxTreeGrouping.EntryPoint.LineStarts; var symbolReferences = SyntaxAggregator.Aggregate( compilation.SyntaxTreeGrouping.EntryPoint.ProgramSyntax, new List <SyntaxBase>(), (accumulated, node) => { if (node is ISymbolReference || node is INamedDeclarationSyntax) { accumulated.Add(node); } return(accumulated); }, accumulated => accumulated); foreach (SyntaxBase symbolReference in symbolReferences) { var syntaxPosition = symbolReference is IDeclarationSyntax declaration ? declaration.Keyword.Span.Position : symbolReference.Span.Position; var hover = await client.RequestHover(new HoverParams { TextDocument = new TextDocumentIdentifier(uri), Position = PositionHelper.GetPosition(lineStarts, syntaxPosition) }); if (symbolTable.TryGetValue(symbolReference, out var symbol) == false) { // symbol ref not bound to a symbol hover.Should().BeNull(); continue; } switch (symbol !.Kind) {
public async Task RequestingCodeActionWithFixableDiagnosticsShouldProduceQuickFixes(DataSet dataSet) { // ensure all files (e.g. modules) are present locally string basePath = dataSet.SaveFilesToTestDirectory(this.TestContext); var entryPoint = Path.Combine(basePath, "main.bicep"); var uri = DocumentUri.FromFileSystemPath(entryPoint); // start language server var client = await IntegrationTestHelper.StartServerWithTextAsync(this.TestContext, dataSet.Bicep, uri, fileResolver : new FileResolver()); // construct a parallel compilation var compilation = dataSet.CopyFilesAndCreateCompilation(TestContext, out _); var lineStarts = compilation.SyntaxTreeGrouping.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.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 VerifyResourceBodyCompletionWithoutExistingKeywordIncludesCustomSnippet() { string text = @"resource aksCluster 'Microsoft.ContainerService/managedClusters@2021-03-01' = "; var syntaxTree = SyntaxTree.Create(new Uri("file:///main.bicep"), text); using var client = await IntegrationTestHelper.StartServerWithTextAsync(text, syntaxTree.FileUri, resourceTypeProvider : TypeProvider); var completions = await client.RequestCompletion(new CompletionParams { TextDocument = new TextDocumentIdentifier(syntaxTree.FileUri), Position = TextCoordinateConverter.GetPosition(syntaxTree.LineStarts, text.Length), }); completions.Should().SatisfyRespectively( c => { c.Label.Should().Be("{}"); }, c => { c.Label.Should().Be("snippet"); }, c => { c.Label.Should().Be("required-properties"); }, c => { c.Label.Should().Be("if"); }, c => { c.Label.Should().Be("for"); }, c => { c.Label.Should().Be("for-indexed"); }, c => { c.Label.Should().Be("for-filtered"); }); }