Example #1
0
        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 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));
                }
            }
        }
        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);
        }
Example #6
0
        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);
                }
            }
        }
        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();
            }
        }
Example #8
0
        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);
            }
        }
Example #10
0
        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];
Example #11
0
        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 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();
                }
            }
        }
Example #13
0
        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)
                    {