Ejemplo n.º 1
0
        public async Task GoToDefinitionOnUnboundSyntaxNodeShouldReturnEmptyResponse(DataSet dataSet)
        {
            var uri = DocumentUri.From($"/{dataSet.Name}");

            using var client = await IntegrationTestHelper.StartServerWithText(dataSet.Bicep, uri);

            var compilation = new Compilation(SyntaxFactory.CreateFromText(dataSet.Bicep));
            var symbolTable = compilation.ReconstructSymbolTable();
            var lineStarts  = TextCoordinateConverter.GetLineStarts(dataSet.Bicep);

            var unboundNodes = SyntaxAggregator.Aggregate(
                source: compilation.ProgramSyntax,
                seed: new List <SyntaxBase>(),
                function: (accumulated, syntax) =>
            {
                if (symbolTable.ContainsKey(syntax) == false && !(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) => symbolTable.ContainsKey(syntax) == false);

            foreach (var syntax in unboundNodes)
            {
                var response = await client.RequestDefinition(new DefinitionParams
                {
                    TextDocument = new TextDocumentIdentifier(uri),
                    Position     = PositionHelper.GetPosition(lineStarts, syntax.Span.Position)
                });

                // go to definition on a syntax node that isn't bound to a symbol should produce an empty response
                response.Should().BeEmpty();
            }
        }
Ejemplo n.º 2
0
        public async Task GoToDefinitionRequestOnValidSymbolReferenceShouldReturnLocationOfDeclaredSymbol(DataSet dataSet)
        {
            var uri = DocumentUri.From($"/{dataSet.Name}");

            using var client = await IntegrationTestHelper.StartServerWithText(dataSet.Bicep, uri);

            var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxFactory.CreateFromText(dataSet.Bicep));
            var symbolTable = compilation.ReconstructSymbolTable();
            var lineStarts  = TextCoordinateConverter.GetLineStarts(dataSet.Bicep);

            // filter out symbols that don't have locations
            var declaredSymbolBindings = symbolTable
                                         .Where(pair => pair.Value is DeclaredSymbol)
                                         .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     = PositionHelper.GetPosition(lineStarts, syntax.Span.Position)
                });

                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));

                // origin selection range should be the span of the syntax node that references the symbol
                link.OriginSelectionRange.Should().Be(syntax.ToRange(lineStarts));
            }
        }
Ejemplo n.º 3
0
        public async Task RequestingHighlightsForWrongNodeShouldProduceNoHighlights(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 lineStarts  = TextCoordinateConverter.GetLineStarts(dataSet.Bicep);

            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 highlights = await client.RequestDocumentHighlight(new DocumentHighlightParams
                {
                    TextDocument = new TextDocumentIdentifier(uri),
                    Position     = IntegrationTestHelper.GetPosition(lineStarts, syntax)
                });

                highlights.Should().BeEmpty();
            }
        }
Ejemplo n.º 4
0
        public async Task FindReferencesWithoutDeclarationsShouldProduceCorrectResults(DataSet dataSet)
        {
            var uri = DocumentUri.From($"/{dataSet.Name}");

            using var client = await IntegrationTestHelper.StartServerWithText(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 locations = await client.RequestReferences(new ReferenceParams
                {
                    TextDocument = new TextDocumentIdentifier(uri),
                    Context      = new ReferenceContext
                    {
                        IncludeDeclaration = false
                    },
                    Position = PositionHelper.GetPosition(lineStarts, syntax.Span.Position)
                });

                // 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 IDeclarationSyntax))
                                     .Select(node => PositionHelper.GetNameRange(lineStarts, node));

                // ranges should match what we got from our own symbol table
                locations.Select(l => l.Range).Should().BeEquivalentTo(expectedRanges);
            }
        }
Ejemplo n.º 5
0
        public async Task RequestingCodeActionWithNonFixableDiagnosticsShouldProduceEmptyQuickFixes(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 nonFixables = compilation.GetSemanticModel().GetAllDiagnostics().Where(diagnostic => !(diagnostic is IFixable));

            foreach (var nonFixable in nonFixables)
            {
                CommandOrCodeActionContainer?quickFixes = await client.RequestCodeAction(new CodeActionParams
                {
                    TextDocument = new TextDocumentIdentifier(uri),
                    Range        = nonFixable.Span.ToRange(lineStarts)
                });

                // Assert.
                quickFixes.Should().NotBeNull();
                quickFixes.Should().BeEmpty();
            }
        }
Ejemplo n.º 6
0
        public async Task HoveringOverSymbolReferencesAndDeclarationsShouldProduceHovers(DataSet dataSet)
        {
            var(compilation, _, fileUri) = await dataSet.SetupPrerequisitesAndCreateCompilation(TestContext);

            var uri    = DocumentUri.From(fileUri);
            var client = await IntegrationTestHelper.StartServerWithTextAsync(this.TestContext, dataSet.Bicep, uri, creationOptions : new LanguageServer.Server.CreationOptions(NamespaceProvider: BicepTestConstants.NamespaceProvider, FileResolver: BicepTestConstants.FileResolver));

            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)
                    {
Ejemplo n.º 7
0
        // If the insertion path already exists, or can't be added (eg array instead of object exists on the path), returns null
        public (int line, int column, string insertText)? InsertIfNotExist(string[] propertyPaths, object valueIfNotExist)
        {
            if (propertyPaths.Length == 0)
            {
                throw new ArgumentException($"{nameof(propertyPaths)} must not be empty");
            }

            if (string.IsNullOrWhiteSpace(_json))
            {
                return(AppendToEndOfJson(Stringify(PropertyPathToObject(propertyPaths, valueIfNotExist))));
            }

            TextReader textReader = new StringReader(_json);
            JsonReader jsonReader = new JsonTextReader(textReader);

            JObject?jObject = null;

            try
            {
                jObject = JObject.Load(jsonReader, new JsonLoadSettings
                {
                    LineInfoHandling = LineInfoHandling.Load,
                    CommentHandling  = CommentHandling.Load,
                    DuplicatePropertyNameHandling = DuplicatePropertyNameHandling.Ignore,
                });
            }
            catch (Exception)
            {
            }

            if (jObject is null)
            {
                // Just append to existing text
                return(AppendToEndOfJson(Stringify(PropertyPathToObject(propertyPaths, valueIfNotExist))));
            }

            JObject?      currentObject  = jObject;
            List <string> remainingPaths = new(propertyPaths);

            while (remainingPaths.Count > 0)
            {
                string path      = PopFromLeft(remainingPaths);
                JToken?nextLevel = currentObject[path];
                if (nextLevel is null)
                {
                    int    line           = ((IJsonLineInfo)currentObject).LineNumber - 1; // 1-indexed to 0-indexed
                    int    column         = ((IJsonLineInfo)currentObject).LinePosition - 1;
                    object insertionValue = valueIfNotExist;
                    remainingPaths.Reverse();
                    foreach (string propertyName in remainingPaths)
                    {
                        Dictionary <string, object> newObject = new();
                        newObject[propertyName] = insertionValue;
                        insertionValue          = newObject;
                    }
                    remainingPaths.Reverse();
                    string newPath = string.Join('.', remainingPaths);
                    string insertionValueAsString = Stringify(insertionValue);

                    int  insertLine;
                    int  insertColumn;
                    int  currentIndent;
                    bool hasSiblings = currentObject.Children().Any(child => child.Type != JTokenType.Comment);
                    insertLine    = line;
                    insertColumn  = column + 1;
                    currentIndent = GetIndentationOfLine(line) + _indent; // use indent of line with the starting "{" as the nested indent level

                    // We will insert before the first sibling
                    string propertyInsertion =
                        "\n" +
                        IndentEachLine(
                            $"\"{path}\": {insertionValueAsString}",
                            currentIndent);
                    if (hasSiblings)
                    {
                        propertyInsertion += ",";
                    }

                    // Need a newline after the insertion if there's anything else on the line
                    var  lineStarts         = TextCoordinateConverter.GetLineStarts(_json);
                    int  offset             = TextCoordinateConverter.GetOffset(lineStarts, line, column);
                    char?charAfterInsertion = _json.Length > offset ? _json[offset + 1] : null;
                    if (charAfterInsertion != '\n' && charAfterInsertion != '\r')
                    {
                        propertyInsertion += '\n';
                    }

                    return(insertLine, insertColumn, propertyInsertion);
                }

                if (remainingPaths.Count == 0)
                {
                    // We found matches all the way to the leaf, doesn't matter what the leaf value is, we will leave it alone
                    return(null);
                }

                if (nextLevel is JObject nextObject)
                {
                    currentObject = nextObject;
                }
                else
                {
                    return(null);
                }
            }

            return(null);
        }
Ejemplo n.º 8
0
 public JsonEditor(string json, int indent = 2)
 {
     _json       = json;
     _indent     = indent;
     _lineStarts = TextCoordinateConverter.GetLineStarts(json);
 }
Ejemplo n.º 9
0
        public static string PrintWithAnnotations(BicepFile bicepFile, IEnumerable <Annotation> annotations, int context, bool includeLineNumbers)
        {
            if (!annotations.Any())
            {
                return("");
            }

            var output       = new StringBuilder();
            var programLines = GetProgramTextLines(bicepFile);

            var annotationPositions = annotations.ToDictionary(
                x => x,
                x => TextCoordinateConverter.GetPosition(bicepFile.LineStarts, x.Span.Position));

            var annotationsByLine = annotationPositions.ToLookup(x => x.Value.line, x => x.Key);

            var minLine = annotationPositions.Values.Aggregate(int.MaxValue, (min, curr) => Math.Min(curr.line, min));
            var maxLine = annotationPositions.Values.Aggregate(0, (max, curr) => Math.Max(curr.line, max)) + 1;

            minLine = Math.Max(0, minLine - context);
            maxLine = Math.Min(bicepFile.LineStarts.Length, maxLine + context);
            var digits = maxLine.ToString().Length;

            for (var i = minLine; i < maxLine; i++)
            {
                var gutterOffset = 0;
                if (includeLineNumbers)
                {
                    var lineNumber = i + 1; // to match VSCode's line numbering (starting at 1)
                    output.Append(lineNumber.ToString().PadLeft(digits, '0'));
                    output.Append("| ");

                    gutterOffset = digits + 2;
                }
                output.Append(programLines[i]);
                output.Append('\n');

                var annotationsToDisplay = annotationsByLine[i].OrderBy(x => annotationPositions[x].character);
                foreach (var annotation in annotationsToDisplay)
                {
                    var position = annotationPositions[annotation];
                    output.Append(new String(' ', gutterOffset + position.character));

                    switch (annotation.Span.Length)
                    {
                    case 0:
                        output.Append("^");
                        break;

                    case int x:
                        // TODO handle annotation spanning multiple lines
                        output.Append(new String('~', x));
                        break;
                    }

                    output.Append(" ");
                    output.Append(annotation.Message);
                    output.Append('\n');
                }
            }

            return(output.ToString());
        }
Ejemplo n.º 10
0
        public async Task VerifyResourceBodyCompletionWithDiscriminatedObjectTypeContainsRequiredPropertiesSnippet()
        {
            string text       = @"resource deploymentScripts 'Microsoft.Resources/deploymentScripts@2020-10-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.InsertTextFormat.Should().Be(InsertTextFormat.Snippet);
                c.Label.Should().Be("required-properties-AzureCLI");
                c.Detail.Should().Be("Required properties");
                c.TextEdit?.NewText?.Should().BeEquivalentToIgnoringNewlines(@"{
	name: $1
	location: $2
	kind: 'AzureCLI'
	properties: {
		azCliVersion: $3
		retentionInterval: $4
	}
	$0
}");
            },
                c =>
            {
                c.Label.Should().Be("required-properties-AzurePowerShell");
                c.Detail.Should().Be("Required properties");
                c.TextEdit?.NewText?.Should().BeEquivalentToIgnoringNewlines(@"{
	name: $1
	location: $2
	kind: 'AzurePowerShell'
	properties: {
		azPowerShellVersion: $3
		retentionInterval: $4
	}
	$0
}");
            },
                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");
            });
        }
Ejemplo n.º 11
0
        public void GetPosition_EmptyLineStarts_ThrowsArgumentException()
        {
            Action sut = () => TextCoordinateConverter.GetPosition(new List <int>().AsReadOnly(), 10);

            sut.Should().Throw <ArgumentException>().WithMessage("*must not be empty*");
        }