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
98 changes: 98 additions & 0 deletions CosmosDBShell.Tests/Parser/CultureInvariantConversionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// ------------------------------------------------------------

namespace CosmosShell.Tests.Parser;

using System.Globalization;
using System.Threading;
using System.Threading.Tasks;

using Azure.Data.Cosmos.Shell.Core;
using Azure.Data.Cosmos.Shell.Parser;

[Collection(CosmosShell.Tests.Shell.ThemeStateTestCollection.Name)]
public class CultureInvariantConversionTests
{
[Fact]
public void ShellText_DecimalConversion_UsesInvariantCulture()
{
var value = WithGermanCulture(() =>
Assert.IsType<double>(new ShellText("1.5").ConvertShellObject(DataType.Decimal)));

Assert.Equal(1.5d, value);
}

[Fact]
public void ShellText_DecimalConversion_RejectsLocalizedDecimalSeparator()
{
WithGermanCulture(() =>
Assert.Throws<InvalidOperationException>(() => new ShellText("1,5").ConvertShellObject(DataType.Decimal)));
}

[Fact]
public void ShellIdentifier_DecimalConversion_UsesInvariantCulture()
{
var value = WithGermanCulture(() =>
Assert.IsType<double>(new ShellIdentifier("1.5").ConvertShellObject(DataType.Decimal)));

Assert.Equal(1.5d, value);

WithGermanCulture(() =>
Assert.Throws<InvalidOperationException>(() => new ShellIdentifier("1,5").ConvertShellObject(DataType.Decimal)));
}

[Fact]
public async Task ExpressionEvaluation_TextDecimalArithmetic_UsesInvariantCulture()
{
var result = await WithGermanCultureAsync(() => EvaluateExpressionAsync("\"1.5\" * 2.0"));
var value = Assert.IsType<ShellDecimal>(result);

Assert.Equal(3.0d, value.Value);
}

private static T WithGermanCulture<T>(Func<T> action)
{
var originalCulture = CultureInfo.CurrentCulture;
var originalUICulture = CultureInfo.CurrentUICulture;

try
{
var culture = CultureInfo.GetCultureInfo("de-DE");
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
return action();
}
finally
{
CultureInfo.CurrentCulture = originalCulture;
CultureInfo.CurrentUICulture = originalUICulture;
}
}

private static async Task<T> WithGermanCultureAsync<T>(Func<Task<T>> action)
{
var originalCulture = CultureInfo.CurrentCulture;
var originalUICulture = CultureInfo.CurrentUICulture;

try
{
var culture = CultureInfo.GetCultureInfo("de-DE");
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
return await action();
}
finally
{
CultureInfo.CurrentCulture = originalCulture;
CultureInfo.CurrentUICulture = originalUICulture;
}
}

private static async Task<ShellObject> EvaluateExpressionAsync(string input)
{
var expression = new ExpressionParser(new Lexer(input)).ParseFilterExpression();
using var shell = ShellInterpreter.CreateInstance();
return await expression.EvaluateAsync(shell, new CommandState(), CancellationToken.None);
}
}
50 changes: 50 additions & 0 deletions CosmosDBShell.Tests/Parser/ExpressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,56 @@ public void EvaluateExpression_JsonObject_ReturnsShellJson()
Assert.Equal(30, json.Value.GetProperty("age").GetInt32());
}

[Fact]
public void EvaluateExpression_JsonObjectWithLargeIntegers_PreservesRawIntegerText()
{
var result = EvaluateExpression("{ \"ts\": 1727600000000, \"neg\": -1727600000000 }");
var json = Assert.IsType<ShellJson>(result);
Assert.Equal("1727600000000", json.Value.GetProperty("ts").GetRawText());
Assert.Equal("-1727600000000", json.Value.GetProperty("neg").GetRawText());
}

[Fact]
public void EvaluateExpression_JsonArrayWithLargeIntegers_PreservesRawIntegerText()
{
var result = EvaluateExpression("[2147483648, -1727600000000, 9007199254740993]");
var json = Assert.IsType<ShellJson>(result);
Assert.Equal("2147483648", json.Value[0].GetRawText());
Assert.Equal("-1727600000000", json.Value[1].GetRawText());
Assert.Equal("9007199254740993", json.Value[2].GetRawText());
}

[Fact]
public void EvaluateExpression_NestedJsonObjectWithLargeInteger_PreservesRawIntegerText()
{
var result = EvaluateExpression("{ \"outer\": { \"n\": 9007199254740993 } }");
var json = Assert.IsType<ShellJson>(result);
Assert.Equal("9007199254740993", json.Value.GetProperty("outer").GetProperty("n").GetRawText());
}

[Fact]
public async Task EvaluateExpression_LargeJsonIntegerPropertyArithmetic_UsesDecimalArithmetic()
{
var objectResult = Assert.IsType<ShellJson>(await EvaluateExpressionAsync("{ \"n\": 3000000000 }"));
var interpreter = new ShellInterpreter();
interpreter.SetVariable("o", objectResult);

var result = await ParseExpression("$o.n + 1").EvaluateAsync(interpreter, new CommandState(), CancellationToken.None);
var number = Assert.IsType<ShellDecimal>(result);
Assert.Equal(3000000001.0, number.Value);
}

[Fact]
public void ParseExpression_LargeIntegerOutsideJson_ReportsDocumentedError()
{
var lexer = new Lexer("2147483648");
var parser = new ExpressionParser(lexer);
var expression = parser.ParseExpression();

Assert.IsType<ErrorExpression>(expression);
Assert.Contains(lexer.Errors, error => error.Message == MessageService.GetArgsString("expression_error_invalid_number", "value", "2147483648"));
}

[Fact]
public void EvaluateExpression_JsonArray_ReturnsShellJson()
{
Expand Down
93 changes: 87 additions & 6 deletions CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/ExpressionParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

namespace Azure.Data.Cosmos.Shell.Parser;

using System.Globalization;
using System.Text.Json;

using Azure.Data.Cosmos.Shell.ArgumentParser;
using Azure.Data.Cosmos.Shell.Util;

Expand Down Expand Up @@ -673,7 +676,7 @@ private Expression ParsePrimaryCore()

this.Advance();

if (int.TryParse(token.Value, out int intValue))
if (int.TryParse(token.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int intValue))
{
return new ConstantExpression(token, new ShellNumber(intValue));
}
Expand All @@ -693,7 +696,7 @@ private Expression ParsePrimaryCore()

this.Advance();

if (double.TryParse(token.Value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double doubleValue))
if (double.TryParse(token.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double doubleValue))
{
return new ConstantExpression(token, new ShellDecimal(doubleValue));
}
Expand Down Expand Up @@ -1233,7 +1236,7 @@ void SkipTrivia()
}

// Parse property value as a full expression
var value = this.ParsePipeExpression();
var value = this.ParseJsonValueExpression();

// Add to properties if key valid
JsonProperty? propertyNode = null;
Expand Down Expand Up @@ -1362,7 +1365,7 @@ private Expression ParseJsonArray()
}

// Parse next element as a full expression
var expr = this.ParsePipeExpression();
var expr = this.ParseJsonValueExpression();
elements.Add(expr);

this.SkipWhitespace();
Expand Down Expand Up @@ -1430,6 +1433,84 @@ private Expression ParseJsonArray()
return new JsonArrayExpression(lbracket, synthetic, elements, commaTokens);
}

private Expression ParseJsonValueExpression()
{
if (this.TryParseLargeJsonIntegerLiteral(out var expression))
{
return expression!;
}

return this.ParsePipeExpression();
}

private bool TryParseLargeJsonIntegerLiteral(out Expression? expression)
{
expression = null;
if (this.aborted)
{
return false;
}

if (this.Check(TokenType.Number))
{
var token = this.Current;
if (token == null || int.TryParse(token.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
{
return false;
}

expression = this.TryCreateJsonIntegerConstant(token, token.Value);
if (expression == null)
{
return false;
}

this.Advance();
return true;
}

if (this.Check(TokenType.Minus))
{
var minusToken = this.Current;
var numberToken = this.Peek();
if (minusToken == null ||
numberToken?.Type != TokenType.Number ||
int.TryParse(numberToken.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
{
return false;
}

var value = "-" + numberToken.Value;
var token = new Token(TokenType.Number, value, minusToken.Start, numberToken.End - minusToken.Start);
expression = this.TryCreateJsonIntegerConstant(token, value);
if (expression == null)
{
return false;
}

this.Advance();
this.Advance();
return true;
}

return false;
}

private Expression? TryCreateJsonIntegerConstant(Token token, string value)
{
try
{
using var document = JsonDocument.Parse(value);
return document.RootElement.ValueKind == JsonValueKind.Number
? new ConstantExpression(token, new ShellJson(document.RootElement.Clone()))
: null;
}
catch (JsonException)
{
return null;
}
}

private void ReportError(string message, Token? token, TokenType? expected = null, int? position = null, ParseErrorKind kind = ParseErrorKind.Generic)
{
int start = position ?? token?.Start ?? (this.lastNonNullToken?.Start ?? 0);
Expand Down Expand Up @@ -1641,7 +1722,7 @@ private Expression ParseFilterPathExpression(Token firstToken)
}

var indexToken = this.Consume(TokenType.Number, MessageService.GetString("expression_error_expected_array_index"));
int index = int.TryParse(indexToken.Value, out var parsedIndex) ? parsedIndex : 0;
int index = int.TryParse(indexToken.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedIndex) ? parsedIndex : 0;
var indexedCloseBracket = this.Consume(TokenType.CloseBracket, MessageService.GetString("expression_error_expected_close_bracket"));
var indexQuestionToken = this.TryConsumeQuestion();
end = indexedCloseBracket.Start + indexedCloseBracket.Length;
Expand Down Expand Up @@ -1745,4 +1826,4 @@ public override void Visit(JsonExpression jsonExpression)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Azure.Data.Cosmos.Shell.Parser;

using System.Globalization;
using System.Text.Json;

using Azure.Data.Cosmos.Shell.Core;
Expand Down Expand Up @@ -49,14 +50,14 @@ internal static object EvaluateString(DataType type, string value)

throw new InvalidOperationException(MessageService.GetArgsString("conversion-error-identifier-boolean", "value", value));
case DataType.Number:
if (int.TryParse(value, out int intValue))
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int intValue))
{
return intValue;
}

throw new InvalidOperationException(MessageService.GetArgsString("conversion-error-identifier-number", "value", value));
case DataType.Decimal:
if (double.TryParse(value, out double decimalValue))
if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out double decimalValue))
{
return decimalValue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Azure.Data.Cosmos.Shell.Parser;

using System.Globalization;
using System.Text.Json;

using Azure.Data.Cosmos.Shell.Core;
Expand Down Expand Up @@ -80,7 +81,7 @@ public override object ConvertShellObject(DataType type)
if (this.Value.ValueKind == JsonValueKind.String)
{
var strValue = this.Value.GetString() ?? string.Empty;
if (int.TryParse(strValue, out int intValue))
if (int.TryParse(strValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out int intValue))
{
return intValue;
}
Expand All @@ -96,7 +97,7 @@ public override object ConvertShellObject(DataType type)
if (this.Value.ValueKind == JsonValueKind.String)
{
var strValue = this.Value.GetString() ?? string.Empty;
if (decimal.TryParse(strValue, out decimal decimalValue))
if (decimal.TryParse(strValue, NumberStyles.Float, CultureInfo.InvariantCulture, out decimal decimalValue))
{
return decimalValue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public static ShellObject Parse(Lexer lexer)
}

// Check if it's a number
if (int.TryParse(token.Value, out int intValue))
if (int.TryParse(token.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int intValue))
{
return new ShellNumber(intValue);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Azure.Data.Cosmos.Shell.Parser;

using System.Globalization;
using System.Text.Json;

using Azure.Data.Cosmos.Shell.Core;
Expand Down Expand Up @@ -58,14 +59,14 @@ internal static object EvaluateString(DataType type, string value)

throw new InvalidOperationException(MessageService.GetArgsString("conversion-error-text-boolean", "value", value));
case DataType.Number:
if (int.TryParse(value, out int intValue))
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int intValue))
{
return intValue;
}

throw new InvalidOperationException(MessageService.GetArgsString("conversion-error-text-number", "value", value));
case DataType.Decimal:
if (double.TryParse(value, out double decimalValue))
if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out double decimalValue))
{
return decimalValue;
}
Expand Down
Loading
Loading