Skip to content

Commit 9af9490

Browse files
Simplify YAML/JSON parser code path with Result pattern (#18713)
###### Microsoft Reviewers: [Open in CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/Azure/bicep/pull/18713)
1 parent 24dbc7c commit 9af9490

8 files changed

Lines changed: 70 additions & 109 deletions

File tree

src/Bicep.Core.IntegrationTests/ModuleTests.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,6 @@ param inputb string
182182
});
183183
}
184184

185-
private delegate bool TryReadDelegate(Uri fileUri, out string? fileContents, out DiagnosticBuilder.DiagnosticBuilderDelegate? failureBuilder);
186-
187185
[TestMethod]
188186
public void SourceFileGroupingBuilder_build_should_throw_diagnostic_exception_if_entrypoint_file_read_fails()
189187
{

src/Bicep.Core.UnitTests/Semantics/ObjectDeserializationTests.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public void JSON_file_content_gets_deserialized_into_JSON()
9595
private static void CompareSimpleJSON(string json)
9696
{
9797
var arguments = new FunctionArgumentSyntax[4];
98-
new YamlObjectParser().TryExtractFromObject(json, null, [arguments[0]], out var errorDiagnostic, out JToken? jToken);
98+
new YamlObjectParser().TryExtractFromObject(json, null, [arguments[0]]).IsSuccess(out var jToken);
9999
var correctList = new List<int> { 1, 2 };
100100
var correctObject = new Dictionary<string, int> { { "nestedInt", 1 }, };
101101

@@ -133,7 +133,7 @@ public void Unparsable_YAML()
133133
- 2";
134134

135135
var span = new TextSpan(0, 10 - 0);
136-
new YamlObjectParser().TryExtractFromObject(invalidYml, null, [span], out var errorDiagnostic, out JToken? jToken);
136+
new YamlObjectParser().TryExtractFromObject(invalidYml, null, [span]).IsSuccess(out _, out var errorDiagnostic);
137137
Assert.AreEqual(errorDiagnostic!.Code, "BCP340");
138138
}
139139

@@ -156,7 +156,7 @@ public void Unparsable_JSON()
156156
- 2";
157157

158158
var span = new TextSpan(0, 10 - 0);
159-
new JsonObjectParser().TryExtractFromObject(invalidJson, null, [span], out var errorDiagnostic, out JToken? jToken);
159+
new JsonObjectParser().TryExtractFromObject(invalidJson, null, [span]).IsSuccess(out _, out var errorDiagnostic);
160160
Assert.AreEqual(errorDiagnostic!.Code, "BCP186");
161161
}
162162

@@ -165,7 +165,7 @@ public void Complex_JSON_gets_deserialized_into_JSON()
165165
{
166166
var json = COMPLEX_JSON;
167167
var arguments = new FunctionArgumentSyntax[4];
168-
new YamlObjectParser().TryExtractFromObject(json, null, [arguments[0]], out var errorDiagnostic, out JToken? jToken);
168+
new YamlObjectParser().TryExtractFromObject(json, null, [arguments[0]]).IsSuccess(out var jToken);
169169
var expectedValue = "```bicep\ndateTimeFromEpoch([epochTime: int]): string\n\n```\nConverts an epoch time integer value to an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) dateTime string.\n";
170170
Assert.AreEqual(expectedValue, jToken!["documentation"]!["value"]);
171171
}
@@ -188,7 +188,7 @@ public void Simple_YAML_file_content_gets_deserialized_into_JSON()
188188
zip: 99970";
189189

190190
var arguments = new FunctionArgumentSyntax[4];
191-
new YamlObjectParser().TryExtractFromObject(yml, null, [arguments[0]], out var errorDiagnostic, out JToken? jToken);
191+
new YamlObjectParser().TryExtractFromObject(yml, null, [arguments[0]]).IsSuccess(out var jToken);
192192

193193
Assert.AreEqual("George Washington", jToken!["name"]);
194194
Assert.AreEqual("400", jToken["addresses"]!["home"]!["street"]!["house_number"]);

src/Bicep.Core/Semantics/IObjectParser.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@
55
using Bicep.Core.Text;
66
using Newtonsoft.Json.Linq;
77

8-
namespace Bicep.Core.Semantics
8+
namespace Bicep.Core.Semantics;
9+
10+
public interface IObjectParser
911
{
10-
public interface IObjectParser
11-
{
12-
bool TryExtractFromObject(string fileContent, string? tokenSelectorPath, IPositionable[] positionable, [NotNullWhen(false)] out IDiagnostic? errorDiagnostic, [NotNullWhen(true)] out JToken? newToken);
13-
}
12+
ResultWithDiagnostic<JToken> TryExtractFromObject(string fileContent, string? tokenSelectorPath, IPositionable[] positionable);
1413
}

src/Bicep.Core/Semantics/JsonObjectParser.cs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,17 @@
55
using Microsoft.WindowsAzure.ResourceStack.Common.Json;
66
using Newtonsoft.Json.Linq;
77

8-
namespace Bicep.Core.Semantics
8+
namespace Bicep.Core.Semantics;
9+
10+
public class JsonObjectParser : ObjectParser
911
{
10-
public class JsonObjectParser : ObjectParser
12+
protected override ResultWithDiagnostic<JToken> ExtractTokenFromObject(string fileContent, IPositionable positionable)
1113
{
12-
/// <summary>
13-
/// TryFromJson returns null if the fileContent is not a valid JSON object
14-
/// </summary>
15-
override protected JToken ExtractTokenFromObject(string fileContent)
16-
=> fileContent.TryFromJson<JToken>();
17-
override protected Diagnostic GetExtractTokenErrorType(IPositionable positionable)
18-
=> DiagnosticBuilder.ForPosition(positionable).UnparsableJsonType();
14+
if (fileContent.TryFromJson<JToken>() is {} jToken)
15+
{
16+
return new(jToken);
17+
}
18+
19+
return new(DiagnosticBuilder.ForPosition(positionable).UnparsableJsonType());
1920
}
2021
}

src/Bicep.Core/Semantics/Namespaces/SystemNamespaceType.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1431,7 +1431,7 @@ private static FunctionResult LoadContentResultBuilder(ObjectParser objectParser
14311431

14321432
if (TryLoadTextContentFromFile(model, diagnostics, (arguments[0], argumentTypes[0]), arguments.Length > 2 ? (arguments[2], argumentTypes[2]) : null, characterLimit)
14331433
.IsSuccess(out var result, out var errorDiagnostic) &&
1434-
objectParser.TryExtractFromObject(result.Content, tokenSelectorPath, positionables, out errorDiagnostic, out var token))
1434+
objectParser.TryExtractFromObject(result.Content, tokenSelectorPath, positionables).IsSuccess(out var token, out errorDiagnostic))
14351435
{
14361436
return new(ConvertJsonToBicepType(token), ConvertJsonToExpression(token));
14371437
}

src/Bicep.Core/Semantics/ObjectParser.cs

Lines changed: 25 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,76 +3,43 @@
33
using System.Diagnostics.CodeAnalysis;
44
using Bicep.Core.Diagnostics;
55
using Bicep.Core.Text;
6+
using Bicep.Core.Utils;
67
using Newtonsoft.Json;
78
using Newtonsoft.Json.Linq;
89

9-
namespace Bicep.Core.Semantics
10+
namespace Bicep.Core.Semantics;
11+
12+
public abstract class ObjectParser : IObjectParser
1013
{
11-
public abstract class ObjectParser : IObjectParser
14+
public ResultWithDiagnostic<JToken> TryExtractFromObject(string fileContent, string? tokenSelectorPath, IPositionable[] positionable)
1215
{
13-
public bool TryExtractFromObject(string fileContent, string? tokenSelectorPath, IPositionable[] positionable, [NotNullWhen(false)] out IDiagnostic? errorDiagnostic, [NotNullWhen(true)] out JToken? newToken)
16+
var result = ExtractTokenFromObject(fileContent, positionable[0]);
17+
if (result.IsSuccess(out var newToken) && tokenSelectorPath is {})
1418
{
15-
errorDiagnostic = null;
16-
newToken = this.ExtractTokenFromObject(fileContent);
17-
if (newToken is not { })
18-
{
19-
// Instead of catching and returning the parsing exception, we simply return a generic error.
20-
// This avoids having to deal with localization, and avoids possible confusion regarding line endings in the message.
21-
errorDiagnostic = this.GetExtractTokenErrorType(positionable[0]);
22-
return false;
23-
}
24-
if (tokenSelectorPath is not null)
25-
{
26-
return this.TryExtractFromTokenByPath(newToken, tokenSelectorPath, positionable[1], out errorDiagnostic, out newToken);
27-
}
28-
return true;
19+
return TryExtractFromTokenByPath(newToken, tokenSelectorPath, positionable[1]);
2920
}
30-
protected abstract JToken? ExtractTokenFromObject(string fileContent);
3121

32-
protected abstract Diagnostic GetExtractTokenErrorType(IPositionable positionable);
22+
return result;
23+
}
3324

34-
private bool TryExtractFromTokenByPath(JToken token, string tokenSelectorPath, IPositionable positionable, [NotNullWhen(false)] out IDiagnostic? errorDiagnostic, out JToken newToken)
35-
{
36-
newToken = token;
37-
errorDiagnostic = null;
38-
if (tokenSelectorPath is null)
39-
{
40-
return true;
41-
}
25+
protected abstract ResultWithDiagnostic<JToken> ExtractTokenFromObject(string fileContent, IPositionable positionable);
4226

43-
try
44-
{
45-
var selectTokens = token.SelectTokens(tokenSelectorPath, false).ToList();
27+
private static ResultWithDiagnostic<JToken> TryExtractFromTokenByPath(JToken token, string tokenSelectorPath, IPositionable positionable)
28+
{
29+
try
30+
{
31+
var selectTokens = token.SelectTokens(tokenSelectorPath, false).ToList();
4632

47-
switch (selectTokens.Count)
48-
{
49-
case 0:
50-
{
51-
errorDiagnostic = DiagnosticBuilder.ForPosition(positionable).NoJsonTokenOnPathOrPathInvalid();
52-
return false;
53-
}
54-
case 1:
55-
{
56-
newToken = selectTokens.First();
57-
break;
58-
}
59-
default:
60-
{
61-
newToken = new JArray(selectTokens);
62-
break;
63-
}
64-
}
65-
return true;
66-
}
67-
catch (JsonException)
33+
return selectTokens switch
6834
{
69-
//path is invalid or user hasn't finished typing it yet
70-
errorDiagnostic = DiagnosticBuilder.ForPosition(positionable).NoJsonTokenOnPathOrPathInvalid();
71-
return false;
72-
}
73-
74-
35+
[] => new(DiagnosticBuilder.ForPosition(positionable).NoJsonTokenOnPathOrPathInvalid()),
36+
[var singleToken] => new(singleToken),
37+
_ => new(new JArray(selectTokens))
38+
};
39+
}
40+
catch (JsonException)
41+
{
42+
return new(DiagnosticBuilder.ForPosition(positionable).NoJsonTokenOnPathOrPathInvalid());
7543
}
76-
7744
}
7845
}

src/Bicep.Core/Semantics/YamlObjectParser.cs

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,29 @@
66
using Newtonsoft.Json.Linq;
77
using SharpYaml.Serialization;
88

9-
namespace Bicep.Core.Semantics
9+
namespace Bicep.Core.Semantics;
10+
11+
public class YamlObjectParser : ObjectParser
1012
{
11-
public class YamlObjectParser : ObjectParser
13+
protected override ResultWithDiagnostic<JToken> ExtractTokenFromObject(string fileContent, IPositionable positionable)
1214
{
13-
/// <summary>
14-
/// Deserialize raises an exception if the fileContent is not a valid YAML object
15-
/// </summary>
16-
override protected JToken? ExtractTokenFromObject(string fileContent)
15+
if (TryDeserialize(fileContent) is { } deserialized)
1716
{
18-
try
19-
{
20-
return new Serializer().Deserialize(fileContent) is { } deserialized ? JToken.FromObject(deserialized) : null;
21-
}
22-
catch
23-
{
24-
return null;
25-
}
17+
return new(JToken.FromObject(deserialized));
2618
}
2719

28-
override protected Diagnostic GetExtractTokenErrorType(IPositionable positionable)
29-
=> DiagnosticBuilder.ForPosition(positionable).UnparsableYamlType();
20+
return new(DiagnosticBuilder.ForPosition(positionable).UnparsableYamlType());
21+
}
3022

23+
private static object? TryDeserialize(string fileContent)
24+
{
25+
try
26+
{
27+
return new Serializer().Deserialize(fileContent);
28+
}
29+
catch
30+
{
31+
return null;
32+
}
3133
}
3234
}

src/Bicep.Core/TypeSystem/ArmFunctionReturnTypeEvaluator.cs

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public static class ArmFunctionReturnTypeEvaluator
4343
args[i + prefixArgsArray.Length] = new(converted);
4444
}
4545

46-
if (EvaluateOperatorAsArmFunction(armFunctionName, out var result, out var builderFunc, args))
46+
if (EvaluateOperatorAsArmFunction(armFunctionName, args).IsSuccess(out var result, out var builderFunc))
4747
{
4848
if (TypeHelper.TryCreateTypeLiteral(result) is { } literalType)
4949
{
@@ -113,28 +113,22 @@ public static class ArmFunctionReturnTypeEvaluator
113113
return target;
114114
}
115115

116-
private static bool EvaluateOperatorAsArmFunction(string armFunctionName,
117-
[NotNullWhen(true)] out JToken? result,
118-
[NotNullWhen(false)] out DiagnosticBuilder.DiagnosticBuilderDelegate? builderFunc,
119-
params FunctionArgument[] arguments)
116+
private static ResultWithDiagnosticBuilder<JToken> EvaluateOperatorAsArmFunction(string armFunctionName, params FunctionArgument[] arguments)
120117
{
121118
try
122119
{
123-
result = ExpressionBuiltInFunctions.Functions.EvaluateFunction(armFunctionName, arguments, new TemplateExpressionEvaluationHelper().EvaluationContext);
124-
builderFunc = default;
125-
return true;
120+
var result = ExpressionBuiltInFunctions.Functions.EvaluateFunction(armFunctionName, arguments, new TemplateExpressionEvaluationHelper().EvaluationContext);
121+
return new(result);
126122
}
127123
catch (Exception e)
128124
{
129125
// The ARM function invoked will almost certainly fail at runtime, but there's a chance a fix has been
130126
// deployed to ARM since this version of Bicep was released. Given that context, this failure will only
131127
// be reported as a warning, and the fallback type will be used.
132-
builderFunc = b => b.ArmFunctionLiteralTypeConversionFailedWithMessage(
128+
return new(b => b.ArmFunctionLiteralTypeConversionFailedWithMessage(
133129
string.Join(", ", arguments.Select(a => a.TryGetToken()?.ToString())),
134130
armFunctionName,
135-
e.Message);
136-
result = default;
137-
return false;
131+
e.Message));
138132
}
139133
}
140-
}
134+
}

0 commit comments

Comments
 (0)