diff --git a/Bond.Parser.CLI/Bond.Parser.CLI.csproj b/Bond.Parser.CLI/Bond.Parser.CLI.csproj index 369266a..19d62f7 100644 --- a/Bond.Parser.CLI/Bond.Parser.CLI.csproj +++ b/Bond.Parser.CLI/Bond.Parser.CLI.csproj @@ -17,7 +17,7 @@ true bbc bbc - 0.2.0 + 0.2.1 Mirco De Zorzi Bond IDL compiler and toolchain Bond.Parser.CLI diff --git a/Bond.Parser.CLI/Program.cs b/Bond.Parser.CLI/Program.cs index da9c7da..5f4d780 100644 --- a/Bond.Parser.CLI/Program.cs +++ b/Bond.Parser.CLI/Program.cs @@ -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 schema to compare against (file path or .git#branch=name)"); Console.WriteLine(" --error-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"); @@ -71,8 +73,10 @@ static async Task 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) { @@ -116,6 +120,7 @@ static async Task 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) { @@ -139,17 +144,22 @@ static async Task 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 ResolveReference(string reference, string currentFilePath) + private sealed record ResolvedReference( + string FilePath, + string? Content, + ImportResolver? ImportResolver); + + static async Task ResolveReference(string reference, string currentFilePath) { if (reference.StartsWith(".git#")) { @@ -158,13 +168,13 @@ static async Task RunBreakingCommand(string[] args) if (File.Exists(reference)) { - return reference; + return new ResolvedReference(Path.GetFullPath(reference), null, null); } return null; } - static async Task ResolveGitReference(string gitRef, string currentFilePath) + static async Task ResolveGitReference(string gitRef, string currentFilePath) { var parts = gitRef.Split('#'); if (parts.Length != 2) @@ -190,18 +200,20 @@ static async Task 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 { @@ -209,7 +221,7 @@ static async Task RunBreakingCommand(string[] args) } } - static async Task RunGitCommand(string arguments) + static async Task RunGitCommand(string arguments, string? workingDirectory = null) { var process = new System.Diagnostics.Process { @@ -220,6 +232,7 @@ static async Task RunBreakingCommand(string[] args) RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, + WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory, CreateNoWindow = true } }; @@ -231,15 +244,32 @@ static async Task RunBreakingCommand(string[] args) return process.ExitCode == 0 ? output.Trim() : null; } - static async Task CheckBreaking(string oldFilePath, string newFilePath, string errorFormat, bool verbose) + static async Task 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); @@ -274,6 +304,34 @@ static async Task 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 errors, string message, string filePath) { if (errorFormat == "json") diff --git a/Bond.Parser.Tests/CompatibilityTests.cs b/Bond.Parser.Tests/CompatibilityTests.cs index 0edcc5b..c078c01 100644 --- a/Bond.Parser.Tests/CompatibilityTests.cs +++ b/Bond.Parser.Tests/CompatibilityTests.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using Bond.Parser.Compatibility; @@ -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(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 } diff --git a/Bond.Parser/Json/BondJsonConverter.cs b/Bond.Parser/Json/BondJsonConverter.cs index 7133de3..7fce8f5 100644 --- a/Bond.Parser/Json/BondJsonConverter.cs +++ b/Bond.Parser/Json/BondJsonConverter.cs @@ -9,7 +9,7 @@ namespace Bond.Parser.Json; /// public class BondJsonConverter : JsonConverter { - 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"); } diff --git a/Bond.Parser/Json/BondTypeJsonConverter.cs b/Bond.Parser/Json/BondTypeJsonConverter.cs index 4f3a336..7918f3c 100644 --- a/Bond.Parser/Json/BondTypeJsonConverter.cs +++ b/Bond.Parser/Json/BondTypeJsonConverter.cs @@ -10,7 +10,7 @@ namespace Bond.Parser.Json; /// public class BondTypeJsonConverter : JsonConverter { - 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"); } diff --git a/Bond.Parser/Json/DeclarationJsonConverter.cs b/Bond.Parser/Json/DeclarationJsonConverter.cs index 1f5a6db..54bf4ce 100644 --- a/Bond.Parser/Json/DeclarationJsonConverter.cs +++ b/Bond.Parser/Json/DeclarationJsonConverter.cs @@ -11,7 +11,7 @@ namespace Bond.Parser.Json; /// public class DeclarationJsonConverter : JsonConverter { - 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"); } diff --git a/Bond.Parser/Json/DefaultJsonConverter.cs b/Bond.Parser/Json/DefaultJsonConverter.cs index 88d8edc..a9cd744 100644 --- a/Bond.Parser/Json/DefaultJsonConverter.cs +++ b/Bond.Parser/Json/DefaultJsonConverter.cs @@ -10,7 +10,7 @@ namespace Bond.Parser.Json; /// public class DefaultJsonConverter : JsonConverter { - 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"); } diff --git a/Bond.Parser/Json/FieldJsonConverter.cs b/Bond.Parser/Json/FieldJsonConverter.cs index e36ea97..f727d57 100644 --- a/Bond.Parser/Json/FieldJsonConverter.cs +++ b/Bond.Parser/Json/FieldJsonConverter.cs @@ -10,7 +10,7 @@ namespace Bond.Parser.Json; /// public class FieldJsonConverter : JsonConverter { - 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"); } diff --git a/Bond.Parser/Json/MethodJsonConverter.cs b/Bond.Parser/Json/MethodJsonConverter.cs index e5a2243..aa96923 100644 --- a/Bond.Parser/Json/MethodJsonConverter.cs +++ b/Bond.Parser/Json/MethodJsonConverter.cs @@ -10,7 +10,7 @@ namespace Bond.Parser.Json; /// public class MethodTypeJsonConverter : JsonConverter { - 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"); } @@ -42,7 +42,7 @@ public override void Write(Utf8JsonWriter writer, MethodType value, JsonSerializ /// public class MethodJsonConverter : JsonConverter { - 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"); } diff --git a/Bond.Parser/Json/SimpleTypeConverters.cs b/Bond.Parser/Json/SimpleTypeConverters.cs index 324567c..fe002d8 100644 --- a/Bond.Parser/Json/SimpleTypeConverters.cs +++ b/Bond.Parser/Json/SimpleTypeConverters.cs @@ -10,7 +10,7 @@ namespace Bond.Parser.Json; /// public class AttributeJsonConverter : JsonConverter { - 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"); } @@ -34,7 +34,7 @@ public override void Write(Utf8JsonWriter writer, Syntax.Attribute value, JsonSe /// public class NamespaceJsonConverter : JsonConverter { - 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"); } @@ -61,7 +61,7 @@ public override void Write(Utf8JsonWriter writer, Namespace value, JsonSerialize /// public class TypeParamJsonConverter : JsonConverter { - 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"); } @@ -97,7 +97,7 @@ public override void Write(Utf8JsonWriter writer, TypeParam value, JsonSerialize /// public class ConstantJsonConverter : JsonConverter { - 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"); } @@ -128,7 +128,7 @@ public override void Write(Utf8JsonWriter writer, Constant value, JsonSerializer /// public class ImportJsonConverter : JsonConverter { - 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"); } diff --git a/Bond.Parser/Parser/AstBuilder.cs b/Bond.Parser/Parser/AstBuilder.cs index bf02318..f9e8eb0 100644 --- a/Bond.Parser/Parser/AstBuilder.cs +++ b/Bond.Parser/Parser/AstBuilder.cs @@ -50,7 +50,7 @@ public override Namespace VisitNamespace(BondParser.NamespaceContext context) return new Namespace(lang, name); } - public override object? VisitLanguage(BondParser.LanguageContext context) + public override object VisitLanguage(BondParser.LanguageContext context) { return context.GetText() switch { @@ -157,11 +157,11 @@ public override Declaration VisitStructDecl(BondParser.StructDeclContext context Declaration result; if (context.structView() != null) { - result = VisitStructView(context.structView(), name, typeParams, attributes)!; + result = VisitStructView(name, typeParams, attributes); } else if (context.structDef() != null) { - result = VisitStructDef(context.structDef(), name, typeParams, attributes)!; + result = VisitStructDef(context.structDef(), name, typeParams, attributes); } else { @@ -174,7 +174,7 @@ public override Declaration VisitStructDecl(BondParser.StructDeclContext context return result; } - private StructDeclaration VisitStructView(BondParser.StructViewContext context, string name, TypeParam[] typeParams, Syntax.Attribute[] attributes) + private StructDeclaration VisitStructView(string name, TypeParam[] typeParams, Syntax.Attribute[] attributes) { return new StructDeclaration { @@ -419,7 +419,7 @@ public override Field VisitField(BondParser.FieldContext context) return new Field(attributes, ordinal, modifier, type, name, defaultValue); } - public override object? VisitModifier(BondParser.ModifierContext context) + public override object VisitModifier(BondParser.ModifierContext context) { if (context.REQUIRED_OPTIONAL() != null) { diff --git a/Bond.Parser/Parser/ParserFacade.cs b/Bond.Parser/Parser/ParserFacade.cs index b604086..aa260ce 100644 --- a/Bond.Parser/Parser/ParserFacade.cs +++ b/Bond.Parser/Parser/ParserFacade.cs @@ -33,15 +33,19 @@ List Errors /// /// Main facade for parsing Bond files /// +public sealed record ParseOptions(bool IgnoreImports = false); + public static class ParserFacade { + /// /// Parses a Bond file from a file path /// public static async Task ParseFileAsync( string filePath, ImportResolver? importResolver = null, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default, + ParseOptions? options = null) { if (!File.Exists(filePath)) { @@ -51,24 +55,52 @@ public static async Task ParseFileAsync( var content = await File.ReadAllTextAsync(filePath, cancellationToken); var absolutePath = Path.GetFullPath(filePath); - return await ParseContentAsync(content, absolutePath, importResolver ?? DefaultImportResolver.Resolve); + return await ParseContentInternalAsync( + content, + absolutePath, + importResolver ?? DefaultImportResolver.Resolve, + options); } /// /// Parses Bond content from a string without file path context /// - public static Task ParseStringAsync(string content, ImportResolver? importResolver = null) + public static Task ParseStringAsync( + string content, + ImportResolver? importResolver = null, + ParseOptions? options = null) + { + return ParseContentInternalAsync( + content, + "", + importResolver ?? DefaultImportResolver.Resolve, + options); + } + + /// + /// Parses Bond content from a string with file path context + /// + public static Task ParseContentAsync( + string content, + string filePath, + ImportResolver? importResolver = null, + ParseOptions? options = null) { - return ParseContentAsync(content, "", importResolver ?? DefaultImportResolver.Resolve); + return ParseContentInternalAsync( + content, + filePath, + importResolver ?? DefaultImportResolver.Resolve, + options); } /// /// Parses Bond content from a string /// - private static async Task ParseContentAsync( + private static async Task ParseContentInternalAsync( string content, string filePath, - ImportResolver importResolver) + ImportResolver importResolver, + ParseOptions? options) { var errors = new List(); @@ -93,6 +125,11 @@ private static async Task ParseContentAsync( var astBuilder = new AstBuilder(); var ast = (Syntax.Bond)astBuilder.Visit(parseTree)!; + if (options?.IgnoreImports == true) + { + return new ParseResult(ast, errors); + } + // Perform semantic analysis var symbolTable = new SymbolTable(); var analyzer = new SemanticAnalyzer(symbolTable, importResolver, filePath); diff --git a/Bond.Parser/Parser/SemanticAnalyzer.cs b/Bond.Parser/Parser/SemanticAnalyzer.cs index b50a467..f469993 100644 --- a/Bond.Parser/Parser/SemanticAnalyzer.cs +++ b/Bond.Parser/Parser/SemanticAnalyzer.cs @@ -39,7 +39,7 @@ public async Task AnalyzeAsync(Syntax.Bond bond) foreach (var declaration in bond.Declarations) { _symbolTable.AddDeclaration(declaration, bond.Namespaces); - ValidateDeclaration(declaration, bond.Namespaces); + ValidateDeclaration(declaration); } } @@ -82,7 +82,7 @@ private static Syntax.Bond ParseContent(string content, string filePath) return (Syntax.Bond)astBuilder.Visit(parseTree)!; } - private void ValidateDeclaration(Declaration declaration, Namespace[] namespaces) + private void ValidateDeclaration(Declaration declaration) { switch (declaration) { diff --git a/Bond.Parser/Parser/TypeResolver.cs b/Bond.Parser/Parser/TypeResolver.cs index 8b420c0..bcf8de2 100644 --- a/Bond.Parser/Parser/TypeResolver.cs +++ b/Bond.Parser/Parser/TypeResolver.cs @@ -153,7 +153,6 @@ private MethodType ResolveMethodType(MethodType methodType, Namespace[] namespac { MethodType.Unary unary => new MethodType.Unary(ResolveType(unary.Type, namespaces)), MethodType.Streaming streaming => new MethodType.Streaming(ResolveType(streaming.Type, namespaces)), - MethodType.Void => methodType, _ => methodType }; } diff --git a/Bond.Parser/Parser/TypeValidator.cs b/Bond.Parser/Parser/TypeValidator.cs index 78e297f..5c24948 100644 --- a/Bond.Parser/Parser/TypeValidator.cs +++ b/Bond.Parser/Parser/TypeValidator.cs @@ -1,5 +1,4 @@ using System; -using System.Numerics; using Bond.Parser.Syntax; using Bond.Parser.Util; diff --git a/version b/version index 0ea3a94..0c62199 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.2.0 +0.2.1