Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Bond.Parser.CLI/Bond.Parser.CLI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<PackAsTool>true</PackAsTool>
<ToolCommandName>bbc</ToolCommandName>
<PackageId>bbc</PackageId>
<Version>0.2.0</Version>
<Version>0.2.1</Version>
<Authors>Mirco De Zorzi</Authors>
<Description>Bond IDL compiler and toolchain</Description>
<RootNamespace>Bond.Parser.CLI</RootNamespace>
Expand Down
94 changes: 76 additions & 18 deletions Bond.Parser.CLI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,12 @@ static void ShowHelp()
Console.WriteLine("Parse Options:");
Console.WriteLine(" -v, --verbose Show detailed AST output");
Console.WriteLine(" --json Output AST as JSON (Bond schema format)");
Console.WriteLine(" --ignore-imports Parse without resolving imports or types");
Console.WriteLine();
Console.WriteLine("Breaking Options:");
Console.WriteLine(" --against <reference> Reference schema to compare against (file path or .git#branch=name)");
Console.WriteLine(" --error-format <format> Output format: text, json (default: text)");
Console.WriteLine(" --ignore-imports Compare without resolving imports or types");
Console.WriteLine();
Console.WriteLine("Examples:");
Console.WriteLine(" bbc parse schema.bond");
Expand All @@ -71,8 +73,10 @@ static async Task<int> RunParseCommand(string[] args)
var filePath = args[0];
var verbose = args.Contains("--verbose") || args.Contains("-v");
var jsonOutput = args.Contains("--json");
var ignoreImports = args.Contains("--ignore-imports");

var result = await ParserFacade.ParseFileAsync(filePath);
var parseOptions = new ParseOptions(IgnoreImports: ignoreImports);
var result = await ParserFacade.ParseFileAsync(filePath, options: parseOptions);

if (!result.Success)
{
Expand Down Expand Up @@ -116,6 +120,7 @@ static async Task<int> RunBreakingCommand(string[] args)
var againstIndex = Array.FindIndex(args, a => a == "--against");
var formatIndex = Array.FindIndex(args, a => a.StartsWith("--error-format"));
var verbose = args.Contains("-v") || args.Contains("--verbose");
var ignoreImports = args.Contains("--ignore-imports");

if (againstIndex < 0 || againstIndex + 1 >= args.Length)
{
Expand All @@ -139,17 +144,22 @@ static async Task<int> RunBreakingCommand(string[] args)
}
}

var referenceFile = await ResolveReference(against, filePath);
if (referenceFile == null)
var reference = await ResolveReference(against, filePath);
if (reference == null)
{
WriteError($"Error: Could not resolve reference: {against}");
return 1;
}

return await CheckBreaking(referenceFile, filePath, errorFormat, verbose);
return await CheckBreaking(reference, filePath, errorFormat, verbose, ignoreImports);
}

static async Task<string?> ResolveReference(string reference, string currentFilePath)
private sealed record ResolvedReference(
string FilePath,
string? Content,
ImportResolver? ImportResolver);

static async Task<ResolvedReference?> ResolveReference(string reference, string currentFilePath)
{
if (reference.StartsWith(".git#"))
{
Expand All @@ -158,13 +168,13 @@ static async Task<int> RunBreakingCommand(string[] args)

if (File.Exists(reference))
{
return reference;
return new ResolvedReference(Path.GetFullPath(reference), null, null);
}

return null;
}

static async Task<string?> ResolveGitReference(string gitRef, string currentFilePath)
static async Task<ResolvedReference?> ResolveGitReference(string gitRef, string currentFilePath)
{
var parts = gitRef.Split('#');
if (parts.Length != 2)
Expand All @@ -190,26 +200,28 @@ static async Task<int> RunBreakingCommand(string[] args)

var fullPath = Path.GetFullPath(currentFilePath);
var gitRelativePath = Path.GetRelativePath(gitRoot, fullPath).Replace('\\', '/');
if (gitRelativePath.StartsWith("..") || Path.IsPathRooted(gitRelativePath))
{
return null;
}

var content = await RunGitCommand($"show {refName}:{gitRelativePath}");
var content = await RunGitCommand($"show {refName}:{gitRelativePath}", gitRoot);
if (content == null)
{
return null;
}

var tempFile = Path.GetTempFileName();
var tempBondFile = Path.ChangeExtension(tempFile, ".bond");
await File.WriteAllTextAsync(tempBondFile, content);

return tempBondFile;
var virtualPath = Path.GetFullPath(Path.Combine(gitRoot, gitRelativePath));
var importResolver = CreateGitAwareImportResolver(gitRoot, refName);
return new ResolvedReference(virtualPath, content, importResolver);
}
catch
{
return null;
}
}

static async Task<string?> RunGitCommand(string arguments)
static async Task<string?> RunGitCommand(string arguments, string? workingDirectory = null)
{
var process = new System.Diagnostics.Process
{
Expand All @@ -220,6 +232,7 @@ static async Task<int> RunBreakingCommand(string[] args)
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory,
CreateNoWindow = true
}
};
Expand All @@ -231,15 +244,32 @@ static async Task<int> RunBreakingCommand(string[] args)
return process.ExitCode == 0 ? output.Trim() : null;
}

static async Task<int> CheckBreaking(string oldFilePath, string newFilePath, string errorFormat, bool verbose)
static async Task<int> CheckBreaking(ResolvedReference oldSchema, string newFilePath, string errorFormat, bool verbose, bool ignoreImports)
{
var oldResult = await ParserFacade.ParseFileAsync(oldFilePath);
var parseOptions = new ParseOptions(IgnoreImports: ignoreImports);
ParseResult oldResult;
if (oldSchema.Content != null)
{
oldResult = await ParserFacade.ParseContentAsync(
oldSchema.Content,
oldSchema.FilePath,
oldSchema.ImportResolver,
parseOptions);
}
else
{
oldResult = await ParserFacade.ParseFileAsync(
oldSchema.FilePath,
oldSchema.ImportResolver,
default,
parseOptions);
}
if (!oldResult.Success)
{
return OutputParseError(errorFormat, oldResult.Errors, "Failed to parse reference schema", oldFilePath);
return OutputParseError(errorFormat, oldResult.Errors, "Failed to parse reference schema", oldSchema.FilePath);
}

var newResult = await ParserFacade.ParseFileAsync(newFilePath);
var newResult = await ParserFacade.ParseFileAsync(newFilePath, options: parseOptions);
if (!newResult.Success)
{
return OutputParseError(errorFormat, newResult.Errors, "Failed to parse current schema", newFilePath);
Expand Down Expand Up @@ -274,6 +304,34 @@ static async Task<int> CheckBreaking(string oldFilePath, string newFilePath, str
return 0;
}

static ImportResolver CreateGitAwareImportResolver(string gitRoot, string refName)
{
return async (currentFile, importPath) =>
{
var currentDir = Path.GetDirectoryName(currentFile) ?? gitRoot;
var absolutePath = Path.GetFullPath(Path.Combine(currentDir, importPath));

var relativePath = Path.GetRelativePath(gitRoot, absolutePath).Replace('\\', '/');
var inRepo = !relativePath.StartsWith("..") && !Path.IsPathRooted(relativePath);
if (inRepo)
{
var contentFromGit = await RunGitCommand($"show {refName}:{relativePath}", gitRoot);
if (contentFromGit != null)
{
return (absolutePath, contentFromGit);
}
}

if (File.Exists(absolutePath))
{
var content = await File.ReadAllTextAsync(absolutePath);
return (absolutePath, content);
}

throw new FileNotFoundException($"Imported file not found: {importPath}", absolutePath);
};
}

static int OutputParseError(string errorFormat, List<ParseError> errors, string message, string filePath)
{
if (errorFormat == "json")
Expand Down
63 changes: 63 additions & 0 deletions Bond.Parser.Tests/CompatibilityTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Bond.Parser.Compatibility;
Expand Down Expand Up @@ -527,4 +530,64 @@ struct User {
}

#endregion

#region Issues

[Fact]
public async Task BreakingCheck_WithImports_ResolvesTypes()
{
var root = Path.Combine(Path.GetTempPath(), "bond-parser-tests", Guid.NewGuid().ToString("N"));
var mainPath = Path.Combine(root, "schema.bond");
var commonPath = Path.Combine(root, "common.bond");

var commonSchema = """
namespace Test
struct Common { 0: required int32 id; }
""";

var oldSchema = """
import "common.bond"
namespace Test
struct User { 0: required Common c; }
""";

var newSchema = """
import "common.bond"
namespace Test
struct User {
0: required Common c;
1: optional int32 age;
}
""";

var files = new Dictionary<string, string>(StringComparer.Ordinal)
{
[commonPath] = commonSchema
};

ImportResolver resolver = (currentFile, importPath) =>
{
var currentDir = Path.GetDirectoryName(currentFile) ?? root;
var absolutePath = Path.GetFullPath(Path.Combine(currentDir, importPath));
if (!files.TryGetValue(absolutePath, out var content))
{
throw new FileNotFoundException($"Imported file not found: {importPath}", absolutePath);
}
return Task.FromResult((absolutePath, content));
};

var oldResult = await ParserFacade.ParseContentAsync(oldSchema, mainPath, resolver);
oldResult.Success.Should().BeTrue($"parsing should succeed but got errors: {string.Join(", ", oldResult.Errors.Select(e => e.Message))}");

var newResult = await ParserFacade.ParseContentAsync(newSchema, mainPath, resolver);
newResult.Success.Should().BeTrue($"parsing should succeed but got errors: {string.Join(", ", newResult.Errors.Select(e => e.Message))}");

var changes = _checker.CheckCompatibility(oldResult.Ast!, newResult.Ast!);

changes.Should().Contain(c =>
c.Category == ChangeCategory.Compatible &&
c.Description.Contains("age"));
}

#endregion
}
2 changes: 1 addition & 1 deletion Bond.Parser/Json/BondJsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Bond.Parser.Json;
/// </summary>
public class BondJsonConverter : JsonConverter<Syntax.Bond>
{
public override Syntax.Bond? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
public override Syntax.Bond Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotSupportedException("Deserialization not implemented");
}
Expand Down
2 changes: 1 addition & 1 deletion Bond.Parser/Json/BondTypeJsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Bond.Parser.Json;
/// </summary>
public class BondTypeJsonConverter : JsonConverter<BondType>
{
public override BondType? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
public override BondType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotSupportedException("Deserialization not implemented");
}
Expand Down
2 changes: 1 addition & 1 deletion Bond.Parser/Json/DeclarationJsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Bond.Parser.Json;
/// </summary>
public class DeclarationJsonConverter : JsonConverter<Declaration>
{
public override Declaration? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
public override Declaration Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotSupportedException("Deserialization not implemented");
}
Expand Down
2 changes: 1 addition & 1 deletion Bond.Parser/Json/DefaultJsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Bond.Parser.Json;
/// </summary>
public class DefaultJsonConverter : JsonConverter<Default>
{
public override Default? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
public override Default Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotSupportedException("Deserialization not implemented");
}
Expand Down
2 changes: 1 addition & 1 deletion Bond.Parser/Json/FieldJsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Bond.Parser.Json;
/// </summary>
public class FieldJsonConverter : JsonConverter<Field>
{
public override Field? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
public override Field Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotSupportedException("Deserialization not implemented");
}
Expand Down
4 changes: 2 additions & 2 deletions Bond.Parser/Json/MethodJsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Bond.Parser.Json;
/// </summary>
public class MethodTypeJsonConverter : JsonConverter<MethodType>
{
public override MethodType? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
public override MethodType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotSupportedException("Deserialization not implemented");
}
Expand Down Expand Up @@ -42,7 +42,7 @@ public override void Write(Utf8JsonWriter writer, MethodType value, JsonSerializ
/// </summary>
public class MethodJsonConverter : JsonConverter<Method>
{
public override Method? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
public override Method Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotSupportedException("Deserialization not implemented");
}
Expand Down
10 changes: 5 additions & 5 deletions Bond.Parser/Json/SimpleTypeConverters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Bond.Parser.Json;
/// </summary>
public class AttributeJsonConverter : JsonConverter<Syntax.Attribute>
{
public override Syntax.Attribute? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
public override Syntax.Attribute Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotSupportedException("Deserialization not implemented");
}
Expand All @@ -34,7 +34,7 @@ public override void Write(Utf8JsonWriter writer, Syntax.Attribute value, JsonSe
/// </summary>
public class NamespaceJsonConverter : JsonConverter<Namespace>
{
public override Namespace? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
public override Namespace Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotSupportedException("Deserialization not implemented");
}
Expand All @@ -61,7 +61,7 @@ public override void Write(Utf8JsonWriter writer, Namespace value, JsonSerialize
/// </summary>
public class TypeParamJsonConverter : JsonConverter<TypeParam>
{
public override TypeParam? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
public override TypeParam Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotSupportedException("Deserialization not implemented");
}
Expand Down Expand Up @@ -97,7 +97,7 @@ public override void Write(Utf8JsonWriter writer, TypeParam value, JsonSerialize
/// </summary>
public class ConstantJsonConverter : JsonConverter<Constant>
{
public override Constant? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
public override Constant Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotSupportedException("Deserialization not implemented");
}
Expand Down Expand Up @@ -128,7 +128,7 @@ public override void Write(Utf8JsonWriter writer, Constant value, JsonSerializer
/// </summary>
public class ImportJsonConverter : JsonConverter<Import>
{
public override Import? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
public override Import Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotSupportedException("Deserialization not implemented");
}
Expand Down
Loading
Loading