Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#nullable enable

using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Text;

namespace Microsoft.DotNet.Interactive.Http.Parsing;
internal class HttpEscapedCharacterSequenceNode : HttpSyntaxNode
{
public HttpEscapedCharacterSequenceNode(SourceText sourceText, HttpSyntaxTree syntaxTree) : base(sourceText, syntaxTree)
{
}

public string UnescapedText => Text.TrimStart('\\');

}
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,19 @@ private static void AddCommentsIfAny(
if (node is null)
{
if (CurrentToken is
{ Kind: TokenKind.Word } or
{ Kind: TokenKind.Punctuation } and ({ Text: "/" } or { Text: "'" } or { Text: "\"" }))
{ Kind: TokenKind.Word } or
{ Kind: TokenKind.Punctuation } and ({ Text: "/" } or { Text: "'" } or { Text: "\"" }))
{
node = new HttpVariableValueNode(_sourceText, _syntaxTree);

ParseLeadingWhitespaceAndComments(node);
}
else if (IsStartOfEscapeSequence())
{
node = new HttpVariableValueNode(_sourceText, _syntaxTree);
var escapedSequence = ParseEscapeSequence();
node.Add(escapedSequence);
}
else if (IsAtStartOfEmbeddedExpression())
{
node = new HttpVariableValueNode(_sourceText, _syntaxTree);
Expand All @@ -164,6 +170,10 @@ private static void AddCommentsIfAny(
{
node.Add(ParseEmbeddedExpression());
}
else if (IsStartOfEscapeSequence())
{
node.Add(ParseEscapeSequence());
}
else
{
ConsumeCurrentTokenInto(node);
Expand Down Expand Up @@ -541,6 +551,11 @@ private bool IsAtStartOfEmbeddedExpression() =>
CurrentToken is { Text: "{" } &&
CurrentTokenPlus(1) is { Text: "{" };

private bool IsStartOfEscapeSequence() =>
CurrentToken is { Kind: TokenKind.Punctuation } and { Text: @"\" } &&
(CurrentTokenPlus(1) is { Kind: TokenKind.Punctuation } and { Text: "{" } &&
CurrentTokenPlus(2) is { Kind: TokenKind.Punctuation } and { Text: "{" });

private HttpEmbeddedExpressionNode ParseEmbeddedExpression()
{
var node = new HttpEmbeddedExpressionNode(_sourceText, _syntaxTree);
Expand Down Expand Up @@ -889,6 +904,17 @@ private HttpCommentStartNode ParseCommentStart()
return node;
}

private HttpEscapedCharacterSequenceNode ParseEscapeSequence()
{
var node = new HttpEscapedCharacterSequenceNode(_sourceText, _syntaxTree);

ConsumeCurrentTokenInto(node); // parse the first \
ConsumeCurrentTokenInto(node); // parse the first { or }
ConsumeCurrentTokenInto(node); // parse the second { or }

return ParseTrailingWhitespace(node);
}

private bool IsComment()
{
if (MoreTokens() && !IsRequestSeparator())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.DotNet.Interactive.Parsing;
using System.Text;

namespace Microsoft.DotNet.Interactive.Http.Parsing;

Expand Down Expand Up @@ -53,51 +54,71 @@ public void Add(HttpRequestSeparatorNode separatorNode)
if (node.ValueNode is not null && node.DeclarationNode is not null)
{
var embeddedExpressionNodes = node.ValueNode.ChildNodes.OfType<HttpEmbeddedExpressionNode>();
if (!embeddedExpressionNodes.Any())
var potentialEscapedCharacters = node.ValueNode.ChildNodes.OfType<HttpEscapedCharacterSequenceNode>();
if (potentialEscapedCharacters.Any())
{
foundVariableValues[node.DeclarationNode.VariableName] = node.ValueNode.Text;
declaredVariables[node.DeclarationNode.VariableName] = new DeclaredVariable(node.DeclarationNode.VariableName, node.ValueNode.Text, HttpBindingResult<string>.Success(Text));
}
else
{
var value = node.ValueNode.TryGetValue(node =>
StringBuilder sb = new StringBuilder();
foreach (var child in node.ValueNode.ChildNodesAndTokens)
{
if (foundVariableValues.TryGetValue(node.Text, out string? stringValue))
if (child is HttpEscapedCharacterSequenceNode sequenceNode)
{
return node.CreateBindingSuccess(stringValue);
}
else if (bind != null)
{
return bind(node);
sb.Append(sequenceNode.UnescapedText);
}
else
{
return DynamicExpressionUtilities.ResolveExpressionBinding(node, node.Text);
sb.Append(child.Text);
}
}

var value = sb.ToString();
foundVariableValues[node.DeclarationNode.VariableName] = value;
declaredVariables[node.DeclarationNode.VariableName] = new DeclaredVariable(node.DeclarationNode.VariableName, value, HttpBindingResult<string>.Success(Text));
}
else if (!embeddedExpressionNodes.Any())
{
foundVariableValues[node.DeclarationNode.VariableName] = node.ValueNode.Text;
declaredVariables[node.DeclarationNode.VariableName] = new DeclaredVariable(node.DeclarationNode.VariableName, node.ValueNode.Text, HttpBindingResult<string>.Success(Text));
}
else
{
var value = node.ValueNode.TryGetValue(node =>
{
if (foundVariableValues.TryGetValue(node.Text, out string? stringValue))
{
return node.CreateBindingSuccess(stringValue);
}
else if (bind != null)
{
return bind(node);
}
else
{
return DynamicExpressionUtilities.ResolveExpressionBinding(node, node.Text);
}

});
});

if (value?.Value != null)
if (value?.Value != null)
{
declaredVariables[node.DeclarationNode.VariableName] = new DeclaredVariable(node.DeclarationNode.VariableName, value.Value, value);
}
else
{
if(diagnostics is null)
{
declaredVariables[node.DeclarationNode.VariableName] = new DeclaredVariable(node.DeclarationNode.VariableName, value.Value, value);
}
else
diagnostics = value?.Diagnostics;
}
else
{
if(diagnostics is null)
{
diagnostics = value?.Diagnostics;
}
else
if (value is not null)
{
if (value is not null)
{
diagnostics.AddRange(value.Diagnostics);
}

}
diagnostics.AddRange(value.Diagnostics);
}

}
}
}
}
}

return (declaredVariables, diagnostics);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ internal HttpVariableValueNode(SourceText sourceText, HttpSyntaxTree syntaxTree)

public void Add(HttpEmbeddedExpressionNode node) => AddInternal(node);

public void Add(HttpEscapedCharacterSequenceNode node) => AddInternal(node);

public HttpBindingResult<string> TryGetValue(HttpBindingDelegate bind) => this.BindByInterpolation(bind);
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ public static IEnumerable<object[]> GenerateValidRequests()
var generationNumber = 0;

foreach(var namedRequest in ValidNamedRequests())
foreach (var variables in ValidVariableDeclarations())
foreach (var method in ValidMethods())
foreach (var url in ValidUrls())
foreach (var version in ValidVersions())
Expand All @@ -115,7 +116,7 @@ public static IEnumerable<object[]> GenerateValidRequests()
++generationNumber;
yield return new object[]
{
new HttpRequestNodeSyntaxSpec(namedRequest, method, url, version, headerSection, bodySection),
new HttpRequestNodeSyntaxSpec(namedRequest, variables, method, url, version, headerSection, bodySection),
generationNumber
};
}
Expand All @@ -125,6 +126,7 @@ public static IEnumerable<object[]> GenerateValidRequestsWithExtraTrivia()
{
var generationNumber = 0;

foreach(var variables in ValidVariableDeclarations())
foreach (var namedRequest in ValidNamedRequests())
foreach (var method in ValidMethods())
foreach (var url in ValidUrls())
Expand All @@ -135,7 +137,7 @@ public static IEnumerable<object[]> GenerateValidRequestsWithExtraTrivia()
++generationNumber;
yield return new object[]
{
new HttpRequestNodeSyntaxSpec(namedRequest, method, url, version, headerSection, bodySection)
new HttpRequestNodeSyntaxSpec(namedRequest, variables, method, url, version, headerSection, bodySection)
{
Randomizer = new Random(1)
},
Expand All @@ -149,6 +151,7 @@ public static IEnumerable<object[]> GenerateInvalidRequests()
var generationNumber = 0;

foreach (var namedRequest in ValidNamedRequests())
foreach (var variables in ValidVariableDeclarations())
foreach (var method in InvalidMethods())
foreach (var url in ValidUrls())
foreach (var version in ValidVersions())
Expand All @@ -158,12 +161,13 @@ public static IEnumerable<object[]> GenerateInvalidRequests()
++generationNumber;
yield return new object[]
{
new HttpRequestNodeSyntaxSpec(namedRequest, method, url, version, headerSection, bodySection),
new HttpRequestNodeSyntaxSpec(namedRequest, variables, method, url, version, headerSection, bodySection),
generationNumber
};
}

foreach (var namedRequest in ValidNamedRequests())
foreach (var variables in ValidVariableDeclarations())
foreach (var method in ValidMethods())
foreach (var url in InvalidUrls())
foreach (var version in ValidVersions())
Expand All @@ -173,12 +177,13 @@ public static IEnumerable<object[]> GenerateInvalidRequests()
++generationNumber;
yield return new object[]
{
new HttpRequestNodeSyntaxSpec(namedRequest, method, url, version, headerSection, bodySection),
new HttpRequestNodeSyntaxSpec(namedRequest, variables, method, url, version, headerSection, bodySection),
generationNumber
};
}

foreach(var namedRequest in ValidNamedRequests())
foreach (var variables in ValidVariableDeclarations())
foreach (var method in ValidMethods())
foreach (var url in ValidUrls())
foreach (var version in InvalidVersions())
Expand All @@ -188,12 +193,13 @@ public static IEnumerable<object[]> GenerateInvalidRequests()
++generationNumber;
yield return new object[]
{
new HttpRequestNodeSyntaxSpec(namedRequest, method, url, version, headerSection, bodySection),
new HttpRequestNodeSyntaxSpec(namedRequest, variables, method, url, version, headerSection, bodySection),
generationNumber
};
}

foreach(var namedRequest in ValidNamedRequests())
foreach (var variables in ValidVariableDeclarations())
foreach (var method in ValidMethods())
foreach (var url in ValidUrls())
foreach (var version in ValidVersions())
Expand All @@ -203,12 +209,14 @@ public static IEnumerable<object[]> GenerateInvalidRequests()
++generationNumber;
yield return new object[]
{
new HttpRequestNodeSyntaxSpec(namedRequest, method, url, version, headerSection, bodySection),
new HttpRequestNodeSyntaxSpec(namedRequest, variables, method, url, version, headerSection, bodySection),
generationNumber
};
}


foreach (var namedRequest in InvalidNamedRequests())
foreach (var variables in ValidVariableDeclarations())
foreach (var method in ValidMethods())
foreach (var url in ValidUrls())
foreach (var version in ValidVersions())
Expand All @@ -218,11 +226,12 @@ public static IEnumerable<object[]> GenerateInvalidRequests()
++generationNumber;
yield return new object[]
{
new HttpRequestNodeSyntaxSpec(namedRequest, method, url, version, headerSection, bodySection),
new HttpRequestNodeSyntaxSpec(namedRequest, variables, method, url, version, headerSection, bodySection),
generationNumber
};
}
}
}

private static IEnumerable<HttpMethodNodeSyntaxSpec> ValidMethods()
{
Expand Down Expand Up @@ -355,5 +364,64 @@ with XML.</description>
.Should().BeEquivalentTo("numberValue", "stringValue");
});
}

private static IEnumerable<HttpVariableDeclarationAndAssignmentNodeSyntaxSpec> ValidVariableDeclarations()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might not be necessary to add so many cases. The goal of these tests is generally not to test every detail of the feature. The unit tests already cover that. They tend to be useful for catching issues with different combinations of syntax (e.g. does the parser move correctly from one node type to another) as well as incomplete syntax (e.g. does the parser handle it gracefully when the user is typing and the syntax is malformed in various normal ways.)

{
yield return null;

yield return new HttpVariableDeclarationAndAssignmentNodeSyntaxSpec("@host=localhost");

yield return new HttpVariableDeclarationAndAssignmentNodeSyntaxSpec("@host=https://example.com");

yield return new HttpVariableDeclarationAndAssignmentNodeSyntaxSpec("@api_key=secret123");

yield return new HttpVariableDeclarationAndAssignmentNodeSyntaxSpec("@base_url=https://{{host}}/api", node =>
{
node.ValueNode.DescendantNodesAndTokens().OfType<HttpEmbeddedExpressionNode>()
.Should().ContainSingle()
.Which.ExpressionNode.Text.Should().Be("host");
});

yield return new HttpVariableDeclarationAndAssignmentNodeSyntaxSpec("@escaped=\\{\\{text\\}\\}", node =>
{
node.ValueNode.Text.Should().Be("{{text}}");
});

yield return new HttpVariableDeclarationAndAssignmentNodeSyntaxSpec("@with_spaces=one two three");

yield return new HttpVariableDeclarationAndAssignmentNodeSyntaxSpec("@quoted=\"hello world\"");

yield return new HttpVariableDeclarationAndAssignmentNodeSyntaxSpec("@single_quoted='hello world'");

yield return new HttpVariableDeclarationAndAssignmentNodeSyntaxSpec("@user.name=john_doe");

yield return new HttpVariableDeclarationAndAssignmentNodeSyntaxSpec("@dynamic={{$guid}}", node =>
{
node.ValueNode.DescendantNodesAndTokens().OfType<HttpEmbeddedExpressionNode>()
.Should().ContainSingle()
.Which.ExpressionNode.Text.Should().Be("$guid");
});
}

private static IEnumerable<HttpVariableDeclarationAndAssignmentNodeSyntaxSpec> InvalidVariableDeclarations()
{
// Missing variable name
yield return new HttpVariableDeclarationAndAssignmentNodeSyntaxSpec("@=value");

// Variable name starting with number
yield return new HttpVariableDeclarationAndAssignmentNodeSyntaxSpec("@123invalid=value");

// Invalid character in variable name (hyphen)
yield return new HttpVariableDeclarationAndAssignmentNodeSyntaxSpec("@my-var=value");

// Space in variable name
yield return new HttpVariableDeclarationAndAssignmentNodeSyntaxSpec("@var name=value");

// Missing equals sign
yield return new HttpVariableDeclarationAndAssignmentNodeSyntaxSpec("@host value");

// Special characters in variable name
yield return new HttpVariableDeclarationAndAssignmentNodeSyntaxSpec("@host!name=value");
}
}
}

Loading