diff --git a/.github/workflows/build-bebopc.yml b/.github/workflows/build-bebopc.yml index c6886517..0387fcc0 100644 --- a/.github/workflows/build-bebopc.yml +++ b/.github/workflows/build-bebopc.yml @@ -40,11 +40,11 @@ jobs: BUILD_ZIP_ARTIFACT_ARM64: ./bin/compiler/Release/publish/${{matrix.ARTIFACT}}-${{matrix.IDENTIFIER}}-arm64.zip steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Get Enviorment Variables id: dotenv - uses: falti/dotenv-action@v0.2.5 + uses: falti/dotenv-action@v1.0.4 - if: matrix.os == 'ubuntu-22.04' name: Install Dependencies @@ -53,10 +53,10 @@ jobs: sudo apt-get install clang zlib1g-dev libkrb5-dev libtinfo5 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: - dotnet-version: "8.0.x" # SDK Version to use; x will use the latest version of the 7.0 channel - include-prerelease: true + dotnet-version: "8.0.x" + dotnet-quality: 'preview' - name: Restore Solution run: dotnet restore @@ -86,13 +86,13 @@ jobs: Compress-Archive -Path ${{env.BUILD_ARTIFACT_ARM64}} -DestinationPath ${{env.BUILD_ZIP_ARTIFACT_ARM64}} - name: Upload X86_64 Build - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: ${{matrix.IDENTIFIER}}-x64 path: ${{env.BUILD_ZIP_ARTIFACT_X86_64}} - name: Upload ARM64 Build - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: ${{matrix.IDENTIFIER}}-arm64 path: ${{env.BUILD_ZIP_ARTIFACT_ARM64}} diff --git a/.github/workflows/build-repl.yml b/.github/workflows/build-repl.yml index ad970816..91426669 100644 --- a/.github/workflows/build-repl.yml +++ b/.github/workflows/build-repl.yml @@ -13,17 +13,17 @@ jobs: REPL_ROOT: ${{github.workspace}}/Repl runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Get Enviorment Variables id: dotenv - uses: falti/dotenv-action@v0.2.5 + uses: falti/dotenv-action@v1.0.4 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: - dotnet-version: "8.0.x" # SDK Version to use; x will use the latest version of the 7.0 channel - include-prerelease: true + dotnet-version: "8.0.x" + dotnet-quality: 'preview' - name: Restore Project run: dotnet restore working-directory: ${{env.REPL_ROOT}} @@ -32,7 +32,7 @@ jobs: working-directory: ${{env.REPL_ROOT}} - name: Upload Package - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: bebop-repl-latest path: ${{github.workspace}}/bin/repl/Release/publish/wwwroot/ diff --git a/.github/workflows/build-runtime-cs.yml b/.github/workflows/build-runtime-cs.yml index b512aa16..75f68de7 100644 --- a/.github/workflows/build-runtime-cs.yml +++ b/.github/workflows/build-runtime-cs.yml @@ -15,21 +15,21 @@ jobs: RUNTIME_ROOT: ${{github.workspace}}/Runtime/C# runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Get Enviorment Variables id: dotenv - uses: falti/dotenv-action@v0.2.5 + uses: falti/dotenv-action@v1.0.4 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: dotnet-version: | 5.0.x 6.0.x 7.0.x 8.0.x - include-prerelease: true + dotnet-quality: 'preview' - name: Generate Output.g.cs working-directory: "./Laboratory/C#" run: dotnet run --project ../../Compiler/ --cs "./GeneratedTestCode/Output.g.cs" --namespace Bebop.Codegen --files $(ls -p ../Schemas/Valid/*.bop | tr '\n' ' ') @@ -59,7 +59,7 @@ jobs: working-directory: ${{env.RUNTIME_ROOT}} - name: Upload Package - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: bebop-runtime-cs-latest path: ${{env.RUNTIME_ROOT}}/bin/Release/*.nupkg diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index bbaa22ec..1982a258 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -15,14 +15,14 @@ jobs: steps: - uses: actions/checkout@v2 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: dotnet-version: | 5.0.x 6.0.x 7.0.x 8.0.x - include-prerelease: true + dotnet-quality: 'preview' - name: Setup Rust uses: actions-rs/toolchain@v1 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3d8ab9cb..3a8c0e6d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -30,11 +30,11 @@ jobs: BUILD_ZIP_ARTIFACT_X86_64: ./bin/compiler/Release/publish/${{matrix.ARTIFACT}}-${{matrix.IDENTIFIER}}-x64.zip BUILD_ZIP_ARTIFACT_ARM64: ./bin/compiler/Release/publish/${{matrix.ARTIFACT}}-${{matrix.IDENTIFIER}}-arm64.zip steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Get Environment Variables id: dotenv - uses: falti/dotenv-action@v0.2.5 + uses: falti/dotenv-action@v1.0.4 # ubuntu is the fastest OS to spin up and if one fails they all fail. - if: matrix.os == 'ubuntu-22.04' @@ -52,10 +52,10 @@ jobs: sudo apt-get install clang zlib1g-dev libkrb5-dev libtinfo5 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: - dotnet-version: "8.0.x" # SDK Version to use; x will use the latest version of the 7.0 channel - include-prerelease: true + dotnet-version: "8.0.x" + dotnet-quality: 'preview' - name: Build bebopc run: | @@ -109,13 +109,13 @@ jobs: Compress-Archive -Path ${{env.BUILD_ARTIFACT_ARM64}} -DestinationPath ${{env.BUILD_ZIP_ARTIFACT_ARM64}} - name: Upload X86_64 Build - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: ${{matrix.IDENTIFIER}}-x64 path: ${{env.BUILD_ZIP_ARTIFACT_X86_64}} - name: Upload ARM64 Build - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: ${{matrix.IDENTIFIER}}-arm64 path: ${{env.BUILD_ZIP_ARTIFACT_ARM64}} @@ -131,7 +131,7 @@ jobs: runs-on: ubuntu-22.04 needs: build-compiler steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - uses: actions/setup-node@v1 with: node-version: "18.16.0" # LTS @@ -141,10 +141,10 @@ jobs: uses: falti/dotenv-action@v0.2.5 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: - dotnet-version: "7.0.x" # SDK Version to use; x will use the latest version of the 7.0 channel - include-prerelease: true + dotnet-version: "8.0.x" + dotnet-quality: 'preview' - name: Test .NET Runtime run: | @@ -159,7 +159,7 @@ jobs: working-directory: ${{env.NET_RUNTIME_ROOT}} - name: Upload .NET Runtime Package - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: bebop-runtime-cs-${{ steps.dotenv.outputs.version }} path: ${{env.NET_RUNTIME_ROOT}}/bin/Release/bebop.${{ steps.dotenv.outputs.version }}.nupkg @@ -198,7 +198,7 @@ jobs: mv tmp Cargo.toml cargo package --allow-dirty - name: Upload Rust Runtime - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: bebop-runtime-rust-${{steps.dotenv.outputs.version}} path: ${{ env.RUST_RUNTIME_ROOT }}/target/package/bebop-${{ steps.dotenv.outputs.version }}.crate @@ -225,7 +225,7 @@ jobs: working-directory: ${{env.TS_RUNTIME_ROOT}} - name: Upload TypeScript Runtime Package - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: bebop-runtime-ts-${{ steps.dotenv.outputs.version }} path: ${{env.TS_RUNTIME_ROOT}}/bebop-v${{ steps.dotenv.outputs.version }}.tgz @@ -237,7 +237,7 @@ jobs: working-directory: ./Repl/ - name: Upload REPL Artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: bebop-repl-${{ steps.dotenv.outputs.version }} path: ./bin/repl/Release/publish/wwwroot/ @@ -249,7 +249,7 @@ jobs: needs: build-compiler runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Setup Rust uses: actions-rs/toolchain@v1 with: @@ -281,7 +281,7 @@ jobs: working-directory: ${{env.TOOLS_ROOT}}/vs - name: Upload Nuget Artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: bebop-tools-nuget-${{ steps.dotenv.outputs.version }} path: ${{env.TOOLS_ROOT}}/vs/packages/bebop-tools.${{ steps.dotenv.outputs.version }}.nupkg @@ -294,7 +294,7 @@ jobs: cargo package --allow-dirty - name: Upload Cargo Tools - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: bebop-tools-cargo-${{ steps.dotenv.outputs.version }} path: ${{ env.TOOLS_ROOT }}/cargo/target/package/bebop-tools-${{ steps.dotenv.outputs.version }}.crate @@ -311,7 +311,7 @@ jobs: working-directory: ${{env.TOOLS_ROOT}}/node - name: Upload NPM Artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: bebop-tools-npm-${{ steps.dotenv.outputs.version }} path: ${{env.TOOLS_ROOT}}/node/bebop-tools-v${{ steps.dotenv.outputs.version }}.tgz @@ -329,7 +329,7 @@ jobs: working-directory: ./vscode-bebop - name: Upload VSCode artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: vscode-bebop path: vscode-bebop/bebop-lang-*.vsix @@ -341,7 +341,7 @@ jobs: runs-on: ubuntu-22.04 needs: [build-compiler, build-runtimes, build-tools] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - uses: actions/setup-node@v1 with: node-version: "18.16.0" # LTS diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 92b8780d..df849f2d 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -35,16 +35,15 @@ jobs: - name: Checkout uses: actions/checkout@v3 - - uses: actions/checkout@v1 - name: Get Environment Variables id: dotenv uses: falti/dotenv-action@v0.2.5 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: - dotnet-version: "7.0.x" # SDK Version to use; x will use the latest version of the 7.0 channel - include-prerelease: true + dotnet-version: "8.0.x" # SDK Version to use; x will use the latest version of the 7.0 channel + dotnet-quality: 'preview' - name: Build REPL run: | diff --git a/.github/workflows/test-csharp.yml b/.github/workflows/test-csharp.yml index 45fc80cc..7c77c685 100644 --- a/.github/workflows/test-csharp.yml +++ b/.github/workflows/test-csharp.yml @@ -8,18 +8,18 @@ jobs: test-csharp: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - uses: actions/setup-dotnet@v1 + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v3 with: dotnet-version: | 5.0.x 6.0.x 7.0.x 8.0.x - include-prerelease: true + dotnet-quality: 'preview' - name: Build and run tests shell: bash run: | - dotnet run --project ../../Compiler/ --cs "./GeneratedTestCode/Output.g.cs" --namespace Bebop.Codegen --files $(ls -p ../Schemas/Valid/*.bop | tr '\n' ' ') + dotnet run --project ../../Compiler/ --trace -i $(ls -p ../Schemas/Valid/*.bop | tr '\n' ' ') build -g "cs:./GeneratedTestCode/Output.g.cs,namespace=Bebop.Codegen" dotnet test -nowarn:CS0618 working-directory: "./Laboratory/C#" diff --git a/.github/workflows/test-dart.yml b/.github/workflows/test-dart.yml index 4381754b..1a08aa23 100644 --- a/.github/workflows/test-dart.yml +++ b/.github/workflows/test-dart.yml @@ -9,12 +9,12 @@ jobs: test-dart: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - uses: dart-lang/setup-dart@v1 - - uses: actions/setup-dotnet@v1 + - uses: actions/setup-dotnet@v3 with: dotnet-version: "8.0.x" # SDK Version to use; x will use the latest version of the 7.0 channel - include-prerelease: true + dotnet-quality: 'preview' - name: Build and run tests run: | mkdir gen diff --git a/.github/workflows/test-rust.yml b/.github/workflows/test-rust.yml index 8740f99d..d6e264fa 100644 --- a/.github/workflows/test-rust.yml +++ b/.github/workflows/test-rust.yml @@ -19,10 +19,10 @@ jobs: run: cargo test working-directory: ./Runtime/Rust - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: - dotnet-version: "8.0.x" # SDK Version to use; x will use the latest version of the 7.0 channel - include-prerelease: true + dotnet-version: "8.0.x" + dotnet-quality: 'preview' - name: Build Compiler run: | dotnet build Compiler diff --git a/.github/workflows/test-typescript.yml b/.github/workflows/test-typescript.yml index d245d4b1..d1a403f3 100644 --- a/.github/workflows/test-typescript.yml +++ b/.github/workflows/test-typescript.yml @@ -9,14 +9,14 @@ jobs: test-typescript: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - uses: actions/setup-node@v1 with: node-version: "18.16.0" # LTS - - uses: actions/setup-dotnet@v1 + - uses: actions/setup-dotnet@v3 with: - dotnet-version: "8.0.x" # SDK Version to use; x will use the latest version of the 7.0 channel - include-prerelease: true + dotnet-version: "8.0.x" + dotnet-quality: 'preview' - name: Build Typescript runtime run: | yarn install diff --git a/Compiler/BebopCompiler.cs b/Compiler/BebopCompiler.cs index 1f34493a..8319e631 100644 --- a/Compiler/BebopCompiler.cs +++ b/Compiler/BebopCompiler.cs @@ -1,13 +1,14 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; -using Core.Generators; -using Core.Logging; +using System.Threading; using Core.Meta; -using Core.Parser; using Core.Exceptions; +using Core.Parser; +using Core.Generators; +using Core.Logging; +using System.IO; namespace Compiler; @@ -16,96 +17,46 @@ public class BebopCompiler public const int Ok = 0; public const int Err = 1; - public CommandLineFlags Flags { get; } - - public BebopCompiler(CommandLineFlags flags) + public static BebopSchema ParseSchema(IEnumerable schemaPaths) { - Flags = flags; - } - - private async Task ParseAndValidateSchema(List schemaPaths, string nameSpace) - { - var parser = new SchemaParser(schemaPaths, nameSpace); - var schema = await parser.Parse(); + var parser = new SchemaParser(schemaPaths); + var schema = parser.Parse(); schema.Validate(); return schema; } - public async Task CompileSchema(Func makeGenerator, - string textualSchema, string outputFile, string nameSpace, TempoServices services, Version? langVersion) + public static void Build(GeneratorConfig generatorConfig, BebopSchema schema, BebopConfig config) { - var parser = new SchemaParser(textualSchema, nameSpace); - var schema = await parser.Parse(); - schema.Validate(); - - var diagonstics = GetSchemaDiagnostics(schema); - if (diagonstics.Errors.Count > 0) + var (warnings, errors) = GetSchemaDiagnostics(schema, config.SupressedWarningCodes); + var generator = GeneratorUtils.ImplementedGenerators[generatorConfig.Alias](schema, generatorConfig); + var compiled = generator.Compile(); + var auxiliary = generator.GetAuxiliaryFile(); + if (string.Equals(generatorConfig.OutFile, "stdout", StringComparison.OrdinalIgnoreCase)) { - var errors = new CompilerOutput(diagonstics.Warnings, diagonstics.Errors, null); - DiagnosticLogger.Instance.WriteCompilerOutput(errors); - return Err; + DiagnosticLogger.Instance.WriteCompilerOutput(new CompilerOutput(warnings, errors, new GeneratedFile("stdout", compiled, generator.Alias, auxiliary))); + return; } - var generator = makeGenerator(schema); - var compiled = generator.Compile(langVersion, services: services, writeGeneratedNotice: Flags?.SkipGeneratedNotice ?? false, emitBinarySchema: Flags?.EmitBinarySchema ?? false); - var auxiliary = generator.GetAuxiliaryFile(); - var generatedFile = new GeneratedFile(outputFile, compiled, generator.Alias, auxiliary); - var results = new CompilerOutput(diagonstics.Warnings, diagonstics.Errors, generatedFile); - - DiagnosticLogger.Instance.WriteCompilerOutput(results); - return Ok; - } - - public async Task CompileSchema(Func makeGenerator, - List schemaPaths, - FileInfo outputFile, - string nameSpace, TempoServices services, Version? langVersion) - { - if (outputFile.Directory is not null && !outputFile.Directory.Exists) + var outFile = generatorConfig.OutFile; + if (!Path.IsPathRooted(outFile)) { - outputFile.Directory.Create(); + outFile = Path.Combine(config.WorkingDirectory, outFile); } - if (outputFile.Exists) + var outDirectory = Path.GetDirectoryName(outFile) ?? throw new CompilerException("Could not determine output directory."); + if (!Directory.Exists(outDirectory)) { - File.Delete(outputFile.FullName); + Directory.CreateDirectory(outDirectory); + } + File.WriteAllText(outFile, compiled); + if (auxiliary is not null) + { + File.WriteAllText(Path.Combine(outDirectory, auxiliary.Name), auxiliary.Contents); } - - var schema = await ParseAndValidateSchema(schemaPaths, nameSpace); - var result = await ReportSchemaDiagnostics(schema); - if (result == Err) return Err; - var generator = makeGenerator(schema); - generator.WriteAuxiliaryFiles(outputFile.DirectoryName ?? string.Empty); - var compiled = generator.Compile(langVersion, services: services, writeGeneratedNotice: Flags?.SkipGeneratedNotice ?? false, emitBinarySchema: Flags?.EmitBinarySchema ?? false); - await File.WriteAllTextAsync(outputFile.FullName, compiled); - return Ok; } - private (List Warnings, List Errors) GetSchemaDiagnostics(BebopSchema schema) + public static (List Warnings, List Errors) GetSchemaDiagnostics(BebopSchema schema, int[] supressWarningCodes) { - var noWarn = Flags?.NoWarn ?? new List(); - var loudWarnings = schema.Warnings.Where(x => !noWarn.Contains(x.ErrorCode.ToString())).ToList(); + var noWarn = supressWarningCodes; + var loudWarnings = schema.Warnings.Where(x => !noWarn.Contains(x.ErrorCode)).ToList(); return (loudWarnings, schema.Errors); } - - private async Task ReportSchemaDiagnostics(BebopSchema schema) - { - var noWarn = Flags?.NoWarn ?? new List(); - var loudWarnings = schema.Warnings.Where(x => !noWarn.Contains(x.ErrorCode.ToString())); - var errors = loudWarnings.Concat(schema.Errors).ToList(); - DiagnosticLogger.Instance.WriteSpanDiagonstics(errors); - return schema.Errors.Count > 0 ? Err : Ok; - } - - public async Task CheckSchema(string textualSchema) - { - var parser = new SchemaParser(textualSchema, "CheckNameSpace"); - var schema = await parser.Parse(); - schema.Validate(); - return await ReportSchemaDiagnostics(schema); - } - - public async Task CheckSchemas(List schemaPaths) - { - var schema = await ParseAndValidateSchema(schemaPaths, "CheckNameSpace"); - return await ReportSchemaDiagnostics(schema); - } } \ No newline at end of file diff --git a/Compiler/BebopConfig.cs b/Compiler/BebopConfig.cs deleted file mode 100644 index 4e9d0b79..00000000 --- a/Compiler/BebopConfig.cs +++ /dev/null @@ -1,201 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; -using Core.Generators; - -namespace Compiler -{ - /// - /// A strongly typed representation of the bebop.json file. - /// - public class BebopConfig - { - /// - /// Specifies a list of code generators to target during compilation. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("generators")] - public GeneratorConfig[]? Generators { get; set; } - - /// - /// Specifies a namespace that generated code will use. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("namespace")] - [JsonConverter(typeof(MinMaxLengthCheckConverter))] - public string? Namespace { get; set; } - - /// - /// Specifies an array of filenames or patterns to compile. These filenames are resolved - /// relative to the directory containing the bebop.json file. If no 'include' property is - /// present in a bebop.json, the compiler defaults to including all files in the containing - /// directory and subdirectories except those specified by 'exclude'. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("include")] - public string[]? Include { get; set; } - - /// - /// Specifies an array of filenames or patterns that should be skipped when resolving - /// include. The 'exclude' property only affects the files included via the 'include' - /// property. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("exclude")] - public string[]? Exclude { get; set; } - - /// - /// Settings for the watch mode in bebopc. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("watchOptions")] - public WatchOptions? WatchOptions { get; set; } - - public static BebopConfig? FromJson(string json) => JsonSerializer.Deserialize(json, Settings); - - private static readonly JsonSerializerOptions Settings = new(JsonSerializerDefaults.General) - { - Converters = - { - ServicesConverter.Singleton - }, - }; - } - - public partial class GeneratorConfig - { - /// - /// Specify the code generator schemas will be compiled to. - /// - [JsonPropertyName("alias")] - public string? Alias { get; set; } - - /// - /// Specify the version of the language the code generator should target. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("langVersion")] - public string? LangVersion { get; set; } - - /// - /// Specify if the code generator should produces a notice at the start of the output file - /// stating code was auto-generated. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("noGenerationNotice")] - public bool? NoGenerationNotice { get; set; } - - - /// - /// Specify if the code generator should emit a binary schema within the output file. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("emitBinarySchema")] - public bool? EmitBinarySchema { get; set; } - - /// - /// Specify a file that bundles all generated code into one file. - /// - [JsonPropertyName("outFile")] - public string? OutFile { get; set; } - - /// - /// By default, bebopc generates a concrete client and a service base class. This property - /// can be used to limit bebopc asset generation. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("services")] - public TempoServices? Services { get; set; } - } - - /// - /// Settings for the watch mode in bebopc. - /// - public partial class WatchOptions - { - /// - /// Remove a list of directories from the watch process. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("excludeDirectories")] - public string[]? ExcludeDirectories { get; set; } - - /// - /// Remove a list of files from the watch mode's processing. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("excludeFiles")] - public string[]? ExcludeFiles { get; set; } - } - - internal class MinMaxLengthCheckConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(string); - - public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var value = reader.GetString(); - if (value?.Length >= 1) - { - return value; - } - throw new Exception("Cannot unmarshal type string"); - } - - public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) - { - if (value.Length >= 1) - { - JsonSerializer.Serialize(writer, value, options); - return; - } - throw new Exception("Cannot marshal type string"); - } - - public static readonly MinMaxLengthCheckConverter Singleton = new MinMaxLengthCheckConverter(); - } - - internal class ServicesConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(TempoServices); - - public override TempoServices Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var value = reader.GetString(); - switch (value) - { - case "both": - return TempoServices.Both; - case "client": - return TempoServices.Client; - case "none": - return TempoServices.None; - case "server": - return TempoServices.Server; - } - throw new Exception("Cannot unmarshal type Services"); - } - - public override void Write(Utf8JsonWriter writer, TempoServices value, JsonSerializerOptions options) - { - switch (value) - { - case TempoServices.Both: - JsonSerializer.Serialize(writer, "both", options); - return; - case TempoServices.Client: - JsonSerializer.Serialize(writer, "client", options); - return; - case TempoServices.None: - JsonSerializer.Serialize(writer, "none", options); - return; - case TempoServices.Server: - JsonSerializer.Serialize(writer, "server", options); - return; - } - throw new Exception("Cannot marshal type Services"); - } - - public static readonly ServicesConverter Singleton = new ServicesConverter(); - } -} \ No newline at end of file diff --git a/Compiler/CliStrings.cs b/Compiler/CliStrings.cs new file mode 100644 index 00000000..40dce6b6 --- /dev/null +++ b/Compiler/CliStrings.cs @@ -0,0 +1,25 @@ +namespace Compiler; + +public static class CliStrings { + + public const string ConfigFlag = "--config"; + public const string NoEmitFlag = "--no-emit"; + public const string NoWarnFlag = "--no-warn"; + public const string GeneratorFlag = "--generator"; + public const string IncludeFlag = "--include"; + public const string ExcludeFlag = "--exclude"; + public const string DiagnosticFormatFlag = "--diagnostic-format"; + public const string ExcludeDirectoriesFlag = "--exclude-directories"; + public const string ExcludeFilesFlag = "--exclude-files"; + public const string PreserveWatchOutputFlag = "--preserve-watch-output"; + public const string InitFlag = "--init"; + public const string ListSchemasFlag = "--list-schemas-only"; + public const string LocaleFlag = "--locale"; + public const string ShowConfigFlag = "--show-config"; + public const string TraceFlag = "--trace"; + public const string StandardInputFlag = "--stdin"; + public const string WatchCommand = "watch"; + public const string BuildCommand = "build"; + public const string LangServerCommand = "langserver"; + +} \ No newline at end of file diff --git a/Compiler/CommandLineFlags.cs b/Compiler/CommandLineFlags.cs deleted file mode 100644 index 990ce267..00000000 --- a/Compiler/CommandLineFlags.cs +++ /dev/null @@ -1,730 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using Core.Generators; -using Core.Logging; -using Core.Meta; -using Microsoft.Extensions.FileSystemGlobbing; - -namespace Compiler -{ - #region Records - /// - /// Represents a code generator passed to bebopc. - /// - /// The alias of the code generator. - /// The quailified file that the generator will produce. - /// The control for which service compontents will be generated. - /// If set this value defines the version of the language generated code will use. - public record CodeGenerator(string Alias, string OutputFile, TempoServices Services, Version? LangVersion); - #endregion - - #region FlagAttribute - - /// - /// Models an application command-line flag. - /// - [AttributeUsage(AttributeTargets.Property)] - public class CommandLineFlagAttribute : Attribute - { - /// - /// Creates a new command-line flag attribute - /// - /// The name of the command-line flag. - /// A detailed description of flag. - /// An example of how to use the attributed flag. - /// Indicates if a flag is used to generate code. - public CommandLineFlagAttribute(string name, - string helpText, - string usageExample = "", - bool isGeneratorFlag = false, - bool valuesRequired = true, - bool hideFromHelp = false) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name)); - } - if (string.IsNullOrWhiteSpace(helpText)) - { - throw new ArgumentNullException(nameof(helpText)); - } - Name = name; - HelpText = helpText; - UsageExample = usageExample; - IsGeneratorFlag = isGeneratorFlag; - ValuesRequired = valuesRequired; - HideFromHelp = hideFromHelp; - } - - - /// - /// The name command-line flag. This name is usually a single english word. - /// - /// - /// For compound words you should use a hyphen separator rather than camel casing. - /// - public string Name { get; } - - /// - /// A detailed description of the command-line flag. - /// - public string HelpText { get; } - - /// - /// If any an example of the parameter that is used in conjunction with the flag - /// - public string UsageExample { get; } - - /// - /// If this property is set to true the attributed command-line flag is used to instantiate a code generator. - /// - public bool IsGeneratorFlag { get; } - - /// - /// If this property is true, the flag requires values (arguments) to be set. - /// - public bool ValuesRequired { get; } - - /// - /// If this property is true, the command-line flag will be hidden from help output. - /// - public bool HideFromHelp { get; } - } - - #endregion - /// - /// Static extension methods for the command-line flag types. - /// - public static class CommandLineExtensions - { - /// - /// Determines if the provided contains the specified - /// - /// The collection of flags to check. - /// The name of the flag to look for. - /// true if the flag is present, otherwise false. - public static bool HasFlag(this List flags, string flagName) - { - return flags.Any(f => f.Name.Equals(flagName, StringComparison.OrdinalIgnoreCase)); - } - - /// - /// Finds the flag associated with the specified - /// - /// The collection of flags to check. - /// The name of the flag to look for. - /// An instance of the desired flag. - public static CommandLineFlag GetFlag(this List flags, string flagName) - { - return flags.Find(f => f.Name.Equals(flagName, StringComparison.OrdinalIgnoreCase))!; - } - - } - /// - /// Represents a parsed command-line flag and its values. - /// - public class CommandLineFlag - { - /// - /// Creates a new command-line flag - /// - /// The name of the flag - /// The values (if any) corresponding to the flag. - public CommandLineFlag(string name, string[] values) - { - Name = name; - Values = values; - } - /// - /// Indicates if the current flag has any values assigned to it. - /// - /// true if the flag has values, otherwise false. - public bool HasValues() => Values is { Length: > 0 }; - - /// - /// Returns the first value associated with the current flag. - /// - /// - public string? GetValue() - { - return Values.FirstOrDefault()?.Trim(); - } - - /// - /// A collection of all values associated with the current flag. - /// - public string[] Values { get; init; } - /// - /// The name of the current flag. - /// - public string Name { get; init; } - } - - /// - /// A class for constructing and parsing all available commands. - /// - public class CommandLineFlags - { - /// - /// The name of the config file used by bebopc. - /// - private const string ConfigFileName = "bebop.json"; - - [CommandLineFlag("config", "Initializes the compiler from the specified configuration file.", - "--config bebop.json")] - public string? ConfigFile { get; private set; } - - [CommandLineFlag("cpp", "Generate C++ source code to the specified file", - "--cpp ./my/output/HelloWorld.hpp", true)] - public string? CPlusPlusOutput { get; private set; } - - [CommandLineFlag("cs", "Generate C# source code to the specified file", "--cs ./my/output/HelloWorld.cs", - true)] - public string? CSharpOutput { get; private set; } - - [CommandLineFlag("dart", "Generate Dart source code to the specified file", - "--dart ./my/output/HelloWorld.dart", true)] - public string? DartOutput { get; private set; } - - [CommandLineFlag("rust", "Generate Rust source code to the specified file", "--rust ./my/output/HelloWorld.rs", - true)] - public string? RustOutput { get; private set; } - - [CommandLineFlag("ts", "Generate TypeScript source code to the specified file", - "--ts ./my/output/HelloWorld.ts", true)] - public string? TypeScriptOutput { get; private set; } - - [CommandLineFlag("py", "Generate Python source code to the specified file", - "--py ./my/output/HelloWorld.py", true)] - public string? PythonOutput { get; private set; } - - [CommandLineFlag("namespace", "When this option is specified generated code will use namespaces", - "--cs --namespace [package]")] - public string? Namespace { get; private set; } - - [CommandLineFlag("skip-generated-notice", "Flag to disable generating the file header announcing the file was autogenerated by bebop.", "--rust --skip-generation-notice", valuesRequired: false)] - public bool SkipGeneratedNotice { get; private set; } - - [CommandLineFlag("emit-binary-schema", "Flag to enable experimental binary schemas in generated code.", "--ts --emit-binary-schema", valuesRequired: false)] - public bool EmitBinarySchema { get; private set; } - - [CommandLineFlag("dir", "Parse and generate code from a directory of schemas", "--ts --dir [input dir]")] - public string? SchemaDirectory { get; private set; } - - [CommandLineFlag("files", "Parse and generate code from a list of schemas", "--files [file1] [file2] ...")] - public List? SchemaFiles { get; private set; } - - [CommandLineFlag("check", "Checks that the provided schema files are valid, or entire project defined by bebop.json if no files provided", "--check [file.bop] [file2.bop] ...", false, false)] - public List? CheckSchemaFiles { get; private set; } - - [CommandLineFlag("check-schema", "Reads a schema from stdin and validates it.", "--check-schema < [schema text]")] - public string? CheckSchemaFile { get; private set; } - - [CommandLineFlag("in", "Reads a schema from stdin.", "--in < [schema text]")] - public string? StandardInput { get; private set; } - - [CommandLineFlag("out", "Writes a compiled schema to standard out.", "--out > [output file]")] - public bool StandardOutput { get; private set; } - - /// - /// When set to true the process will output the product version and exit with a zero return code. - /// - [CommandLineFlag("version", "Show version info and exit.", "--version")] - public bool Version { get; private set; } - - /// - /// When set to true the process will output the and exit with a zero return code. - /// - [CommandLineFlag("help", "Show this text and exit.", "--help")] - public bool Help { get; private set; } - - [CommandLineFlag("langserv", "Starts the language server", "--langserv", hideFromHelp: true)] - public bool LanguageServer { get; private set; } - - [CommandLineFlag("debug", "Waits for a debugger to attach", "--debug", hideFromHelp: true)] - public bool Debug { get; private set; } - - [CommandLineFlag("watch", "Watches schemas for changes and regenerates code", "--watch")] - public bool Watch { get; private set; } - - [CommandLineFlag("watch-excluded-directories", "Directories which willbe excluded from the watch process. Supports globs.", "--watch-excluded-directories [directory1] [directory2] ...")] - public List? WatchExcludeDirectories { get; private set; } - - [CommandLineFlag("watch-excluded-files", "Files which willbe excluded from the watch process. Supports globs.", "--watch-excluded-files [file1] [file2] ...")] - public List? WatchExcludeFiles { get; private set; } - - [CommandLineFlag("preserve-watch-output", "Whether to keep outdated console output in watch mode instead of clearing the screen every time a change happened.", "--watch --preserve-watch-output")] - public bool PreserveWatchOutput { get; private set; } - - /// - /// Controls how loggers format data. - /// - [CommandLineFlag("log-format", "Defines the formatter that will be used with logging.", - "--log-format (structured|msbuild|json)")] - public LogFormatter LogFormatter { get; private set; } - - /// - /// An optional flag to set the version of a language a code generator will use. - /// - [CommandLineFlag("cs-version", "Defines the C# language version the C# generator will target.", - "--cs ./my/output/HelloWorld.cs --cs-version (9.0|8.0)")] - public Version? CSharpVersion { get; private set; } - - [CommandLineFlag("no-warn", "Disable a list of warning codes", "--no-warn 200 201 202")] - public List? NoWarn { get; private set; } - - [CommandLineFlag("quiet", "Suppresses all output including errors", "--quiet")] - public bool Quiet { get; private set; } - - public string HelpText { get; } - - public string WorkingDirectory { get; private set; } - - private readonly Dictionary ServiceConfigs = new Dictionary() - { - ["cpp"] = TempoServices.Both, - ["cs"] = TempoServices.Both, - ["dart"] = TempoServices.Both, - ["rust"] = TempoServices.Both, - ["ts"] = TempoServices.Both, - ["py"] = TempoServices.Both - }; - - /// - /// Finds a language version flag set for a generator. - /// - private Version? GetGeneratorVersion(CommandLineFlagAttribute attribute) - { - foreach (var flag in GetFlagAttributes()) - { - if ($"{attribute.Name}-version".Equals(flag.Attribute.Name, StringComparison.OrdinalIgnoreCase) && flag.Property.GetValue(this) is Version value) - { - return value; - } - } - return null; - } - - /// - /// Returns the alias and output file of all command-line specified code generators. - /// - public IEnumerable GetParsedGenerators() - { - foreach (var flag in GetFlagAttributes()) - { - if (flag.Attribute.IsGeneratorFlag && flag.Property.GetValue(this) is string value) - { - yield return new CodeGenerator(flag.Attribute.Name, value, ServiceConfigs[flag.Attribute.Name], GetGeneratorVersion(flag.Attribute)); - } - } - } - - #region Static - - /// - /// Walks all properties in and maps them to their assigned - /// - /// - /// - private static List<(PropertyInfo Property, CommandLineFlagAttribute Attribute)> GetFlagAttributes() - { - return (from p in typeof(CommandLineFlags).GetProperties() - let attr = p.GetCustomAttributes(typeof(CommandLineFlagAttribute), true) - where attr.Length == 1 - select (p, attr.First() as CommandLineFlagAttribute)) - .Select(t => ((PropertyInfo, CommandLineFlagAttribute))t) - .ToList(); - } - - /// - /// Hide the constructor to prevent direct initializationW - /// - private CommandLineFlags(string helpText) - { - HelpText = helpText; - } - - /// - /// Searches recursively upward to locate the config file belonging to . - /// - /// The fully qualified path to the config file, or null if not found. - public static string? FindBebopConfig() - { - return null; - var workingDirectory = Directory.GetCurrentDirectory(); - var configFile = Directory.GetFiles(workingDirectory, ConfigFileName).FirstOrDefault(); - while (string.IsNullOrWhiteSpace(configFile)) - { - if (Directory.GetParent(workingDirectory) is not { Exists: true } parent) - { - break; - } - workingDirectory = parent.FullName; - if (parent.GetFiles(ConfigFileName)?.FirstOrDefault() is { Exists: true } file) - { - configFile = file.FullName; - } - } - return configFile; - } - - #endregion - - #region Parsing - - /// - /// Parses an array of command-line flags into dictionary. - /// - /// The flags to be parsed. - /// A dictionary containing all parsed flags and their value if any. - private static List GetFlags(string[] args) - { - var flags = new List(); - foreach (var token in args) - { - if (token.StartsWith("--")) - { - var key = new string(token.SkipWhile(c => c == '-').ToArray()).ToLowerInvariant(); - var value = args.SkipWhile(i => i != $"--{key}").Skip(1).TakeWhile(i => !i.StartsWith("--")).ToArray(); - - flags.Add(new CommandLineFlag(key, value)); - } - } - return flags; - } - - /// - /// Attempts to find the flag and parse its value. - /// - /// The command-line arguments to sort through - /// - /// If the flag was present an had a valid value, that enum member will be returned. - /// Otherwise the default formatter is used. - /// - public static LogFormatter FindLogFormatter(string[] args) - { - var flags = GetFlags(args); - foreach (var flag in flags) - { - if (flag.Name.Equals("log-format", StringComparison.OrdinalIgnoreCase) && - Enum.TryParse(flag.GetValue(), true, out var parsedEnum)) - { - return parsedEnum; - } - } - return LogFormatter.Enhanced; - } - - - /// - /// Attempts to parse command-line flags into a instance - /// - /// the array of arguments to parse - /// An instance which contains all parsed flags and their values - /// A human-friendly message describing why parsing failed. - /// - /// If the provided - /// - /// were parsed this method returns true. - /// - public static bool TryParse(string[] args, out CommandLineFlags flagStore, out string errorMessage) - { - errorMessage = string.Empty; - var props = GetFlagAttributes(); - - var stringBuilder = new IndentedStringBuilder(); - - stringBuilder.AppendLine("Usage:"); - stringBuilder.Indent(4); - foreach (var prop in props.Where(prop => !string.IsNullOrWhiteSpace(prop.Attribute.UsageExample))) - { - stringBuilder.AppendLine($"{ReservedWords.CompilerName} {prop.Attribute.UsageExample}"); - } - stringBuilder.Dedent(4); - - stringBuilder.AppendLine(string.Empty); - stringBuilder.AppendLine(string.Empty); - stringBuilder.AppendLine("Options:"); - stringBuilder.Indent(4); - foreach (var prop in props.Where(p => !p.Attribute.HideFromHelp)) - { - stringBuilder.AppendLine($"--{prop.Attribute.Name} {prop.Attribute.HelpText}"); - } - - flagStore = new CommandLineFlags(stringBuilder.ToString()); - - var parsedFlags = GetFlags(args); - - // prevent the user from passing both --langserv and --config - // and also for the compiler trying to find a config file when running as a language server - if (parsedFlags.HasFlag("langserv")) - { - flagStore.LanguageServer = true; - return true; - } - - - string? configPath; - if (parsedFlags.HasFlag("config")) - { - configPath = parsedFlags.GetFlag("config").GetValue(); - if (string.IsNullOrWhiteSpace(configPath)) - { - errorMessage = $"'--config' must be followed by the explicit path to a bebop.json config"; - return false; - } - } - else - { - configPath = FindBebopConfig(); - } - - var rootDirectory = !string.IsNullOrWhiteSpace(configPath) ? Path.GetDirectoryName(configPath) : Directory.GetCurrentDirectory(); - if (string.IsNullOrWhiteSpace(rootDirectory)) - { - errorMessage = "Failed to determine the working directory."; - return false; - } - - flagStore.WorkingDirectory = rootDirectory; - - - var parsedConfig = false; - // always parse the config, we'll override any values with command-line flags - if (!string.IsNullOrWhiteSpace(configPath)) - { - if (!new FileInfo(configPath).Exists) - { - errorMessage = $"Bebop configuration file not found at '{configPath}'"; - return false; - } - parsedConfig = TryParseConfig(flagStore, configPath); - if (!parsedConfig) - { - errorMessage = $"Failed to parse Bebop configuration file at '{configPath}'"; - return false; - } - } - - // if we didn't parse a 'bebop.json' and no flags were passed, we can't continue. - if (!parsedConfig && parsedFlags.Count == 0) - { - errorMessage = "No command-line flags found."; - return false; - } - - if (parsedFlags.HasFlag("help")) - { - flagStore.Help = true; - return true; - } - - if (parsedFlags.HasFlag("version")) - { - flagStore.Version = true; - return true; - } - - - var validFlagNames = props.Select(p => p.Attribute.Name).ToHashSet(); - if (parsedFlags.Find(x => !validFlagNames.Contains(x.Name)) is CommandLineFlag unrecognizedFlag) - { - errorMessage = $"Unrecognized flag: --{unrecognizedFlag.Name}"; - return false; - } - - // parse all present command-line flags - // any flag on the command-line that was also present in bebop.json will be overwritten. - foreach (var flag in props) - { - if (!parsedFlags.HasFlag(flag.Attribute.Name)) - { - continue; - } - - var parsedFlag = parsedFlags.GetFlag(flag.Attribute.Name); - var propertyType = flag.Property.PropertyType; - if (flag.Attribute.Name.Equals("check-schema")) - { - using var reader = new StreamReader(Console.OpenStandardInput()); - flagStore.CheckSchemaFile = reader.ReadToEnd(); - continue; - } - if (flag.Attribute.Name.Equals("in")) - { - using var reader = new StreamReader(Console.OpenStandardInput()); - flagStore.StandardInput = reader.ReadToEnd(); - continue; - } - if (propertyType == typeof(bool)) - { - flag.Property.SetValue(flagStore, true); - continue; - } - if (flag.Attribute.ValuesRequired && !parsedFlag.HasValues()) - { - errorMessage = $"command-line flag '{flag.Attribute.Name}' was not assigned any values."; - return false; - } - if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(List<>)) - { - Type itemType = propertyType.GetGenericArguments()[0]; - if (!(Activator.CreateInstance(typeof(List<>).MakeGenericType(itemType)) is IList genericList)) - { - errorMessage = $"Failed to activate '{flag.Property.Name}'."; - return false; - } - - if (flag.Attribute.Name.Equals("watch-excluded-directories") || flag.Attribute.Name.Equals("watch-excluded-files")) - { - var excluded = FindFiles(rootDirectory, parsedFlag.Values, Array.Empty()); - flag.Property.SetValue(flagStore, excluded, null); - } - else - { - // file paths wrapped in quotes may contain spaces. - foreach (var item in parsedFlag.Values) - { - if (string.IsNullOrWhiteSpace(item)) - { - continue; - } - // remove double quotes from the string so file paths can be parsed properly. - genericList.Add(Convert.ChangeType(item.Trim(), itemType)); - } - flag.Property.SetValue(flagStore, genericList, null); - } - } - else if (propertyType.IsEnum) - { - if (!Enum.TryParse(propertyType, parsedFlag.GetValue(), true, out var parsedEnum)) - { - errorMessage = $"Failed to parse '{parsedFlag.GetValue()}' into a member of '{propertyType}'."; - return false; - } - flag.Property.SetValue(flagStore, parsedEnum, null); - } - else if (propertyType == typeof(Version)) - { - if (System.Version.TryParse(parsedFlag.GetValue(), out var version) && version is Version) - { - flag.Property.SetValue(flagStore, version, null); - } - } - else - { - flag.Property.SetValue(flagStore, Convert.ChangeType(parsedFlag.GetValue(), flag.Property.PropertyType), - null); - } - } - errorMessage = string.Empty; - return true; - } - - - private static List FindFiles(string rootDirectory, string[] includes, string[] excludes) - { - var matcher = new Matcher(); - matcher.AddIncludePatterns(includes); - matcher.AddExcludePatterns(excludes); - IEnumerable matchingFiles = matcher.GetResultsInFullPath(rootDirectory); - return matchingFiles.ToList(); - } - - /// - /// Parses the bebop config file and assigns entries to their corresponding command-line flag. - /// - /// A instance. - /// The fully qualified path to the bebop config file, or null to trigger searching. - /// true if the config could be parsed without error, otherwise false. - private static bool TryParseConfig(CommandLineFlags flagStore, string? configPath) - { - if (string.IsNullOrWhiteSpace(configPath)) - { - return false; - } - var configFile = new FileInfo(configPath); - if (!configFile.Exists) - { - return false; - } - var configDirectory = configFile.DirectoryName; - if (string.IsNullOrWhiteSpace(configDirectory)) - { - return false; - } - - var bebopConfig = BebopConfig.FromJson(File.ReadAllText(configPath)); - if (bebopConfig is null) - { - return false; - } - - const string defaultIncludeGlob = "**/*.bop"; - var includeGlob = bebopConfig.Include ?? new string[] { defaultIncludeGlob }; - var excludeGlob = bebopConfig.Exclude ?? Array.Empty(); - flagStore.SchemaFiles = FindFiles(configDirectory, includeGlob, excludeGlob); - flagStore.Namespace = bebopConfig.Namespace; - if (bebopConfig.Generators is null) - { - return false; - } - foreach (var generator in bebopConfig.Generators) - { - if (generator.Alias is null || generator.OutFile is null) - { - return false; - } - foreach (var flagAttribute in GetFlagAttributes() - .Where(flagAttribute => flagAttribute.Attribute.IsGeneratorFlag && - flagAttribute.Attribute.Name.Equals(generator.Alias))) - { - flagAttribute.Property.SetValue(flagStore, Path.GetFullPath(Path.Combine(configDirectory, generator.OutFile))); - if (generator.Services.HasValue) - { - flagStore.ServiceConfigs[generator.Alias] = generator.Services.Value; - } - else - { - flagStore.ServiceConfigs[generator.Alias] = TempoServices.Both; - } - if (generator.LangVersion is not null && System.Version.TryParse(generator.LangVersion, out var version)) - { - foreach (var flag in GetFlagAttributes()) - { - if ($"{flagAttribute.Attribute.Name}-version".Equals(flag.Attribute.Name, StringComparison.OrdinalIgnoreCase)) - { - flag.Property.SetValue(flagStore, version, null); - } - } - } - if (generator.NoGenerationNotice is not null && generator.NoGenerationNotice.Value is true) { - flagStore.SkipGeneratedNotice = true; - } - if (generator.EmitBinarySchema is not null && generator.EmitBinarySchema is true) { - flagStore.EmitBinarySchema = true; - } - } - } - - if (bebopConfig.WatchOptions is not null) - { - if (bebopConfig.WatchOptions.ExcludeDirectories is not null) - { - flagStore.WatchExcludeDirectories = bebopConfig.WatchOptions.ExcludeDirectories.ToList(); - } - if (bebopConfig.WatchOptions.ExcludeFiles is not null) - { - flagStore.WatchExcludeFiles = bebopConfig.WatchOptions.ExcludeFiles.ToList(); - } - } - return true; - } - } - - #endregion -} \ No newline at end of file diff --git a/Compiler/Commands/BuildCommand.cs b/Compiler/Commands/BuildCommand.cs new file mode 100644 index 00000000..6a15e651 --- /dev/null +++ b/Compiler/Commands/BuildCommand.cs @@ -0,0 +1,86 @@ +using System; +using System.CommandLine; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Core.Exceptions; +using Core.Generators; +using Core.Logging; +using Core.Meta; + +namespace Compiler.Commands; + +public class BuildCommand : CliCommand +{ + public BuildCommand() : base(CliStrings.BuildCommand, "Build schemas into one or more target languages.") + { + SetAction(HandleCommandAsync); + } + + private Task HandleCommandAsync(ParseResult result, CancellationToken token) + { + var config = result.GetValue(CliStrings.ConfigFlag)!; + config.Validate(); + var compiler = new BebopCompiler(); + BebopSchema schema = default; + string? tempFilePath = null; + try + { + // if input is redirected, read from stdin, ignore includes. + // this is a temporary flag as it appears there is a bug in wasi builds + // that always returns true for Console.IsInputRedirected + if (result.GetValue(CliStrings.StandardInputFlag)) + { + // we should write the textual stream to a temp file and then parse it. + tempFilePath = Helpers.GetTempFileName(); + using var fs = File.Open(tempFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); + using var standardInput = Console.OpenStandardInput(); + // dont use async as wasi currently has threading issues + standardInput.CopyTo(fs); + fs.Seek(0, SeekOrigin.Begin); + schema = BebopCompiler.ParseSchema([tempFilePath]); + } + else + { + var resolvedSchemas = config.ResolveIncludes(); + if (!resolvedSchemas.Any()) + { + DiagnosticLogger.Instance.WriteDiagonstic(new CompilerException("No input files specified.")); + return Task.FromResult(BebopCompiler.Err); + } + schema = BebopCompiler.ParseSchema(resolvedSchemas); + } + + var (Warnings, Errors) = BebopCompiler.GetSchemaDiagnostics(schema, config.SupressedWarningCodes); + DiagnosticLogger.Instance.WriteSpanDiagonstics([.. Warnings, .. Errors]); + if (config.NoEmit) + { + return Task.FromResult(Errors.Count != 0 ? BebopCompiler.Err : BebopCompiler.Ok); + } + if (config is { Generators.Length: <= 0 }) + { + DiagnosticLogger.Instance.WriteDiagonstic(new CompilerException("No code generators specified.")); + return Task.FromResult(BebopCompiler.Err); + } + foreach (var generatorConfig in config.Generators) + { + BebopCompiler.Build(generatorConfig, schema, config); + } + return Task.FromResult(BebopCompiler.Ok); + } + catch (Exception ex) + { + DiagnosticLogger.Instance.WriteDiagonstic(ex); + return Task.FromResult(BebopCompiler.Err); + } + finally + { + if (tempFilePath is not null) + { + File.Delete(tempFilePath); + } + } + } +} \ No newline at end of file diff --git a/Compiler/Commands/LanguageServerCommand.cs b/Compiler/Commands/LanguageServerCommand.cs new file mode 100644 index 00000000..ac138970 --- /dev/null +++ b/Compiler/Commands/LanguageServerCommand.cs @@ -0,0 +1,23 @@ +using System.CommandLine; +using System.Threading; +using System.Threading.Tasks; +using Compiler.LangServer; + +namespace Compiler.Commands; + +public class LanguageServerCommand : CliCommand +{ + public LanguageServerCommand() : base(CliStrings.LangServerCommand, "Start the language server") + { + Hidden = true; + SetAction(HandleCommandAsync); + } + + private async Task HandleCommandAsync(ParseResult result, CancellationToken token) + { + #if !WASI_WASM_BUILD + await BebopLangServer.RunAsync(token); + #endif + return BebopCompiler.Ok; + } +} \ No newline at end of file diff --git a/Compiler/Commands/RootCommand.cs b/Compiler/Commands/RootCommand.cs new file mode 100644 index 00000000..443d08d4 --- /dev/null +++ b/Compiler/Commands/RootCommand.cs @@ -0,0 +1,70 @@ +using System.CommandLine; +using System.IO; +using Core.Logging; +using Core.Meta; + +namespace Compiler.Commands; + +public class RootCommand +{ + + public static int HandleCommand(ParseResult result) + { + var config = result.GetValue(CliStrings.ConfigFlag)!; + if (result.GetValue(CliStrings.InitFlag) is true) + { + return InitProject(); + } + if (result.GetValue(CliStrings.ListSchemasFlag) is true) + { + return ListSchemas(config); + } + if (result.GetValue(CliStrings.ShowConfigFlag) is true) + { + return ShowConfig(config); + } + return 0; + } + + /// + /// Shows the current configuration and stops processing. + /// + /// The bebop configuration object. + /// An integer representing the status of the operation. + private static int ShowConfig(BebopConfig config) + { + DiagnosticLogger.Instance.WriteLine(config.ToJson()); + return 0; + } + + /// + /// Lists all schemas defined in the configuration. + /// + /// The bebop configuration object. + /// An integer representing the status of the operation. + private static int ListSchemas(BebopConfig config) + { + foreach (var schema in config.ResolveIncludes()) + { + DiagnosticLogger.Instance.WriteLine(schema); + } + return 0; + } + + /// + /// Initializes a new project with a default configuration. + /// + /// The bebop configuration object to initialize the project with. + /// An integer representing the status of the operation. + private static int InitProject() + { + var workingDirectory = Directory.GetCurrentDirectory(); + var configPath = Path.Combine(workingDirectory, BebopConfig.ConfigFileName); + if (File.Exists(configPath)) + { + return 1; + } + File.WriteAllText(configPath, BebopConfig.Default.ToJson()); + return 0; + } +} \ No newline at end of file diff --git a/Compiler/Commands/WatchCommand.cs b/Compiler/Commands/WatchCommand.cs new file mode 100644 index 00000000..993988ed --- /dev/null +++ b/Compiler/Commands/WatchCommand.cs @@ -0,0 +1,22 @@ +using System.CommandLine; +using System.Threading; +using System.Threading.Tasks; +using Core.Meta; + +namespace Compiler.Commands; + +public class WatchCommand : CliCommand +{ + public WatchCommand() : base(CliStrings.WatchCommand, "Watch input files.") + { + SetAction(HandleCommandAsync); + } + + private async Task HandleCommandAsync(ParseResult result, CancellationToken token) + { + var config = result.GetValue(CliStrings.ConfigFlag)!; + config.Validate(); + var watcher = new SchemaWatcher(config.WorkingDirectory, config); + return await watcher.StartAsync(token); + } +} \ No newline at end of file diff --git a/Compiler/Compiler.csproj b/Compiler/Compiler.csproj index 2f3dc17b..47e57263 100644 --- a/Compiler/Compiler.csproj +++ b/Compiler/Compiler.csproj @@ -21,10 +21,15 @@ false + + + + - + + @@ -43,9 +48,7 @@ - - - + diff --git a/Compiler/Compiler.sln b/Compiler/Compiler.sln new file mode 100644 index 00000000..40f2bea4 --- /dev/null +++ b/Compiler/Compiler.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Compiler", "Compiler.csproj", "{6ACD7520-B667-4633-95DE-3CB2C252005A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6ACD7520-B667-4633-95DE-3CB2C252005A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6ACD7520-B667-4633-95DE-3CB2C252005A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6ACD7520-B667-4633-95DE-3CB2C252005A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6ACD7520-B667-4633-95DE-3CB2C252005A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {938C961D-5B5E-4954-AE3D-6C9AD2769225} + EndGlobalSection +EndGlobal diff --git a/Compiler/Helpers.cs b/Compiler/Helpers.cs index 760e44b6..8c19820a 100644 --- a/Compiler/Helpers.cs +++ b/Compiler/Helpers.cs @@ -1,7 +1,89 @@ +using System; +using System.CommandLine; +using System.IO; +#if WASI_WASM_BUILD +using Core.Exceptions; +#endif +using Core.Generators; +using Core.Meta; + namespace Compiler; public static class Helpers { + + /// + /// Creates a uniquely named, zero-byte temporary file on disk and returns the full path of that file. + /// + /// + public static string GetTempFileName() + { +#if WASI_WASM_BUILD + const string tempDirectory = "/tmp"; + try + { + if (!Directory.Exists(tempDirectory)) + { + Directory.CreateDirectory(tempDirectory); + } + var tempFileName = Path.Combine(tempDirectory, Path.GetRandomFileName()); + File.Create(tempFileName).Dispose(); + return tempFileName; + } + catch (Exception ex) + { + throw new CompilerException($"Could not create temporary file in {tempDirectory}.", ex); + } +#else + return Path.GetTempFileName(); +#endif + } + + /// + /// Merges the results of a bebopc command line parse into the bebop.json config instance. + /// + /// + /// When options are supplied on the command line, the corresponding bebop.json fields will be ignored. + /// + /// The parsed commandline. + public static void MergeConfig(ParseResult parseResults, BebopConfig config) + { + if (parseResults.GetValue(CliStrings.IncludeFlag) is { Length: > 0 } includes) + { + config.Includes = includes; + } + if (parseResults.GetValue(CliStrings.ExcludeFlag) is { Length: > 0 } excludes) + { + config.Excludes = excludes; + } +#if !WASI_WASM_BUILD + if (parseResults.GetValue(CliStrings.ExcludeDirectoriesFlag) is { Length: > 0 } watchExcludeDirectories) + { + config.WatchOptions.ExcludeDirectories = watchExcludeDirectories; + } + if (parseResults.GetValue(CliStrings.ExcludeFilesFlag) is { Length: > 0 } watchExcludeFiles) + { + config.WatchOptions.ExcludeFiles = watchExcludeFiles; + } + if (parseResults.GetValue(CliStrings.PreserveWatchOutputFlag) is true) + { + config.WatchOptions.PreserveWatchOutput = true; + } +#endif + if (parseResults.GetValue(CliStrings.NoWarnFlag) is { Length: > 0 } noWarn) + { + config.SupressedWarningCodes = noWarn; + } + if (parseResults.GetValue(CliStrings.NoEmitFlag) is true) + { + config.NoEmit = true; + } + if (parseResults.GetValue(CliStrings.GeneratorFlag) is { Length: > 0 } generators) + { + config.Generators = generators; + } + } + public static int ProcessId { get @@ -15,4 +97,4 @@ public static int ProcessId } } private static int? _processId; -} \ No newline at end of file +} diff --git a/Compiler/LangServer/BebopLangServer.cs b/Compiler/LangServer/BebopLangServer.cs index 61007705..349c72c8 100644 --- a/Compiler/LangServer/BebopLangServer.cs +++ b/Compiler/LangServer/BebopLangServer.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -9,27 +10,28 @@ namespace Compiler.LangServer { internal sealed class BebopLangServer { - public static async Task RunAsync() + public static async Task RunAsync(CancellationToken cancellationToken) { var server = await OmnisharpLanguageServer.From(options => - options - .WithInput(Console.OpenStandardInput()) - .WithOutput(Console.OpenStandardOutput()) - .WithLoggerFactory(new LoggerFactory()) - .AddDefaultLoggingProvider() - .WithServices(services => - { - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - }) - .WithHandler() - .WithHandler() - .WithHandler()); + options + .WithInput(Console.OpenStandardInput()) + .WithOutput(Console.OpenStandardOutput()) + .WithLoggerFactory(new LoggerFactory()) + .AddDefaultLoggingProvider() + .WithServices(services => + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + }) + .WithHandler() + .WithHandler() + .WithHandler(), cancellationToken); - await server.WaitForExit; + await server.WaitForExit; + } } } diff --git a/Compiler/LangServer/Handlers/CompletionHandler.cs b/Compiler/LangServer/Handlers/CompletionHandler.cs index 7a3b3ada..1c00c2a9 100644 --- a/Compiler/LangServer/Handlers/CompletionHandler.cs +++ b/Compiler/LangServer/Handlers/CompletionHandler.cs @@ -61,11 +61,11 @@ public Task Handle(CompletionParams request, CancellationToken c var buffer = _bufferManager.GetBuffer(request.TextDocument.Uri); if (buffer?.Schema != null) { - items.Add(new CompletionItem + /*items.Add(new CompletionItem { Label = buffer.Schema.Value.Namespace, Kind = CompletionItemKind.Reference, - }); + });*/ // TODO: Only top level definitions here? foreach (var definition in buffer.Schema.Value.Definitions) diff --git a/Compiler/LangServer/Handlers/TextDocumentSyncHandler.cs b/Compiler/LangServer/Handlers/TextDocumentSyncHandler.cs index 9c3d7a30..871e4470 100644 --- a/Compiler/LangServer/Handlers/TextDocumentSyncHandler.cs +++ b/Compiler/LangServer/Handlers/TextDocumentSyncHandler.cs @@ -57,20 +57,19 @@ protected override TextDocumentSyncRegistrationOptions CreateRegistrationOptions }; } - public override async Task Handle(DidOpenTextDocumentParams request, CancellationToken cancellationToken) + public override Task Handle(DidOpenTextDocumentParams request, CancellationToken cancellationToken) { _logger.LogInfo($"Opening document: {request.TextDocument.Uri}"); - await UpdateBufferAsync(request.TextDocument.Uri, request.TextDocument.Text, request.TextDocument.Version); - return Unit.Value; + UpdateBuffer(request.TextDocument.Uri, request.TextDocument.Text, request.TextDocument.Version); + return Task.FromResult(Unit.Value); } - public override async Task Handle(DidChangeTextDocumentParams request, CancellationToken cancellationToken) + public override Task Handle(DidChangeTextDocumentParams request, CancellationToken cancellationToken) { var text = request.ContentChanges.FirstOrDefault()?.Text ?? string.Empty; - await UpdateBufferAsync(request.TextDocument.Uri, text, request.TextDocument.Version); - - return Unit.Value; + UpdateBuffer(request.TextDocument.Uri, text, request.TextDocument.Version); + return Task.FromResult(Unit.Value); } public override Task Handle(DidSaveTextDocumentParams request, CancellationToken cancellationToken) @@ -110,11 +109,11 @@ public override Task Handle(DidCloseTextDocumentParams request, Cancellati return Unit.Task; } - private async Task UpdateBufferAsync(DocumentUri uri, string text, int? version) + private void UpdateBuffer(DocumentUri uri, string text, int? version) { try { - var schema = await ParseSchemaAsync(uri, text, version); + var schema = ParseSchema(uri, text, version); _bufferManager.UpdateBuffer(uri, new Buffer(schema, text, version)); } catch (Exception ex) @@ -127,9 +126,9 @@ private async Task UpdateBufferAsync(DocumentUri uri, string text, int? version) } } - private async Task ParseSchemaAsync(DocumentUri uri, string text, int? version) + private BebopSchema ParseSchema(DocumentUri uri, string text, int? version) { - var (schema, errors) = await ParseSchemaAsync(uri, text); + var (schema, errors) = ParseSchema(uri, text); // TODO: Don't count indirect errors here. // If there only is indirect errors (from an import), @@ -148,18 +147,18 @@ private async Task ParseSchemaAsync(DocumentUri uri, string text, i return schema; } - private async Task<(BebopSchema, List)> ParseSchemaAsync(DocumentUri uri, string text) + private (BebopSchema, List) ParseSchema(DocumentUri uri, string text) { var diagnostics = new List(); try { - var parser = new SchemaParser(text, DocumentUri.GetFileSystemPath(uri) ?? string.Empty) + var parser = new SchemaParser(text) { ImportResolver = new BebopLangServerImportResolver(uri, _logger) }; - var schema = await parser.Parse(); + var schema = parser.Parse(); // Perform validation PerformValidation(ref schema); diff --git a/Compiler/Options/BebopConfigOption.cs b/Compiler/Options/BebopConfigOption.cs new file mode 100644 index 00000000..53432e4a --- /dev/null +++ b/Compiler/Options/BebopConfigOption.cs @@ -0,0 +1,49 @@ +using System; +using System.CommandLine; +using System.CommandLine.Parsing; +using System.IO; +using System.Linq; +using Core.Logging; +using Core.Meta; +using Errata; + +namespace Compiler.Options +{ + /// + /// Represents a command line option for specifying Bebop configuration. + /// + public class BebopConfigOption : CliOption + { + /// + /// Initializes a new instance of the class. + /// + public BebopConfigOption() : base(name: CliStrings.ConfigFlag, aliases: ["-c"]) + { + Description = "Compile the project given the path to its configuration file."; + AllowMultipleArgumentsPerToken = false; + AcceptLegalFilePathsOnly(); + + Validators.Add(static result => + { + var token = result.Tokens.SingleOrDefault()?.Value?.Trim(); + if (string.IsNullOrWhiteSpace(token)) return; + if (new FileInfo(token).Exists) return; + result.AddError($"Specified config '{token}' does not exist."); + }); + + DefaultValueFactory = (_) => + { + var path = BebopConfig.Locate(); + if (string.IsNullOrEmpty(path)) return BebopConfig.Default; + return BebopConfig.FromFile(path); + }; + + CustomParser = new((ArgumentResult result) => + { + var configPath = result.Tokens.SingleOrDefault()?.Value; + var config = new FileInfo(configPath!); + return BebopConfig.FromFile(config.FullName); + }); + } + } +} \ No newline at end of file diff --git a/Compiler/Options/DiagnosticFormatOption.cs b/Compiler/Options/DiagnosticFormatOption.cs new file mode 100644 index 00000000..ba91e015 --- /dev/null +++ b/Compiler/Options/DiagnosticFormatOption.cs @@ -0,0 +1,71 @@ +using System; +using System.CommandLine; +using System.CommandLine.Parsing; +using System.Linq; +using Core.Logging; + +namespace Compiler.Options +{ + /// + /// Represents a command line option for specifying the format of diagnostic messages. + /// + public class DiagnosticFormatOption : CliOption + { + /// + /// Initializes a new instance of the class. + /// + public DiagnosticFormatOption() : base(name: CliStrings.DiagnosticFormatFlag, aliases: ["-df"]) + { + Description = "Specifies the format which diagnostic messages are printed in."; + AllowMultipleArgumentsPerToken = false; + Validators.Add(result => + { + var token = result.Tokens.SingleOrDefault()?.Value?.Trim(); + if (string.IsNullOrWhiteSpace(token)) return; + if (!IsLogFormatter(token)) + { + result.AddError($"Invalid diagnostic format '{token}'."); + return; + } + }); + + CustomParser = new((ArgumentResult result) => + { + var token = result.Tokens.SingleOrDefault()?.Value?.Trim(); + return Parse(token); + }); + + + DefaultValueFactory = (a) => + { + return LogFormatter.Enhanced; + }; + } + + private static bool IsLogFormatter(string? token) + { + if (string.IsNullOrWhiteSpace(token)) return false; + return token.ToLowerInvariant() switch + { + "enhanced" => true, + "json" => true, + "structured" => true, + "msbuild" => true, + _ => false, + }; + } + + private static LogFormatter Parse(string? token) + { + if (string.IsNullOrWhiteSpace(token)) return LogFormatter.Enhanced; + return token.ToLowerInvariant() switch + { + "enhanced" => LogFormatter.Enhanced, + "json" => LogFormatter.JSON, + "structured" => LogFormatter.Structured, + "msbuild" => LogFormatter.MSBuild, + _ => throw new ArgumentException($"Invalid diagnostic format '{token}'."), + }; + } + } +} \ No newline at end of file diff --git a/Compiler/Options/GeneratorOption.cs b/Compiler/Options/GeneratorOption.cs new file mode 100644 index 00000000..4b50ceb5 --- /dev/null +++ b/Compiler/Options/GeneratorOption.cs @@ -0,0 +1,116 @@ +using System; +using System.CommandLine; +using System.CommandLine.Parsing; +using System.IO; +using System.Linq; +using Core.Generators; +using Core.Meta.Extensions; +namespace Compiler.Options +{ + /// + /// Represents a command line option for specifying generator configurations. + /// + public class GeneratorOption : CliOption + { + /// + /// Initializes a new instance of the class. + /// + public GeneratorOption() : base(name: CliStrings.GeneratorFlag, ["-g"]) + { + Description = "Specifies code generators to use for compilation."; + AllowMultipleArgumentsPerToken = false; + CustomParser = new((ArgumentResult result) => + { + if (result.Tokens.Count == 0) + { + return []; + } + return result.Tokens.Select(t => ParseGeneratorToken(t.Value, result)).Where(c => c is not null).Select(c => c!).ToArray(); + }); + } + + /// + /// Parses a generator token into a object. + /// + /// The generator token to parse. + /// The argument result to which errors can be added. + /// A object if the token could be parsed; otherwise, null. + private static GeneratorConfig? ParseGeneratorToken(string token, ArgumentResult result) + { + var parts = token.Split(':'); + if (parts.Length != 2) + { + result.AddError($"Incomplete generator token specified '{token}'."); + return null; + } + + var generatorAlias = parts[0].Trim().ToLower(); + if (!GeneratorUtils.ImplementedGeneratorNames.ContainsKey(generatorAlias)) + { + result.AddError($"Unknown generator alias '{generatorAlias}'."); + return null; + } + var remaining = parts[1].Split(','); + var outputPath = remaining[0] ?? string.Empty; + + + // If the output path is 'stdout', no need to validate it as a file path + if (!string.Equals("stdout", outputPath, StringComparison.OrdinalIgnoreCase) && !outputPath.IsLegalFilePath(out var invalidCharacterIndex)) + { + if (invalidCharacterIndex >= 0) + { + result.AddError($"Invalid character '{outputPath[invalidCharacterIndex]}' in output path of generator '{generatorAlias}': '{outputPath}'."); + return null; + } + } + + var additionalOptions = remaining.Skip(1).Select(s => s.Split('=')).Where(p => p.Length == 2).ToDictionary(p => p[0].Trim(), p => p[1].Trim()); + + // Initialize default values + var services = TempoServices.Both; + var emitNotice = true; + var emitBinarySchema = true; + string @namespace = string.Empty; + + foreach (var option in additionalOptions) + { + switch (option.Key.ToLower()) + { + case "services": + if (!Enum.TryParse(option.Value, true, out var parsedServices)) + { + result.AddError($"Invalid value '{option.Value}' for option 'services'."); + break; + } + services = parsedServices; + break; + case "emitnotice": + if (!bool.TryParse(option.Value, out var parsedEmitNotice)) + { + result.AddError($"Invalid value '{option.Value}' for option 'emitNotice'."); + break; + } + emitNotice = parsedEmitNotice; + break; + case "emitbinaryschema": + if (!bool.TryParse(option.Value, out var parsedEmitBinarySchema)) + { + result.AddError($"Invalid value '{option.Value}' for option 'emitBinarySchema'."); + break; + } + emitBinarySchema = parsedEmitBinarySchema; + break; + case "namespace": + if (string.IsNullOrWhiteSpace(option.Value)) + { + result.AddError($"Invalid value '{option.Value}' for option 'namespace'."); + break; + } + @namespace = option.Value; + break; + } + } + return new GeneratorConfig(generatorAlias, outputPath, services, emitNotice, @namespace, emitBinarySchema, additionalOptions); + } + } +} \ No newline at end of file diff --git a/Compiler/Options/SimpleOptions.cs b/Compiler/Options/SimpleOptions.cs new file mode 100644 index 00000000..667714f4 --- /dev/null +++ b/Compiler/Options/SimpleOptions.cs @@ -0,0 +1,154 @@ +using System.CommandLine; + +namespace Compiler.Options +{ + /// + /// Represents a command line option for including certain schemas. + /// + public class IncludeOption : CliOption + { + public IncludeOption() : base( + name: CliStrings.IncludeFlag, + aliases: ["-i"]) + { + Description = "Specifies an array of filenames or patterns to include in the compiler. These filenames are resolved relative to the directory containing the bebop.json file."; + AllowMultipleArgumentsPerToken = true; + } + } + + /// + /// Represents a command line option for excluding certain schemas. + /// + public class ExcludeOption : CliOption + { + public ExcludeOption() : base( + name: CliStrings.ExcludeFlag, + aliases: ["-e"]) + { + Description = "Specifies an array of filenames or patterns that should be skipped when resolving include."; + AllowMultipleArgumentsPerToken = true; + } + } + + /// + /// Represents a command line option for excluding certain directories from being watched. + /// + public class WatchExcludeDirectoriesOption : CliOption + { + public WatchExcludeDirectoriesOption() : base( + name: CliStrings.ExcludeDirectoriesFlag) + { + Description = "Remove a list of directories from the watch process."; + AllowMultipleArgumentsPerToken = true; + } + } + + /// + /// Represents a command line option for excluding certain files from being watched. + /// + public class WatchExcludeFilesOption : CliOption + { + public WatchExcludeFilesOption() : base( + name: CliStrings.ExcludeFilesFlag) + { + Description = "Remove a list of files from the watch mode's processing."; + AllowMultipleArgumentsPerToken = true; + } + } + + public class PreserveWatchOutputOption : CliOption + { + public PreserveWatchOutputOption() : base( + name: CliStrings.PreserveWatchOutputFlag) + { + Description = "Disable wiping the console in watch mode."; + AllowMultipleArgumentsPerToken = false; + } + } + + /// + /// Represents a command line option for disabling the emission of files. + /// + public class NoEmitOption : CliOption + { + public NoEmitOption() : base( + name: CliStrings.NoEmitFlag) + { + Description = "Disable emitting files from a compilation."; + AllowMultipleArgumentsPerToken = false; + } + } + + /// + /// Represents a command line option for suppressing specific warnings. + /// + public class NoWarnOption : CliOption + { + public NoWarnOption() : base( + name: CliStrings.NoWarnFlag) + { + Description = "Suppresses the specified warning codes from reports during compilation."; + AllowMultipleArgumentsPerToken = true; + } + } + + public class InitOption : CliOption + { + public InitOption() : base( + name: CliStrings.InitFlag) + { + Description = "Initializes a Bebop project and creates a bebop.json file."; + AllowMultipleArgumentsPerToken = false; + } + } + + public class ListSchemaOption : CliOption + { + public ListSchemaOption() : base( + name: CliStrings.ListSchemasFlag) + { + Description = "Print names of schemas that are part of the compilation and then stop processing."; + AllowMultipleArgumentsPerToken = false; + } + } + + public class ShowConfigOption : CliOption + { + public ShowConfigOption() : base( + name: CliStrings.ShowConfigFlag) + { + Description = "Print the final configuration instead of building."; + AllowMultipleArgumentsPerToken = false; + } + } + + public class LocaleOption : CliOption + { + public LocaleOption() : base( + name: CliStrings.LocaleFlag) + { + Description = "Set the language of the messaging from bebopc. This does not affect emit."; + AllowMultipleArgumentsPerToken = false; + } + } + + public class TraceOption : CliOption + { + public TraceOption() : base( + name: CliStrings.TraceFlag) + { + Description = "Enable tracing of the compiler."; + AllowMultipleArgumentsPerToken = false; + } + } + + public class StandardInputOption : CliOption + { + public StandardInputOption() : base( + name: CliStrings.StandardInputFlag) + { + Description = "Read a schema from standard input."; + AllowMultipleArgumentsPerToken = false; + } + } +} \ No newline at end of file diff --git a/Compiler/Program.cs b/Compiler/Program.cs index 1cc8015e..be7ad625 100644 --- a/Compiler/Program.cs +++ b/Compiler/Program.cs @@ -1,219 +1,94 @@ using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; +using System.CommandLine; using System.Runtime.InteropServices; -using System.IO; using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Compiler.LangServer; -using Core.Exceptions; -using Core.Generators; +using Compiler.Commands; +using Compiler.Options; using Core.Logging; using Core.Meta; +using System.Text.Json; +using Core.Exceptions; +using Compiler; +using System.IO; -namespace Compiler +if (RuntimeInformation.OSArchitecture is Architecture.Wasm) { - internal class Program - { - private static CommandLineFlags? _flags; + Environment.SetEnvironmentVariable("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "1"); +} +Console.OutputEncoding = System.Text.Encoding.UTF8; +DiagnosticLogger.Initialize(LogFormatter.Enhanced); - private static void WriteHelpText() +try +{ + var rootCommand = new CliRootCommand("The Bebop schema language compiler") + { + new DiagnosticFormatOption(), + new BebopConfigOption(), + new IncludeOption(), + new ExcludeOption(), + new InitOption(), + new ListSchemaOption(), + new ShowConfigOption(), + new LocaleOption(), + new TraceOption(), + #if !WASI_WASM_BUILD + new LanguageServerCommand(), + #endif + new BuildCommand() { - if (_flags is not null) - { - DiagnosticLogger.Instance.WriteLine(string.Empty); - DiagnosticLogger.Instance.WriteLine(_flags.HelpText); - } - } - - private static async Task Main() + new GeneratorOption(), + new NoEmitOption(), + new NoWarnOption(), + new StandardInputOption() + }, + #if !WASI_WASM_BUILD + new WatchCommand() { - if (RuntimeInformation.OSArchitecture is Architecture.Wasm) - { - Environment.SetEnvironmentVariable("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "1"); - } - System.Console.OutputEncoding = System.Text.Encoding.UTF8; - DiagnosticLogger.Initialize(CommandLineFlags.FindLogFormatter(Environment.GetCommandLineArgs())); - - try - { - if (!CommandLineFlags.TryParse(Environment.GetCommandLineArgs(), out _flags, out var message)) - { - DiagnosticLogger.Instance.WriteDiagonstic(new CompilerException(message)); - return BebopCompiler.Err; - } - - if (_flags.Quiet) - { - DiagnosticLogger.Instance.SuppressDiagnostics(); - } -#if !WASI_WASM_BUILD - if (_flags.Debug) - { - - DiagnosticLogger.Instance.WriteLine($"Waiting for debugger to attach (PID={Helpers.ProcessId})..."); - // Wait 5 minutes for a debugger to attach - var timeoutToken = new CancellationTokenSource(TimeSpan.FromMinutes(5)).Token; - while (!Debugger.IsAttached) - { - await Task.Delay(100, timeoutToken); - } - Debugger.Break(); - } -#endif - - if (_flags.Version) - { - DiagnosticLogger.Instance.WriteLine($"{ReservedWords.CompilerName} {DotEnv.Generated.Environment.Version}"); - return BebopCompiler.Ok; - } - - if (_flags.Help) - { - WriteHelpText(); - return BebopCompiler.Ok; - } -#if !WASI_WASM_BUILD - if (_flags.LanguageServer) - { - await BebopLangServer.RunAsync(); - return BebopCompiler.Ok; - } -#endif - - var compiler = new BebopCompiler(_flags); + new GeneratorOption(), + new WatchExcludeDirectoriesOption(), + new WatchExcludeFilesOption(), + new NoEmitOption(), + new NoWarnOption(), + new PreserveWatchOutputOption(), + }, + #endif + }; - if (_flags.CheckSchemaFile is not null) - { - if (string.IsNullOrWhiteSpace(_flags.CheckSchemaFile)) - { - DiagnosticLogger.Instance.WriteDiagonstic(new CompilerException("No textual schema was read from standard input.")); - return BebopCompiler.Err; - } - return await compiler.CheckSchema(_flags.CheckSchemaFile); - } + rootCommand.SetAction(RootCommand.HandleCommand); - if (_flags.StandardInput is not null) - { - if (string.IsNullOrWhiteSpace(_flags.StandardInput)) - { - DiagnosticLogger.Instance.WriteDiagonstic(new CompilerException("No textual schema was read from standard input.")); - return BebopCompiler.Err; - } - } + var results = rootCommand.Parse(args); + results.Configuration.EnableDefaultExceptionHandler = false; + results.Configuration.ProcessTerminationTimeout = null; + if (results.GetValue(CliStrings.TraceFlag)) + { + DiagnosticLogger.Instance.EnableTrace(); + } - List? paths = null; - - if (_flags.SchemaDirectory is not null) - { - paths = new DirectoryInfo(_flags.SchemaDirectory!) - .GetFiles($"*.{ReservedWords.SchemaExt}", SearchOption.AllDirectories) - .Select(f => f.FullName) - .ToList(); - } - else if (_flags.SchemaFiles is not null) - { - paths = _flags.SchemaFiles; - } - - if (_flags.CheckSchemaFiles is not null) - { - if (_flags.CheckSchemaFiles.Count > 0) - { - return await compiler.CheckSchemas(_flags.CheckSchemaFiles); - } - // Fall back to the paths defined by the config if none specified - else if (paths is not null && paths.Count > 0) - { - return await compiler.CheckSchemas(paths); - } - DiagnosticLogger.Instance.WriteDiagonstic(new CompilerException("No schemas specified in check.")); - return BebopCompiler.Err; - } - - if (!_flags.GetParsedGenerators().Any()) - { - DiagnosticLogger.Instance.WriteDiagonstic(new CompilerException("No code generators were specified.")); - return BebopCompiler.Err; - } - - if (_flags.SchemaDirectory is not null && _flags.SchemaFiles is not null) - { - DiagnosticLogger.Instance.WriteDiagonstic( - new CompilerException("Can't specify both an input directory and individual input files")); - return BebopCompiler.Err; - } -#if !WASI_WASM_BUILD - if (_flags.Watch) - { - - var watcher = new Watcher(_flags.WorkingDirectory, compiler, _flags.PreserveWatchOutput); - - if (_flags.WatchExcludeDirectories is not null) - { - watcher.AddExcludeDirectories(_flags.WatchExcludeDirectories); - } - if (_flags.WatchExcludeFiles is not null) - { - watcher.AddExcludeFiles(_flags.WatchExcludeFiles); - } - return await watcher.StartAsync(); - } -#endif - - if (_flags.StandardInput is null) - { - if (paths is null) - { - DiagnosticLogger.Instance.WriteDiagonstic(new CompilerException("Specify one or more input files with --dir or --files.")); - return BebopCompiler.Err; - } - if (paths.Count == 0) - { - DiagnosticLogger.Instance.WriteDiagonstic(new CompilerException("No input files were found at the specified target location.")); - return BebopCompiler.Err; - } - } - // Everything below this point requires paths - foreach (var parsedGenerator in _flags.GetParsedGenerators()) - { - if (!GeneratorUtils.ImplementedGenerators.ContainsKey(parsedGenerator.Alias)) - { - DiagnosticLogger.Instance.WriteDiagonstic(new CompilerException($"'{parsedGenerator.Alias}' is not a recognized code generator")); - return BebopCompiler.Err; - } - if (string.IsNullOrWhiteSpace(parsedGenerator.OutputFile)) - { - DiagnosticLogger.Instance.WriteDiagonstic(new CompilerException("No output file was specified.")); - return BebopCompiler.Err; - } - int result = -1; - if (_flags.StandardInput is not null) - { - result = await compiler.CompileSchema(GeneratorUtils.ImplementedGenerators[parsedGenerator.Alias], _flags.StandardInput, parsedGenerator.OutputFile, _flags.Namespace ?? string.Empty, parsedGenerator.Services, parsedGenerator.LangVersion); - } - else - { - result = await compiler.CompileSchema(GeneratorUtils.ImplementedGenerators[parsedGenerator.Alias], paths, new FileInfo(parsedGenerator.OutputFile), _flags.Namespace ?? string.Empty, parsedGenerator.Services, parsedGenerator.LangVersion); - } - if (result != BebopCompiler.Ok) - { - return result; - } - } - return BebopCompiler.Ok; + var formatter = results.GetValue(CliStrings.DiagnosticFormatFlag); + DiagnosticLogger.Instance.SetFormatter(formatter ?? LogFormatter.Enhanced); - } - catch (Exception e) - { - DiagnosticLogger.Instance.WriteDiagonstic(e); - return BebopCompiler.Err; - } + if (!results.Errors.Any()) + { + var parsedConfig = results.GetValue(CliStrings.ConfigFlag); + if (parsedConfig is not null) + { + Helpers.MergeConfig(results, parsedConfig); } - } + return await results.InvokeAsync(); } +catch (Exception e) +{ + switch (e) + { + case JsonException: + DiagnosticLogger.Instance.WriteDiagonstic(new CompilerException("Invalid JSON in config file.", e)); + break; + default: + DiagnosticLogger.Instance.WriteDiagonstic(e); + break; + } + return BebopCompiler.Err; +} \ No newline at end of file diff --git a/Compiler/TrimmerRoots.xml b/Compiler/TrimmerRoots.xml deleted file mode 100644 index fc391f0a..00000000 --- a/Compiler/TrimmerRoots.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Compiler/Watcher.cs b/Compiler/Watcher.cs index 27a51def..2379699d 100644 --- a/Compiler/Watcher.cs +++ b/Compiler/Watcher.cs @@ -7,19 +7,21 @@ using Compiler; using Core.Generators; using Core.Logging; +using Core.Meta; using Microsoft.Extensions.FileSystemGlobbing; using Spectre.Console; -public class Watcher +public class SchemaWatcher { private readonly string _watchDirectory; private readonly FileSystemWatcher _filewatcher; private List _excludeDirectories = new List(); private List _excludeFiles = new List(); private List _trackedFiles = new List(); - private readonly BebopCompiler _compiler; + private readonly BebopConfig _config; private readonly bool _preserveWatchOutput; - private readonly TaskCompletionSource _tcs; + private TaskCompletionSource? _tcs; + private CancellationToken _cancellationToken; private readonly SemaphoreSlim _compileSemaphore = new SemaphoreSlim(1, 1); private readonly TimeSpan _recompileTimeout = TimeSpan.FromSeconds(2); @@ -29,12 +31,11 @@ public class Watcher /// ///The directory to watch. ///The list of files to track. - public Watcher(string watchDirectory, BebopCompiler compiler, bool preserveWatchOutput) + public SchemaWatcher(string watchDirectory, BebopConfig config) { - _compiler = compiler; - _preserveWatchOutput = preserveWatchOutput; - _tcs = new TaskCompletionSource(); - _trackedFiles = compiler.Flags.SchemaFiles!; + _config = config; + _preserveWatchOutput = config.WatchOptions.PreserveWatchOutput; + _trackedFiles = config.ResolveIncludes().ToList(); _watchDirectory = watchDirectory; _filewatcher = new FileSystemWatcher(); _filewatcher.Path = _watchDirectory; @@ -54,8 +55,12 @@ public Watcher(string watchDirectory, BebopCompiler compiler, bool preserveWatch /// Starts the watcher. /// ///Returns a Task representing the watcher operation. - public async Task StartAsync() + public async Task StartAsync(CancellationToken cancellationToken = default) { + if (_tcs is not null) throw new InvalidOperationException("Watcher is already running."); + _cancellationToken = cancellationToken; + _tcs = new TaskCompletionSource(_cancellationToken); + var table = new Table().HeavyBorder().BorderColor(Color.Grey).Title("Watching").RoundedBorder(); table.AddColumns("[white]Status[/]", "[white]Path[/]"); table.AddRow(new Text("Watching", new Style(Color.Green)), new TextPath(_watchDirectory)); @@ -73,7 +78,14 @@ public async Task StartAsync() table.AddRow(new Text(""), new TextPath(excludeFile)); } DiagnosticLogger.Instance.WriteTable(table); - return await _tcs.Task; + try { return await _tcs.Task; } + catch (OperationCanceledException) + { + // Handle the cancellation + _filewatcher.EnableRaisingEvents = false; + _filewatcher.Dispose(); + return 0; + } } /// @@ -82,7 +94,7 @@ public async Task StartAsync() private void OnError(object sender, ErrorEventArgs e) { DiagnosticLogger.Instance.WriteDiagonstic(e.GetException()); - _tcs.TrySetResult(1); + _tcs?.TrySetResult(1); } /// @@ -122,7 +134,7 @@ public void AddExcludeFiles(List excludeFiles) /// /// Handles the Renamed event of the FileSystemWatcher. /// - private async void FileRenamed(object sender, RenamedEventArgs e) + private void FileRenamed(object sender, RenamedEventArgs e) { string oldFullPath = Path.GetFullPath(e.OldFullPath); string newFullPath = Path.GetFullPath(e.FullPath); @@ -136,7 +148,7 @@ private async void FileRenamed(object sender, RenamedEventArgs e) { // The file or directory may have been deleted or is inaccessible DiagnosticLogger.Instance.WriteDiagonstic(ex); - _tcs.SetResult(1); + _tcs?.SetResult(1); return; } @@ -199,7 +211,7 @@ private async void FileRenamed(object sender, RenamedEventArgs e) } } LogEvent("[orangered1]Schema renamed. Start recompile[/]", e.OldFullPath, e.FullPath); - await CompileSchemas(); + CompileSchemas(_cancellationToken); } @@ -207,7 +219,7 @@ private async void FileRenamed(object sender, RenamedEventArgs e) /// /// Handles the Deleted event of the FileSystemWatcher. /// - private async void FileDeleted(object sender, FileSystemEventArgs e) + private void FileDeleted(object sender, FileSystemEventArgs e) { if (!Path.GetExtension(e.FullPath).Equals(".bop", StringComparison.InvariantCultureIgnoreCase) || IsPathExcluded(e.FullPath)) { @@ -216,13 +228,13 @@ private async void FileDeleted(object sender, FileSystemEventArgs e) _trackedFiles.Remove(e.FullPath); LogEvent("[indianred1]Schema deleted. Starting recompile[/]", e.FullPath); - await CompileSchemas(); + CompileSchemas(_cancellationToken); } /// /// Handles the Created event of the FileSystemWatcher. /// - private async void FileCreated(object sender, FileSystemEventArgs e) + private void FileCreated(object sender, FileSystemEventArgs e) { if (!Path.GetExtension(e.FullPath).Equals(".bop", StringComparison.InvariantCultureIgnoreCase) || IsPathExcluded(e.FullPath)) { @@ -240,7 +252,7 @@ private async void FileCreated(object sender, FileSystemEventArgs e) /// /// Handles the Changed event of the FileSystemWatcher. /// - private async void FileChanged(object sender, FileSystemEventArgs e) + private void FileChanged(object sender, FileSystemEventArgs e) { if (!Path.GetExtension(e.FullPath).Equals(".bop", StringComparison.InvariantCultureIgnoreCase) || IsPathExcluded(e.FullPath)) { @@ -250,12 +262,12 @@ private async void FileChanged(object sender, FileSystemEventArgs e) // Handle file changes if (_trackedFiles.Contains(e.FullPath)) { - if (await _compileSemaphore.WaitAsync(_recompileTimeout)) + if (_compileSemaphore.Wait(_recompileTimeout, _cancellationToken)) { try { LogEvent("[blue]Schema changed. Starting recompile[/]", e.FullPath); - var result = await CompileSchemas(); + var result = CompileSchemas(_cancellationToken); if (result is BebopCompiler.Ok) { LogEvent("[green]Schema recompilation succeeded. Resuming watch.[/]"); @@ -341,7 +353,7 @@ private void LogEvent(string message, string? filePath = null, string? newFilePa /// An integer representing the compilation result. Returns BebopCompiler.Ok if the compilation is successful, /// otherwise, returns BebopCompiler.Err. /// - public async Task CompileSchemas() + public int CompileSchemas(CancellationToken cancellationToken = default) { try { @@ -349,23 +361,21 @@ public async Task CompileSchemas() { DiagnosticLogger.Instance.Error.Clear(); } - foreach (var parsedGenerator in _compiler.Flags.GetParsedGenerators()) + if (_trackedFiles.Count == 0) { - if (!GeneratorUtils.ImplementedGenerators.ContainsKey(parsedGenerator.Alias)) - { - LogEvent($"[red]The alias '{parsedGenerator.Alias}' is not a recognized code generator[/]", isError: true); - return BebopCompiler.Err; - } - if (string.IsNullOrWhiteSpace(parsedGenerator.OutputFile)) - { - LogEvent($"[red]No out file was specified for generator '{parsedGenerator.Alias}'[/]", isError: true); - return BebopCompiler.Err; - } - var result = await _compiler.CompileSchema(GeneratorUtils.ImplementedGenerators[parsedGenerator.Alias], _trackedFiles, new FileInfo(parsedGenerator.OutputFile), _compiler.Flags.Namespace ?? string.Empty, parsedGenerator.Services, parsedGenerator.LangVersion); - if (result != BebopCompiler.Ok) - { - return result; - } + LogEvent("[yellow]Recompile skipped. No schemas being tracked.[/]"); + return BebopCompiler.Ok; + } + var schema = BebopCompiler.ParseSchema(_trackedFiles); + var (warnings, errors) = BebopCompiler.GetSchemaDiagnostics(schema, _config.SupressedWarningCodes); + DiagnosticLogger.Instance.WriteSpanDiagonstics([.. warnings, .. errors]); + if (_config.NoEmit) + { + return errors.Count != 0 ? BebopCompiler.Err : BebopCompiler.Ok; + } + foreach (var generatorConfig in _config.Generators) + { + BebopCompiler.Build(generatorConfig, schema, _config); } return BebopCompiler.Ok; } @@ -374,5 +384,6 @@ public async Task CompileSchemas() DiagnosticLogger.Instance.WriteDiagonstic(ex); return BebopCompiler.Err; } + } } \ No newline at end of file diff --git a/Compiler/nuget.config b/Compiler/nuget.config new file mode 100644 index 00000000..46175792 --- /dev/null +++ b/Compiler/nuget.config @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Core/Core.csproj b/Core/Core.csproj index 00f3fff7..f50bef80 100644 --- a/Core/Core.csproj +++ b/Core/Core.csproj @@ -34,6 +34,7 @@ all + diff --git a/Core/Core.sln b/Core/Core.sln new file mode 100644 index 00000000..10efd14c --- /dev/null +++ b/Core/Core.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core", "Core.csproj", "{EE6FD49B-E1D9-47C0-A86B-741B0A3DB2BB}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EE6FD49B-E1D9-47C0-A86B-741B0A3DB2BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE6FD49B-E1D9-47C0-A86B-741B0A3DB2BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EE6FD49B-E1D9-47C0-A86B-741B0A3DB2BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE6FD49B-E1D9-47C0-A86B-741B0A3DB2BB}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {41DD72E6-C309-4605-B148-4EF928302A7F} + EndGlobalSection +EndGlobal diff --git a/Core/Exceptions/Exceptions.cs b/Core/Exceptions/Exceptions.cs index 53bee033..a7556390 100644 --- a/Core/Exceptions/Exceptions.cs +++ b/Core/Exceptions/Exceptions.cs @@ -23,7 +23,23 @@ public class CompilerException : Exception public CompilerException(string message) : base(message) { } + public CompilerException(string message, Exception innerException) : base(message, innerException) + { + } + } + + [Serializable] + public class MalformedBebopConfigException : Exception + { + /// + /// A unique error code identifying the type of exception + /// + public int ErrorCode => 601; + public MalformedBebopConfigException(string message) : base($"Error parsing bebop.json: {message}") + { + } } + [Serializable] public class SpanException : Exception { diff --git a/Core/Generators/BaseGenerator.cs b/Core/Generators/BaseGenerator.cs index 0d48b6f4..c5e7442b 100644 --- a/Core/Generators/BaseGenerator.cs +++ b/Core/Generators/BaseGenerator.cs @@ -3,43 +3,54 @@ namespace Core.Generators { + /// + /// Represents an abstract base class for generating code from Bebop schemas. + /// This class encapsulates the common functionalities needed for various code generators. + /// public abstract class BaseGenerator { - - /// - /// The schema to generate code from. + /// The Bebop schema from which the code is generated. /// protected BebopSchema Schema; - protected BaseGenerator(BebopSchema schema) + /// + /// Configuration settings specific to the generator. + /// + protected GeneratorConfig Config; + + /// + /// Initializes a new instance of the class with a given schema and configuration. + /// + /// The Bebop schema used for code generation. + /// The generator-specific configuration settings. + protected BaseGenerator(BebopSchema schema, GeneratorConfig config) { Schema = schema; + Config = config; } /// - /// Generate code for a Bebop schema. + /// Generates code based on the provided Bebop schema. /// - /// Determines a default language version the generated code will target. - /// Determines which components of a service will be generated. default to both client and server. - /// Whether a generation notice should be written at the top of files. This is true by default. - /// Whether a binary schema should be emitted. This is false by default. - /// The generated code. - public abstract string Compile(Version? languageVersion, TempoServices services = TempoServices.Both, bool writeGeneratedNotice = true, bool emitBinarySchema = false); + /// A string containing the generated code. + public abstract string Compile(); /// - /// Write auxiliary files to an output directory path. + /// Writes auxiliary files, if any, associated with the generated code to the specified output directory. /// - /// The output directory path. + /// The directory path where auxiliary files should be written. public abstract void WriteAuxiliaryFiles(string outputPath); /// - /// Get auxiliary file contents that should be written to disk. + /// Retrieves information about any auxiliary files associated with the generated code. /// + /// An representing the contents and metadata of the auxiliary file, or null if there are no auxiliary files. public abstract AuxiliaryFile? GetAuxiliaryFile(); + /// - /// Get the alias of the generator. + /// Gets the alias of the code generator, which uniquely identifies it among other generators. /// public abstract string Alias { get; } } -} \ No newline at end of file +} diff --git a/Core/Generators/CPlusPlus/CPlusPlusGenerator.cs b/Core/Generators/CPlusPlus/CPlusPlusGenerator.cs index be17fb20..79f10cf4 100644 --- a/Core/Generators/CPlusPlus/CPlusPlusGenerator.cs +++ b/Core/Generators/CPlusPlus/CPlusPlusGenerator.cs @@ -15,7 +15,7 @@ public class CPlusPlusGenerator : BaseGenerator { const int indentStep = 2; - public CPlusPlusGenerator(BebopSchema schema) : base(schema) { } + public CPlusPlusGenerator(BebopSchema schema, GeneratorConfig config) : base(schema, config) { } private string FormatDocumentation(string documentation, int spaces) { @@ -355,10 +355,10 @@ private string EmitLiteral(Literal literal) /// Generate code for a Bebop schema. /// /// The generated code. - public override string Compile(Version? languageVersion, TempoServices services = TempoServices.Both, bool writeGeneratedNotice = true, bool emitBinarySchema = false) + public override string Compile() { var builder = new StringBuilder(); - if (writeGeneratedNotice) + if (Config.EmitNotice) { builder.AppendLine(GeneratorUtils.GetXmlAutoGeneratedNotice()); } @@ -374,9 +374,9 @@ public override string Compile(Version? languageVersion, TempoServices services builder.AppendLine("#include \"bebop.hpp\""); builder.AppendLine(""); - if (!string.IsNullOrWhiteSpace(Schema.Namespace)) + if (!string.IsNullOrWhiteSpace(Config.Namespace)) { - builder.AppendLine($"namespace {Schema.Namespace} {{"); + builder.AppendLine($"namespace {Config.Namespace} {{"); builder.AppendLine(""); } @@ -508,9 +508,9 @@ public override string Compile(Version? languageVersion, TempoServices services } } - if (!string.IsNullOrWhiteSpace(Schema.Namespace)) + if (!string.IsNullOrWhiteSpace(Config.Namespace)) { - builder.AppendLine($"}} // namespace {Schema.Namespace}"); + builder.AppendLine($"}} // namespace {Config.Namespace}"); builder.AppendLine(""); } diff --git a/Core/Generators/CSharp/CSharpGenerator.cs b/Core/Generators/CSharp/CSharpGenerator.cs index 772cb3d2..8b36c3fd 100644 --- a/Core/Generators/CSharp/CSharpGenerator.cs +++ b/Core/Generators/CSharp/CSharpGenerator.cs @@ -13,19 +13,19 @@ public class CSharpGenerator : BaseGenerator private Version LanguageVersion = CSharpNine; - public CSharpGenerator(BebopSchema schema) : base(schema) + public CSharpGenerator(BebopSchema schema, GeneratorConfig config) : base(schema, config) { + if (Version.TryParse(config.GetOptionRawValue("langVersion"), out var langVersion)) + { + LanguageVersion = langVersion; + } } - public override string Compile(Version? languageVersion, TempoServices services = TempoServices.Both, bool writeGeneratedNotice = true, bool emitBinarySchema = false) + public override string Compile() { - if (languageVersion is not null) - { - LanguageVersion = languageVersion; - } var builder = new IndentedStringBuilder(); - if (writeGeneratedNotice) + if (Config.EmitNotice) { builder .AppendLine(GeneratorUtils.GetXmlAutoGeneratedNotice()) @@ -34,9 +34,9 @@ public override string Compile(Version? languageVersion, TempoServices services .AppendLine("//"); } - if (!string.IsNullOrWhiteSpace(Schema.Namespace)) + if (!string.IsNullOrWhiteSpace(Config.Namespace)) { - builder.AppendLine($"namespace {Schema.Namespace.ToPascalCase()} {{"); + builder.AppendLine($"namespace {Config.Namespace.ToPascalCase()} {{"); builder.Indent(indentStep).AppendLine(); } @@ -265,7 +265,7 @@ public override string Compile(Version? languageVersion, TempoServices services } - if (!string.IsNullOrWhiteSpace(Schema.Namespace)) + if (!string.IsNullOrWhiteSpace(Config.Namespace)) { builder.Dedent(indentStep); builder.AppendLine("}"); @@ -1233,7 +1233,7 @@ DefinedType dt when dt.IsEnum(Schema) => true, private string PrefixNamespace(string definitionName) { - var nameSpace = string.IsNullOrWhiteSpace(Schema.Namespace) ? string.Empty : $"global::{Schema.Namespace.ToPascalCase()}."; + var nameSpace = string.IsNullOrWhiteSpace(Config.Namespace) ? string.Empty : $"global::{Config.Namespace.ToPascalCase()}."; return $"{nameSpace}{definitionName}"; } diff --git a/Core/Generators/Dart/DartGenerator.cs b/Core/Generators/Dart/DartGenerator.cs index 6ed0d12a..0325a5de 100644 --- a/Core/Generators/Dart/DartGenerator.cs +++ b/Core/Generators/Dart/DartGenerator.cs @@ -14,7 +14,7 @@ public class DartGenerator : BaseGenerator { const int indentStep = 2; - public DartGenerator(BebopSchema schema) : base(schema) { } + public DartGenerator(BebopSchema schema, GeneratorConfig config) : base(schema, config) { } private string FormatDocumentation(string documentation, int spaces) { @@ -296,7 +296,7 @@ private string EmitLiteral(Literal literal) { /// Generate code for a Bebop schema. /// /// The generated code. - public override string Compile(Version? languageVersion, TempoServices services = TempoServices.Both, bool writeGeneratedNotice = true, bool emitBinarySchema = false) + public override string Compile() { var builder = new StringBuilder(); builder.AppendLine("import 'dart:typed_data';"); diff --git a/Core/Generators/GeneratorConfig.cs b/Core/Generators/GeneratorConfig.cs new file mode 100644 index 00000000..3c5ba42a --- /dev/null +++ b/Core/Generators/GeneratorConfig.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace Core.Generators +{ + /// + /// Represents a configuration for a generator. + /// + public sealed record GeneratorConfig + { + public GeneratorConfig(string alias, string outFile): this(alias, outFile, TempoServices.Both, true, string.Empty, true, null) + { + + } + + //// + /// Initializes a new instance of the class with all parameters. + /// + public GeneratorConfig(string alias, + string outFile, + TempoServices services, + bool emitNotice, + string @namespace, + bool emitBinarySchema, + Dictionary? options) + { + ArgumentNullException.ThrowIfNullOrWhiteSpace(alias, nameof(alias)); + ArgumentNullException.ThrowIfNullOrWhiteSpace(outFile, nameof(outFile)); + Alias = alias; + OutFile = outFile; + Services = services; + EmitNotice = emitNotice; + EmitBinarySchema = emitBinarySchema; + Namespace = string.IsNullOrWhiteSpace(@namespace) ? string.Empty : @namespace; + Options = options ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + [JsonIgnore] + public string Alias { get; init; } + public string OutFile { get; init; } + public TempoServices Services { get; init; } + public bool EmitNotice { get; init; } + public bool EmitBinarySchema { get; init; } + public string Namespace { get; init; } + private Dictionary Options { get; init; } + + public int OptionCount => Options.Count; + + /// + /// Gets a boolean option value. + /// + public bool GetOptionBoolValue(string key, bool defaultValue = false) + { + return Options.TryGetValue(key, out var value) + ? bool.TryParse(value, out var boolValue) + ? boolValue + : defaultValue + : defaultValue; + } + + /// + /// Gets an enum option value. + /// + public T GetOptionEnumValue(string key, T? defaultValue = null) where T : struct, Enum + { + return Options.TryGetValue(key, out var value) + ? Enum.TryParse(value, out var enumValue) + ? enumValue + : defaultValue ?? default + : defaultValue ?? default; + } + + /// + /// Gets a raw option value. + /// + public string? GetOptionRawValue(string key) + { + return Options.TryGetValue(key, out var value) ? value : null; + } + + /// + /// Gets an integer option value. + /// + public int? GetOptionIntValue(string key) + { + return Options.TryGetValue(key, out var value) + ? int.TryParse(value, out var intValue) + ? intValue + : null + : null; + } + + /// + /// Adds a new option to the configuration. + /// + /// The key of the option. + /// The value of the option. + public void AddOption(string key, string value) + { + Options.Add(key, value); + } + + /// + /// Adds multiple options to the configuration. + /// + /// The options to add, represented as a dictionary. + public void AddOptions(Dictionary options) + { + foreach (var (key, value) in options) + { + Options.Add(key, value); + } + } + + public KeyValuePair[] GetOptions() + { + var options = new KeyValuePair[Options.Count]; + for (var i = 0; i < options.Length; i++) + { + options[i] = Options.ElementAt(i); + } + return options; + } + } +} \ No newline at end of file diff --git a/Core/Generators/GeneratorUtils.cs b/Core/Generators/GeneratorUtils.cs index c7a74e3a..568476a9 100644 --- a/Core/Generators/GeneratorUtils.cs +++ b/Core/Generators/GeneratorUtils.cs @@ -116,14 +116,14 @@ public static string BaseClassName(this Definition definition) /// /// Generators are keyed via their commandline alias. /// - public static Dictionary> ImplementedGenerators = new() + public static Dictionary> ImplementedGenerators = new() { - { "cpp", s => new CPlusPlusGenerator(s) }, - { "cs", s => new CSharpGenerator(s) }, - { "dart", s => new DartGenerator(s) }, - { "rust", s => new RustGenerator(s) }, - { "ts", s => new TypeScriptGenerator(s) }, - { "py", s => new PythonGenerator(s) } + { "cpp", (s, c) => new CPlusPlusGenerator(s, c) }, + { "cs", (s, c) => new CSharpGenerator(s, c) }, + { "dart", (s, c) => new DartGenerator(s, c) }, + { "rust", (s, c) => new RustGenerator(s, c) }, + { "ts", (s, c) => new TypeScriptGenerator(s, c) }, + { "py", (s, c) => new PythonGenerator(s, c) } }; public static Dictionary ImplementedGeneratorNames = new() diff --git a/Core/Generators/Python/PythonGenerator.cs b/Core/Generators/Python/PythonGenerator.cs index 6462973e..cfc31d65 100644 --- a/Core/Generators/Python/PythonGenerator.cs +++ b/Core/Generators/Python/PythonGenerator.cs @@ -14,7 +14,7 @@ public class PythonGenerator : BaseGenerator { const int indentStep = 4; - public PythonGenerator(BebopSchema schema) : base(schema) { } + public PythonGenerator(BebopSchema schema, GeneratorConfig config) : base(schema, config) { } private string FormatDocumentation(string documentation, BaseAttribute? deprecated) { @@ -334,7 +334,7 @@ private string EmitLiteral(Literal literal) /// Generate code for a Bebop schema. /// /// The generated code. - public override string Compile(Version? languageVersion, TempoServices services = TempoServices.Both, bool writeGeneratedNotice = true, bool emitBinarySchema = false) + public override string Compile() { var builder = new IndentedStringBuilder(); builder.AppendLine("from enum import Enum"); diff --git a/Core/Generators/Rust/RustGenerator.cs b/Core/Generators/Rust/RustGenerator.cs index f76e7a03..3035ccc0 100644 --- a/Core/Generators/Rust/RustGenerator.cs +++ b/Core/Generators/Rust/RustGenerator.cs @@ -52,9 +52,9 @@ public class RustGenerator : BaseGenerator #region entrypoints - public RustGenerator(BebopSchema schema) : base(schema) { } + public RustGenerator(BebopSchema schema, GeneratorConfig config) : base(schema, config) { } - public override string Compile(Version? languageVersion, TempoServices services = TempoServices.Both, bool writeGeneratedNotice = true, bool emitBinarySchema = false) + public override string Compile() { // the main scope which is where we write the const definitions and the borrowed types (as these are the // primary way to use bebop in Rust) @@ -63,16 +63,16 @@ public override string Compile(Version? languageVersion, TempoServices services // access the main scope. var ownedBuilder = new IndentedStringBuilder(); - if (writeGeneratedNotice) + if (Config.EmitNotice) { mainBuilder .AppendLine(GeneratorUtils.GetMarkdownAutoGeneratedNotice()) .AppendLine(); } - if (!string.IsNullOrWhiteSpace(Schema.Namespace)) + if (!string.IsNullOrWhiteSpace(Config.Namespace)) { - mainBuilder.AppendLine($"#![cfg(feature = \"{Schema.Namespace.ToKebabCase()}\")]"); + mainBuilder.AppendLine($"#![cfg(feature = \"{Config.Namespace.ToKebabCase()}\")]"); } WriteStandardImportsForModule(mainBuilder); @@ -118,9 +118,9 @@ public override string Compile(Version? languageVersion, TempoServices services } mainBuilder - .AppendLine(string.IsNullOrWhiteSpace(Schema.Namespace) + .AppendLine(string.IsNullOrWhiteSpace(Config.Namespace) ? "#[cfg(feature = \"bebop-owned-all\")]" - : $"#[cfg(any(feature = \"bebop-owned-all\", feature = \"{Schema.Namespace.ToKebabCase()}-owned\"))]") + : $"#[cfg(any(feature = \"bebop-owned-all\", feature = \"{Config.Namespace.ToKebabCase()}-owned\"))]") .CodeBlock("pub mod owned", _tab, () => { mainBuilder.Append(ownedBuilder.ToString()); diff --git a/Core/Generators/TypeScript/TypeScriptGenerator.cs b/Core/Generators/TypeScript/TypeScriptGenerator.cs index a8b2260d..6e551ddd 100644 --- a/Core/Generators/TypeScript/TypeScriptGenerator.cs +++ b/Core/Generators/TypeScript/TypeScriptGenerator.cs @@ -17,7 +17,7 @@ public class TypeScriptGenerator : BaseGenerator { const int indentStep = 2; - public TypeScriptGenerator(BebopSchema schema) : base(schema) { } + public TypeScriptGenerator(BebopSchema schema, GeneratorConfig config) : base(schema, config) { } private static string FormatDocumentation(string documentation, string deprecationReason, int spaces) { @@ -638,10 +638,10 @@ private string EmitLiteral(Literal literal) /// Generate code for a Bebop schema. /// /// The generated code. - public override string Compile(Version? languageVersion, TempoServices services = TempoServices.Both, bool writeGeneratedNotice = true, bool emitBinarySchema = false) + public override string Compile() { var builder = new IndentedStringBuilder(); - if (writeGeneratedNotice) + if (Config.EmitNotice) { builder.AppendLine(GeneratorUtils.GetXmlAutoGeneratedNotice()); } @@ -649,24 +649,24 @@ public override string Compile(Version? languageVersion, TempoServices services if (Schema.Definitions.Values.OfType().Any()) { builder.AppendLine("import { Metadata, MethodType } from \"@tempojs/common\";"); - if (services is TempoServices.Client or TempoServices.Both) + if (Config.Services is TempoServices.Client or TempoServices.Both) { builder.AppendLine("import { BaseClient, MethodInfo, CallOptions } from \"@tempojs/client\";"); } - if (services is TempoServices.Server or TempoServices.Both) + if (Config.Services is TempoServices.Server or TempoServices.Both) { builder.AppendLine("import { ServiceRegistry, BaseService, ServerContext, BebopMethodAny, BebopMethod } from \"@tempojs/server\";"); } } builder.AppendLine(""); - if (!string.IsNullOrWhiteSpace(Schema.Namespace)) + if (!string.IsNullOrWhiteSpace(Config.Namespace)) { - builder.AppendLine($"export namespace {Schema.Namespace} {{"); + builder.AppendLine($"export namespace {Config.Namespace} {{"); builder.Indent(2); } - if (emitBinarySchema) + if (Config.EmitBinarySchema) { builder.AppendLine($"export {Schema.ToBinary().ConvertToTypeScriptUInt8ArrayInitializer("BEBOP_SCHEMA")}"); @@ -886,9 +886,9 @@ public override string Compile(Version? languageVersion, TempoServices services } } var serviceDefinitions = Schema.Definitions.Values.OfType(); - if (serviceDefinitions is not null && serviceDefinitions.Any() && services is not TempoServices.None) + if (serviceDefinitions is not null && serviceDefinitions.Any() && Config.Services is not TempoServices.None) { - if (services is TempoServices.Server or TempoServices.Both) + if (Config.Services is TempoServices.Server or TempoServices.Both) { foreach (var service in serviceDefinitions) { @@ -1012,7 +1012,7 @@ public override string Compile(Version? languageVersion, TempoServices services } - if (services is TempoServices.Client or TempoServices.Both) + if (Config.Services is TempoServices.Client or TempoServices.Both) { static (string RequestType, string ResponseType) GetFunctionTypes(MethodDefinition definition) { @@ -1106,7 +1106,7 @@ public override string Compile(Version? languageVersion, TempoServices services } - if (!string.IsNullOrWhiteSpace(Schema.Namespace)) + if (!string.IsNullOrWhiteSpace(Config.Namespace)) { builder.Dedent(2); builder.AppendLine("}"); diff --git a/Core/IO/BinarySchemaWriter.cs b/Core/IO/BinarySchemaWriter.cs index d77b2910..66d2448a 100644 --- a/Core/IO/BinarySchemaWriter.cs +++ b/Core/IO/BinarySchemaWriter.cs @@ -71,8 +71,6 @@ private void WriteArray(ArrayType arrayType) } memberType = at.MemberType; } - Console.WriteLine($"Array depth: {depth}"); - Console.WriteLine($"Array member type: {memberType}"); _writer.Write(depth); _writer.Write(TypeToId(memberType)); } @@ -333,7 +331,6 @@ private void WriteStruct(StructDefinition definition) throw new CompilerException($"{definition.Name} exceeds maximum fields: has {fieldCount} fields"); } _writer.Write((byte)fieldCount); - Console.WriteLine($"Writing {fieldCount} fields for {definition.Name}"); foreach (var field in definition.Fields) { WriteField(definition, field); diff --git a/Core/IO/SchemaReader.cs b/Core/IO/SchemaReader.cs index 6444f7c2..bc4de91f 100644 --- a/Core/IO/SchemaReader.cs +++ b/Core/IO/SchemaReader.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Core.Lexer.Extensions; using Core.Lexer.Tokenization.Models; @@ -36,7 +37,12 @@ public static SchemaReader FromTextualSchema(string textualSchema) public static SchemaReader FromSchemaPaths(IEnumerable schemaPaths) { - return new SchemaReader(schemaPaths.Select(path => (path, File.ReadAllText(path))).ToList()); + return new SchemaReader(schemaPaths.Select(path => + { + if (!File.Exists(path)) + throw new FileNotFoundException($"Schema file not found: {path}"); + return (path, File.ReadAllText(path)); + }).ToList()); } private string CurrentFile => _schemas[_schemaIndex].Item2; @@ -108,12 +114,12 @@ public char GetChar() /// Append a file path to be read. /// /// True if a new file was actually added and must now be tokenized; false if this path was a duplicate. - public async Task AddFile(string absolutePath) + public bool AddFile(string absolutePath) { var fullPath = Path.GetFullPath(absolutePath); if (!_schemas.Any(t => Path.GetFullPath(t.Item1) == fullPath)) { - var text = await File.ReadAllTextAsync(fullPath); + var text = File.ReadAllText(fullPath); _schemas.Add((fullPath, text)); return true; } diff --git a/Core/Internal/SafeDirectoryInfoWrapper.cs b/Core/Internal/SafeDirectoryInfoWrapper.cs new file mode 100644 index 00000000..c8bbcb49 --- /dev/null +++ b/Core/Internal/SafeDirectoryInfoWrapper.cs @@ -0,0 +1,131 @@ + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Extensions.FileSystemGlobbing.Abstractions; + +namespace Core.Internal; + +/// +/// A wrapper for that doesn't throw for inaccessible directories or files. +/// +/// +/// This is a workaround for WASI where some weird behavior is observed when preopening directories. +/// +internal class SafeDirectoryInfoWrapper : DirectoryInfoBase +{ + private readonly DirectoryInfo _directoryInfo; + private readonly bool _isParentPath; + + private static readonly EnumerationOptions _enumerationOptions = new() + { + RecurseSubdirectories = false, + ReturnSpecialDirectories = false, + IgnoreInaccessible = true + }; + + /// + /// Initializes an instance of . + /// + /// The . + public SafeDirectoryInfoWrapper(DirectoryInfo directoryInfo) + : this(directoryInfo, isParentPath: false) + { } + + private SafeDirectoryInfoWrapper(DirectoryInfo directoryInfo, bool isParentPath) + { + _directoryInfo = directoryInfo; + _isParentPath = isParentPath; + } + + /// + public override IEnumerable EnumerateFileSystemInfos() + { + if (_directoryInfo.Exists) + { + IEnumerable fileSystemInfos; + try + { + fileSystemInfos = _directoryInfo.EnumerateFileSystemInfos("*", _enumerationOptions); + } + catch (DirectoryNotFoundException) + { + yield break; + } + foreach (FileSystemInfo fileSystemInfo in fileSystemInfos) + { + if (fileSystemInfo is DirectoryInfo directoryInfo) + { + yield return new SafeDirectoryInfoWrapper(directoryInfo); + } + else + { + yield return new FileInfoWrapper((FileInfo)fileSystemInfo); + } + } + } + } + + /// + /// Returns an instance of that represents a subdirectory. + /// + /// + /// If equals '..', this returns the parent directory. + /// + /// The directory name + /// The directory + public override DirectoryInfoBase? GetDirectory(string name) + { + bool isParentPath = string.Equals(name, "..", StringComparison.Ordinal); + + if (isParentPath) + { + return new SafeDirectoryInfoWrapper( + new DirectoryInfo(Path.Combine(_directoryInfo.FullName, name)), + isParentPath); + } + else + { + DirectoryInfo[] dirs = _directoryInfo.GetDirectories(name); + + if (dirs.Length == 1) + { + return new SafeDirectoryInfoWrapper(dirs[0], isParentPath); + } + else if (dirs.Length == 0) + { + return null; + } + else + { + // This shouldn't happen. The parameter name isn't supposed to contain wild card. + throw new InvalidOperationException( + $"More than one sub directories are found under {_directoryInfo.FullName} with name {name}."); + } + } + } + + /// + public override FileInfoBase GetFile(string name) + => new FileInfoWrapper(new FileInfo(Path.Combine(_directoryInfo.FullName, name))); + + /// + public override string Name => _isParentPath ? ".." : _directoryInfo.Name; + + /// + /// Returns the full path to the directory. + /// + /// + /// Equals the value of . + /// + public override string FullName => _directoryInfo.FullName; + + /// + /// Returns the parent directory. + /// + /// + /// Equals the value of . + /// + public override DirectoryInfoBase? ParentDirectory + => new DirectoryInfoWrapper(_directoryInfo.Parent!); +} \ No newline at end of file diff --git a/Core/Lexer/Tokenization/Tokenizer.cs b/Core/Lexer/Tokenization/Tokenizer.cs index 7052be4a..558001ec 100644 --- a/Core/Lexer/Tokenization/Tokenizer.cs +++ b/Core/Lexer/Tokenization/Tokenizer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using System.Threading; using System.Threading.Tasks; using Core.Exceptions; using Core.IO; @@ -41,9 +42,9 @@ public List Tokens } } - public async Task AddFile(string absolutePath) + public void AddFile(string absolutePath) { - if (await _reader.AddFile(absolutePath)) + if (_reader.AddFile(absolutePath)) { _newFilesToTokenize = true; } diff --git a/Core/Logging/DiagnosticLogger.Enhanced.cs b/Core/Logging/DiagnosticLogger.Enhanced.cs index 82c4834e..d2265c12 100644 --- a/Core/Logging/DiagnosticLogger.Enhanced.cs +++ b/Core/Logging/DiagnosticLogger.Enhanced.cs @@ -74,7 +74,18 @@ private void RenderEnhancedException(Exception ex, int errorCode) _err.Write(filePath); _err.WriteLine(); } - if (!string.IsNullOrWhiteSpace(ex.StackTrace)) + if (ex is { StackTrace: null } and { InnerException: not null}) + { + _err.WriteLine(); + _err.MarkupLine("[red bold]Inner Exception:[/]"); + if (_traceEnabled) { + _err.WriteException(ex.InnerException); + } else { + _err.MarkupLine($"[white]{ex.InnerException.Message}[/]"); + + } + } + if (_traceEnabled && !string.IsNullOrWhiteSpace(ex.StackTrace)) { // Write exception message _err.WriteException(ex); diff --git a/Core/Logging/DiagnosticLogger.Format.cs b/Core/Logging/DiagnosticLogger.Format.cs index eff6c606..f350762f 100644 --- a/Core/Logging/DiagnosticLogger.Format.cs +++ b/Core/Logging/DiagnosticLogger.Format.cs @@ -9,17 +9,6 @@ namespace Core.Logging; -[JsonSourceGenerationOptions( - JsonSerializerDefaults.Web, - AllowTrailingCommas = true, - UseStringEnumConverter = true, - DefaultBufferSize = 10)] -[JsonSerializable(typeof(LogFormatter))] -[JsonSerializable(typeof(CompilerOutput))] -[JsonSerializable(typeof(GeneratedFile))] -[JsonSerializable(typeof(AuxiliaryFile))] -public partial class ConfigContext : JsonSerializerContext { } - public partial class DiagnosticLogger { private string FormatDiagnostic(Diagnostic diagnostic) @@ -39,15 +28,7 @@ private string FormatDiagnostic(Diagnostic diagnostic) where = span == null ? "" : $"Issue located in '{span?.FileName}' at {span?.StartColonString()}: "; return $"[{DateTime.Now}][Compiler][{diagnostic.Severity}] {where}{message}"; case LogFormatter.JSON: - var options = new JsonSerializerOptions - { - TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault - ? new DefaultJsonTypeInfoResolver() - : ConfigContext.Default, - Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase), new SpanExceptionConverter(), new ExceptionConverter() }, - - }; - return JsonSerializer.Serialize(diagnostic, options); + return JsonSerializer.Serialize(diagnostic, JsonContext.Default.Diagnostic); case LogFormatter.Enhanced: default: throw new ArgumentOutOfRangeException(); @@ -56,67 +37,6 @@ private string FormatDiagnostic(Diagnostic diagnostic) private string FormatCompilerOutput(CompilerOutput output) { - var options = new JsonSerializerOptions - { - TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault - ? new DefaultJsonTypeInfoResolver() - : ConfigContext.Default, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - WriteIndented = false, - Converters = { - new JsonStringEnumConverter(JsonNamingPolicy.CamelCase), - new SpanExceptionConverter(), - new ExceptionConverter() - } - }; - - - - return JsonSerializer.Serialize(output, options); - } - - class SpanExceptionConverter : JsonConverter - { - public override SpanException Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - throw new NotImplementedException(); - } - - public override void Write(Utf8JsonWriter writer, SpanException value, JsonSerializerOptions options) - { - writer.WriteStartObject(); - writer.WriteStartObject("span"); - writer.WriteString("fileName", value.Span.FileName); - writer.WriteNumber("startLine", value.Span.StartLine); - writer.WriteNumber("endLine", value.Span.EndLine); - writer.WriteNumber("startColumn", value.Span.StartColumn); - writer.WriteNumber("endColumn", value.Span.EndColumn); - writer.WriteNumber("lines", value.Span.Lines); - writer.WriteEndObject(); - writer.WriteNumber("errorCode", value.ErrorCode); - writer.WriteString("severity", value.Severity.ToString().ToCamelCase()); - writer.WriteString("message", value.Message); - writer.WriteEndObject(); - } - } - - class ExceptionConverter : JsonConverter - { - public override Exception Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - throw new NotImplementedException(); - } - - public override void Write(Utf8JsonWriter writer, Exception value, JsonSerializerOptions options) - { - writer.WriteStartObject(); - if (value is CompilerException compilerException) - { - writer.WriteNumber("errorCode", compilerException.ErrorCode); - } - writer.WriteString("message", value.Message); - writer.WriteEndObject(); - } + return JsonSerializer.Serialize(output, JsonContext.Default.CompilerOutput); } } \ No newline at end of file diff --git a/Core/Logging/DiagnosticLogger.cs b/Core/Logging/DiagnosticLogger.cs index 1f376fcc..d4dcd3f9 100644 --- a/Core/Logging/DiagnosticLogger.cs +++ b/Core/Logging/DiagnosticLogger.cs @@ -12,13 +12,16 @@ namespace Core.Logging; public partial class DiagnosticLogger { private static DiagnosticLogger? _instance; - private readonly LogFormatter _formatter; + private LogFormatter _formatter; + private bool _traceEnabled; private bool _diagnosticsSupressed; private readonly IAnsiConsole _out; private readonly IAnsiConsole _err; public IAnsiConsole Out => _out; public IAnsiConsole Error => _err; + public bool TraceEnabled => _traceEnabled; + #region Static Methods private DiagnosticLogger(LogFormatter formatter) { @@ -46,6 +49,16 @@ private DiagnosticLogger(LogFormatter formatter) } } + public void SetFormatter(LogFormatter formatter) + { + _formatter = formatter; + } + + public void EnableTrace() + { + _traceEnabled = true; + } + public static void Initialize(LogFormatter formatter) { if (!Enum.IsDefined(typeof(LogFormatter), formatter)) @@ -99,6 +112,14 @@ public void WriteSpanDiagonstics(List exceptions) if (_formatter is LogFormatter.Enhanced) { RenderEnhancedSpanErrors(exceptions); + WriteLine(string.Empty); + return; + } + if (_formatter is LogFormatter.JSON) + { + var warnings = exceptions.Where(e => e.Severity == Severity.Warning).ToList(); + var errors = exceptions.Where(e => e.Severity == Severity.Error).ToList(); + WriteCompilerOutput(new CompilerOutput(warnings, errors, null)); return; } var messages = exceptions.Select(FormatSpanError); @@ -149,11 +170,23 @@ private void WriteBaseDiagonstic(Exception ex) _err.WriteLine(FormatDiagnostic(new(Severity.Error, ex.Message, Unknown, null))); } + public void WriteCompilerOutput(CompilerOutput output) { // TODO figure out why the virtual console breaks outputs - - Console.Out.WriteLine(FormatCompilerOutput(output)); + if (_formatter is LogFormatter.JSON) + { + Console.Out.WriteLine(FormatCompilerOutput(output)); + return; + } + var errorsAndWarnings = output.Errors.Concat(output.Warnings).ToList(); + if (errorsAndWarnings.Count > 0) + { + WriteSpanDiagonstics(errorsAndWarnings); + } + if (output.Result is not null) { + Console.Out.WriteLine(output.Result.Contents); + } } public void WriteLine(string message) diff --git a/Core/Logging/DiagonsticLogger.Misc.cs b/Core/Logging/DiagonsticLogger.Misc.cs index 2c21a81d..781724a2 100644 --- a/Core/Logging/DiagonsticLogger.Misc.cs +++ b/Core/Logging/DiagonsticLogger.Misc.cs @@ -7,5 +7,5 @@ public partial class DiagnosticLogger { internal const int FileNotFound = 404; internal const int Unknown = 1000; - internal record Diagnostic(Severity Severity, string Message, int ErrorCode, Span? Span) { } + public record Diagnostic(Severity Severity, string Message, int ErrorCode, Span? Span) { } } \ No newline at end of file diff --git a/Core/Meta/BebopConfig.cs b/Core/Meta/BebopConfig.cs new file mode 100644 index 00000000..8474f788 --- /dev/null +++ b/Core/Meta/BebopConfig.cs @@ -0,0 +1,743 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Core.Generators; +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Extensions.FileSystemGlobbing; +using System.Linq; +using Core.Exceptions; +using Core.Meta.Extensions; +using Core.Logging; +using Core.Internal; + +namespace Core.Meta; + +/// +/// Represents the configuration for Bebop compiler and runtime behavior. +/// +public partial class BebopConfig +{ + /// + /// The name of the config file used by bebopc. + /// + public const string ConfigFileName = "bebop.json"; + + internal const string DefaultIncludeGlob = "**/*.bop"; + + /// + /// Gets or sets the file inclusion patterns for the Bebop compiler. + /// + [JsonPropertyName("include")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string[] Includes { get; set; } = [DefaultIncludeGlob]; + + /// + /// Gets or sets the file exclusion patterns for the Bebop compiler. + /// + [JsonPropertyName("exclude")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string[] Excludes { get; set; } = []; + + /// + /// Gets or sets the configurations for each code generator. + /// + [JsonPropertyName("generators")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public GeneratorConfig[] Generators { get; set; } = []; + + /// + /// Gets or sets the options for file system watching. + /// + [JsonPropertyName("watchOptions")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public WatchOptions WatchOptions { get; set; } = new(); + + /// + /// Gets or sets the warning codes to be suppressed by the Bebop compiler. + /// + [JsonPropertyName("noWarn")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int[] SupressedWarningCodes { get; set; } = []; + /// + /// Gets or sets a value indicating whether the Bebop compiler should emit code. + /// + [JsonPropertyName("noEmit")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool NoEmit { get; set; } = false; + + [JsonIgnore] + public string WorkingDirectory { get; private set; } = null!; + + /// + /// Resolves the file paths included in the configuration using the specified glob patterns. + /// + /// An enumerable of full file paths that match the include patterns and do not match the exclude patterns. + public IEnumerable ResolveIncludes() + { + List files = []; + + var matcher = new Matcher(); + // Add relative paths + matcher.AddIncludePatterns(Includes.Where(p => !Path.IsPathRooted(p) || !p.Contains("**"))); + matcher.AddExcludePatterns(Excludes); + var matches = matcher.Execute(new SafeDirectoryInfoWrapper(new DirectoryInfo(WorkingDirectory))).Files; + files.AddRange(matches.Select(match => Path.GetFullPath(Path.Combine(WorkingDirectory, match.Path)))); + + foreach (var include in Includes) + { + // Handle non-glob absolute paths + if (Path.IsPathRooted(include) && !include.Contains('*')) + { + if (File.Exists(include) && !Excludes.Contains(include)) + { + files.Add(include); + } + } + // Handle absolute paths with glob patterns, including recursive patterns + else if (Path.IsPathRooted(include) && include.Contains('*')) + { + string? baseDirPath; + string? globPattern; + + if (include.Contains("**")) + { + var separatorIndex = include.IndexOf("**"); + baseDirPath = include[..separatorIndex]; + globPattern = include[separatorIndex..]; + } + else + { + baseDirPath = Path.GetDirectoryName(include); + if (baseDirPath is null) + { + // Handle error or invalid path + continue; // Skip this iteration + } + globPattern = include[baseDirPath.Length..].TrimStart(Path.DirectorySeparatorChar); + } + var baseDirInfo = new DirectoryInfo(baseDirPath); + if (baseDirInfo.Exists) + { + var dirMatcher = new Matcher(); + dirMatcher.AddInclude(globPattern); + var dirMatches = dirMatcher.Execute(new SafeDirectoryInfoWrapper(baseDirInfo)).Files; + files.AddRange(dirMatches.Select(match => Path.Combine(baseDirPath, match.Path))); + } + } + } + return files.Distinct(); // To avoid duplicates if any + } + + /// + /// Searches recursively upwards to locate the Bebop configuration file. + /// + /// The fully qualified path to the configuration file, or null if not found. + public static string? Locate() + { + try + { + var workingDirectory = Directory.GetCurrentDirectory(); + var configFile = Directory.GetFiles(workingDirectory, ConfigFileName).FirstOrDefault(); + while (string.IsNullOrWhiteSpace(configFile)) + { + if (Directory.GetParent(workingDirectory) is not { Exists: true } parent) + { + break; + } + workingDirectory = parent.FullName; + if (parent.GetFiles(ConfigFileName)?.FirstOrDefault() is { Exists: true } file) + { + configFile = file.FullName; + } + } + return configFile; + } + catch (Exception ex) + { + if (DiagnosticLogger.Instance is { TraceEnabled: true } logger) + { + logger.WriteDiagonstic(ex); + } + return null; + } + } + + /// + /// Loads a BebopConfig from a file. + /// + /// The path to the configuration file. + /// The deserialized BebopConfig object. + /// Thrown when the specified configuration file is not found. + public static BebopConfig FromFile(string? configPath) + { + ArgumentNullException.ThrowIfNullOrWhiteSpace(configPath, nameof(configPath)); + if (!File.Exists(configPath)) + throw new FileNotFoundException("Failed to find bebop.json", configPath); + + var json = File.ReadAllText(configPath); + var config = FromJson(json); + config.WorkingDirectory = Path.GetDirectoryName(configPath) ?? throw new DirectoryNotFoundException("Failed to find directory containing bebop.json"); + return config; + } + + /// + /// Deserializes a BebopConfig from a JSON string. + /// + /// The JSON string to deserialize. + /// The deserialized BebopConfig object. + /// Thrown when deserialization fails. + public static BebopConfig FromJson(string json) => JsonSerializer.Deserialize(json, JsonContext.Default.BebopConfig) ?? throw new JsonException("Failed to deserialize bebop.json"); + + /// + /// Serializes the BebopConfig to a JSON string. + /// + /// The current configuration as JSON. + public string ToJson() => JsonSerializer.Serialize(this, JsonContext.Default.BebopConfig); + + public static BebopConfig Default => new() + { + WorkingDirectory = Directory.GetCurrentDirectory(), + }; + + public void Validate() + { + if (string.IsNullOrWhiteSpace(WorkingDirectory)) + { + throw new CompilerException("working directory is not defined."); + } + if (Directory.Exists(WorkingDirectory) is false) + { + throw new CompilerException($"working directory '{WorkingDirectory}' does not exist."); + } + if (Includes is { Length: > 0 }) + { + for (var i = 0; i < Includes.Length; i++) + { + var include = Includes[i]; + if (string.IsNullOrWhiteSpace(include)) + { + throw new CompilerException($"include pattern at index {i} is null or whitespace"); + } + if (!include.IsLegalFilePathOrGlob(out var invalidCharIndex)) + { + throw new CompilerException($"include pattern at index {i} is not a valid path or glob pattern{(invalidCharIndex >= 0 ? $": invalid character '{include[invalidCharIndex]}' at index {invalidCharIndex}" : ".")}"); + } + } + } + if (Excludes is { Length: > 0 }) + { + for (var i = 0; i < Excludes.Length; i++) + { + var exclude = Excludes[i]; + if (string.IsNullOrWhiteSpace(exclude)) + { + throw new CompilerException($"exclude pattern at index {i} is null or whitespace"); + } + if (!exclude.IsLegalFilePathOrGlob(out var invalidCharIndex)) + { + throw new CompilerException($"exclude pattern at index {i} is not a valid path or glob pattern{(invalidCharIndex >= 0 ? $": invalid character '{exclude[invalidCharIndex]}' at index {invalidCharIndex}" : ".")}"); + } + } + } + if (Generators is { Length: 0 }) + { + throw new CompilerException("No generators were specified in the configuration."); + } + foreach (var generator in Generators) + { + if (string.IsNullOrWhiteSpace(generator.Alias)) + { + throw new CompilerException("Generator alias is null or whitespace"); + } + if (!GeneratorUtils.ImplementedGeneratorNames.ContainsKey(generator.Alias)) + { + throw new CompilerException($"Generator alias '{generator.Alias}' is not a known or valid generator name."); + } + if (!generator.OutFile.IsLegalFilePath(out var invalidCharIndex)) + { + throw new CompilerException($"Generator outFile '{generator.OutFile}' is not a valid file path{(invalidCharIndex >= 0 ? $": invalid character '{generator.OutFile[invalidCharIndex]}' at index {invalidCharIndex}" : ".")}"); + } + if (!string.IsNullOrEmpty(generator.Namespace) && !generator.Namespace.IsValidNamespace()) + { + throw new CompilerException($"Generator namespace '{generator.Namespace}' is not a valid namespace."); + } + + if (generator is { OptionCount: > 0 }) + { + foreach (var option in generator.GetOptions()) + { + if (string.IsNullOrWhiteSpace(option.Key)) + { + throw new CompilerException($"Generator option key is null or whitespace"); + } + if (string.IsNullOrWhiteSpace(option.Value)) + { + throw new CompilerException($"Generator option value is null or whitespace"); + } + } + } + } + + for (var i = 0; i < WatchOptions.ExcludeDirectories.Length; i++) + { + var excludeDirectory = WatchOptions.ExcludeDirectories[i]; + if (string.IsNullOrWhiteSpace(excludeDirectory)) + { + throw new CompilerException($"exclude directory at index {i} is null or whitespace"); + } + if (!excludeDirectory.IsLegalPathOrGlob(out var invalidCharIndex)) + { + throw new CompilerException($"exclude directory at index {i} is not a valid path or glob pattern{(invalidCharIndex >= 0 ? $": invalid character '{excludeDirectory[invalidCharIndex]}' at index {invalidCharIndex}" : ".")}"); + } + } + for (var i = 0; i < WatchOptions.ExcludeFiles.Length; i++) + { + var excludeFile = WatchOptions.ExcludeFiles[i]; + if (string.IsNullOrWhiteSpace(excludeFile)) + { + throw new CompilerException($"exclude file at index {i} is null or whitespace"); + } + if (!excludeFile.IsLegalFilePathOrGlob(out var invalidCharIndex)) + { + throw new CompilerException($"exclude file at index {i} is not a valid file path or glob pattern{(invalidCharIndex >= 0 ? $": invalid character '{excludeFile[invalidCharIndex]}' at index {invalidCharIndex}" : ".")}"); + } + } + } +} + +public class WatchOptions +{ + /// + /// Gets or sets a list of directories to be excluded from file system watching. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("excludeDirectories")] + public string[] ExcludeDirectories { get; set; } = []; + + /// + /// Gets or sets a list of files to be excluded from file system watching. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("excludeFiles")] + public string[] ExcludeFiles { get; set; } = []; + + /// + /// Gets or sets a value indicating whether the output of the watch process should be preserved. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("preserveWatchOutput")] + public bool PreserveWatchOutput { get; set; } = false; +} + +/// +/// Provides custom JSON serialization and deserialization for . +/// This converter handles the unique JSON structure of the Bebop configuration. +/// +public class BebopConfigConverter : JsonConverter +{ + /// + /// Reads and converts the JSON to an instance of . + /// + /// The to read from. + /// The type of object to convert to. + /// The serializer options to use. + /// The deserialized object from the JSON. + /// Thrown when the JSON structure does not meet the expected format. + public override BebopConfig Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType is not JsonTokenType.StartObject) + { + throw new JsonException("expected StartObject token"); + } + var bebopConfig = new BebopConfig(); + while (reader.Read()) + { + if (reader.TokenType is JsonTokenType.EndObject) + { + return bebopConfig; + } + if (reader.TokenType is not JsonTokenType.PropertyName) + { + throw new JsonException("expected PropertyName token"); + } + var propertyName = reader.GetString(); + reader.Read(); + switch (propertyName) + { + case "include": + var includes = JsonSerializer.Deserialize(ref reader, options); + if (includes is not { Length: > 0 }) + { + includes = [BebopConfig.DefaultIncludeGlob]; + } + bebopConfig.Includes = includes; + break; + case "exclude": + bebopConfig.Excludes = JsonSerializer.Deserialize(ref reader, options) ?? []; + break; + case "generators": + bebopConfig.Generators = ReadGenerators(ref reader, options); + break; + case "watchOptions": + bebopConfig.WatchOptions = ReadWatchOptions(ref reader, options); + break; + case "noWarn": + bebopConfig.SupressedWarningCodes = JsonSerializer.Deserialize(ref reader, options) ?? []; + break; + case "noEmit": + bebopConfig.NoEmit = reader.GetBoolean(); + break; + default: + throw new JsonException($"unexpected property {propertyName}"); + } + } + return bebopConfig; + } + + private static GeneratorConfig[] ReadGenerators(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + var generators = new List(); + + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("expected StartObject token for generators"); + } + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + if (reader.TokenType == JsonTokenType.PropertyName) + { + var alias = reader.GetString(); + if (string.IsNullOrWhiteSpace(alias)) + { + throw new JsonException("unable to read generator alias"); + } + reader.Read(); + + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("expected StartObject token for generator config"); + } + bool simpleConstructor = true; + string? outFile = null; + TempoServices? services = null; + bool? emitNotice = null; + bool? emitBinarySchema = null; + string? @namespace = null; + var generatorOptions = new Dictionary(); + + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + if (reader.TokenType == JsonTokenType.PropertyName) + { + var propName = reader.GetString(); + reader.Read(); // Move to the value + + switch (propName) + { + case "outFile": + outFile = reader.GetString(); + break; + case "services": + services = JsonSerializer.Deserialize(ref reader, options); + simpleConstructor = false; + break; + case "emitNotice": + emitNotice = reader.GetBoolean(); + simpleConstructor = false; + break; + case "emitBinarySchema": + emitBinarySchema = reader.GetBoolean(); + simpleConstructor = false; + break; + case "namespace": + @namespace = reader.GetString(); + simpleConstructor = false; + break; + case "options": + ReadGeneratorOptions(ref reader, generatorOptions); + simpleConstructor = false; + break; + } + } + } + if (string.IsNullOrWhiteSpace(outFile)) + { + throw new JsonException("'outFile' is null or whitespace"); + } + EnsureLegalFilePath(outFile); + + GeneratorConfig generatorConfig; + if (simpleConstructor) + { + // Use the minimal constructor if only required properties are present + generatorConfig = new GeneratorConfig(alias, outFile); + } + else + { + // Use the full constructor if any optional properties are present + generatorConfig = new GeneratorConfig(alias, + outFile, + services.GetValueOrDefault(TempoServices.Both), + emitNotice.GetValueOrDefault(true), + @namespace ?? string.Empty, + emitBinarySchema.GetValueOrDefault(true), + generatorOptions); + } + generators.Add(generatorConfig); + } + } + return generators.ToArray(); + } + + + /// + /// Reads the generator configurations from the JSON reader and constructs an array of . + /// + /// The JSON reader to read from. + /// The JSON serializer options. + /// An array of objects. + /// Thrown when the JSON structure of the generators is not valid. + private static void ReadGeneratorOptions(ref Utf8JsonReader reader, Dictionary options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("expected StartObject token for options"); + } + + while (reader.Read()) + { + if (reader.TokenType is JsonTokenType.EndObject) + { + break; + } + + if (reader.TokenType == JsonTokenType.PropertyName) + { + var optionKey = reader.GetString(); + if (string.IsNullOrWhiteSpace(optionKey)) + { + throw new JsonException("option key is null or whitespace"); + } + reader.Read(); // Move to the value + if (reader.TokenType is not JsonTokenType.String) + { + throw new JsonException("expected String token for option value"); + } + + var optionValue = reader.GetString(); + if (string.IsNullOrWhiteSpace(optionValue)) + { + throw new JsonException($"option value for '{optionKey}' is null or whitespace"); + } + options[optionKey] = optionValue; + } + } + } + + /// + /// Reads watch options from the JSON reader and returns an instance of . + /// + /// The JSON reader to read from. + /// The JSON serializer options. + /// An instance of . + /// Thrown when the JSON structure of the watch options is not valid. + private static WatchOptions ReadWatchOptions(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + var watchOptions = new WatchOptions(); + if (reader.TokenType is not JsonTokenType.StartObject) + { + throw new JsonException("expected StartObject token for watchOptions"); + } + + while (reader.Read()) + { + if (reader.TokenType is JsonTokenType.EndObject) + { + break; + } + + if (reader.TokenType == JsonTokenType.PropertyName) + { + var propName = reader.GetString(); + reader.Read(); // Move to the value + + switch (propName) + { + case "excludeDirectories": + watchOptions.ExcludeDirectories = JsonSerializer.Deserialize(ref reader, options) ?? []; + break; + case "excludeFiles": + watchOptions.ExcludeFiles = JsonSerializer.Deserialize(ref reader, options) ?? []; + break; + case "preserveWatchOutput": + watchOptions.PreserveWatchOutput = reader.GetBoolean(); + break; + } + } + } + return watchOptions; + } + + + /// + /// Writes the object to JSON. + /// + /// The to write to. + /// The object to serialize. + /// The serializer options to use. + /// Thrown when the method is not implemented. + public override void Write(Utf8JsonWriter writer, BebopConfig value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + if (value.Includes is { Length: > 0 }) + { + // Write "include" array + writer.WritePropertyName("include"); + writer.WriteStartArray(); + foreach (var include in value.Includes) + { + writer.WriteStringValue(include); + } + writer.WriteEndArray(); + } + + // Write "exclude" array + if (value.Excludes is { Length: > 0 }) + { + writer.WritePropertyName("exclude"); + writer.WriteStartArray(); + foreach (var exclude in value.Excludes) + { + writer.WriteStringValue(exclude); + } + writer.WriteEndArray(); + } + + if (value.Generators is { Length: > 0 }) + { + // Write "generators" object + WriteGenerators(writer, value.Generators, options); + } + + if (value.WatchOptions is { ExcludeDirectories.Length: > 0 } or { ExcludeFiles.Length: > 0 }) + { + // Write "watchOptions" object + WriteWatchOptions(writer, value.WatchOptions, options); + } + if (value.SupressedWarningCodes is { Length: > 0 }) + { + writer.WritePropertyName("noWarn"); + writer.WriteStartArray(); + foreach (var warningCode in value.SupressedWarningCodes) + { + writer.WriteNumberValue(warningCode); + } + writer.WriteEndArray(); + } + if (value.NoEmit) + { + writer.WriteBoolean("noEmit", value.NoEmit); + } + + writer.WriteEndObject(); + } + + private static void WriteGenerators(Utf8JsonWriter writer, GeneratorConfig[] generators, JsonSerializerOptions options) + { + writer.WriteStartObject("generators"); + + foreach (var generator in generators) + { + writer.WritePropertyName(generator.Alias); + + writer.WriteStartObject(); + writer.WriteString("outFile", generator.OutFile); + + if (generator.Services is not TempoServices.Both) + JsonSerializer.Serialize(writer, generator.Services, options); + + if (generator.EmitNotice is false) + writer.WriteBoolean("emitNotice", generator.EmitNotice); + + if (generator.EmitBinarySchema is false) + writer.WriteBoolean("emitBinarySchema", generator.EmitBinarySchema); + + if (!string.IsNullOrWhiteSpace(generator.Namespace)) + writer.WriteString("namespace", generator.Namespace); + + if (generator is { OptionCount: > 0 }) + { + writer.WriteStartObject("options"); + foreach (var option in generator.GetOptions()) + { + writer.WritePropertyName(option.Key); + writer.WriteStringValue(option.Value); + } + writer.WriteEndObject(); + } + writer.WriteEndObject(); + } + writer.WriteEndObject(); + } + + private static void WriteWatchOptions(Utf8JsonWriter writer, WatchOptions watchOptions, JsonSerializerOptions options) + { + writer.WriteStartObject("watchOptions"); + if (watchOptions.ExcludeDirectories is { Length: > 0 }) + { + writer.WritePropertyName("excludeDirectories"); + writer.WriteStartArray(); + foreach (var excludeDirectory in watchOptions.ExcludeDirectories) + { + writer.WriteStringValue(excludeDirectory); + } + writer.WriteEndArray(); + } + + if (watchOptions.ExcludeFiles is { Length: > 0 }) + { + writer.WritePropertyName("excludeFiles"); + writer.WriteStartArray(); + foreach (var excludeFile in watchOptions.ExcludeFiles) + { + writer.WriteStringValue(excludeFile); + } + writer.WriteEndArray(); + } + + if (watchOptions.PreserveWatchOutput is true) + { + writer.WriteBoolean("preserveWatchOutput", watchOptions.PreserveWatchOutput); + } + + writer.WriteEndObject(); + } + + /// + /// Ensures that the given file path and file name do not contain any illegal characters. + /// + /// The file path to validate. + /// Thrown if the file path or file name contains illegal characters. + private static void EnsureLegalFilePath(string path) + { + // Check for invalid path characters + if (!path.IsLegalPath(out var invalidPathCharIndex)) + { + throw new JsonException($"The path '{path}' contains invalid characters: '{path[invalidPathCharIndex]}'"); + } + + // Extract the file name from the path and check for invalid file name characters + if (!path.IsLegalFilePath(out var invalidFileNameCharIndex)) + { + throw new JsonException($"The file name '{Path.GetFileName(path)}' contains invalid characters: '{path[invalidFileNameCharIndex]}'"); + } + } + +} \ No newline at end of file diff --git a/Core/Meta/BebopSchema.cs b/Core/Meta/BebopSchema.cs index b38f850b..59f0db50 100644 --- a/Core/Meta/BebopSchema.cs +++ b/Core/Meta/BebopSchema.cs @@ -30,9 +30,8 @@ public struct BebopSchema public List Imports { get; } - public BebopSchema(string nameSpace, Dictionary definitions, HashSet<(Token, Token)> typeReferences, List? parsingErrors = null, List? parsingWarnings = null, List? imports = null) + public BebopSchema(Dictionary definitions, HashSet<(Token, Token)> typeReferences, List? parsingErrors = null, List? parsingWarnings = null, List? imports = null) { - Namespace = nameSpace; Definitions = definitions; Imports = imports ?? new List(); @@ -43,10 +42,7 @@ public BebopSchema(string nameSpace, Dictionary definitions, _parsingWarnings = parsingWarnings ?? new(); _typeReferences = typeReferences; } - /// - /// An optional namespace that is provided to the compiler. - /// - public string Namespace { get; } + /// /// All Bebop definitions in this schema, keyed by their name. /// diff --git a/Core/Meta/Extensions/StringExtensions.cs b/Core/Meta/Extensions/StringExtensions.cs index 6b1ad71a..a047b8ce 100644 --- a/Core/Meta/Extensions/StringExtensions.cs +++ b/Core/Meta/Extensions/StringExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text; @@ -7,7 +8,7 @@ namespace Core.Meta.Extensions { - public static class StringExtensions + public static partial class StringExtensions { private const char SnakeSeparator = '_'; private const char KebabSeparator = '-'; @@ -292,5 +293,130 @@ public static string ConvertToTypeScriptUInt8ArrayInitializer(this byte[] byteAr return builder.ToString(); } + + public static bool IsLegalPath(this string path, out int index) + { + index = -1; + if (string.IsNullOrWhiteSpace(path)) + { + return false; + } + // Check for invalid path characters + var invalidPathChars = Path.GetInvalidPathChars(); + var invalidPathCharIndex = path.IndexOfAny(invalidPathChars); + if (invalidPathCharIndex >= 0) + { + index = invalidPathCharIndex; + return false; + } + return true; + } + + public static bool IsLegalFilePath(this string filePath, out int index) + { + index = -1; + if (string.IsNullOrWhiteSpace(filePath)) + { + return false; + } + + // Check for invalid path characters in the entire filePath + if (!IsLegalPath(filePath, out index)) + { + return false; + } + + // Extract the file name from the path and check for invalid file name characters + var fileName = Path.GetFileName(filePath); + var invalidFileNameChars = Path.GetInvalidFileNameChars(); + var invalidFileNameCharIndex = fileName.IndexOfAny(invalidFileNameChars); + if (invalidFileNameCharIndex >= 0) + { + // Adjust the index to be in the context of the full filePath, not just fileName + index = filePath.LastIndexOf(fileName) + invalidFileNameCharIndex; + return false; + } + return true; + } + + public static bool IsLegalPathGlob(this string pathOrGlob) + { + if (string.IsNullOrWhiteSpace(pathOrGlob)) + { + return false; + } + if (LegalPathGlobRegex().IsMatch(pathOrGlob)) + { + return true; + } + return true; + } + + public static bool IsLegalFileGlobal(this string fileGlob) + { + if (string.IsNullOrWhiteSpace(fileGlob)) + { + return false; + } + if (LegalFileGlobRegex().IsMatch(fileGlob)) + { + return true; + } + return true; + } + + public static bool IsValidNamespace(this string @namespace) + { + if (string.IsNullOrWhiteSpace(@namespace)) + { + return false; + } + return NamespaceRegex().IsMatch(@namespace); + } + + public static bool IsLegalPathOrGlob(this string pathOrGlob, out int invalidIndex) + { + invalidIndex = -1; + if (string.IsNullOrWhiteSpace(pathOrGlob)) + { + return false; + } + if (IsLegalPathGlob(pathOrGlob)) + { + return true; + } + if (IsLegalPath(pathOrGlob, out invalidIndex)) + { + return true; + } + return false; + } + public static bool IsLegalFilePathOrGlob(this string filePathOrGlob, out int invalidIndex) + { + invalidIndex = -1; + if (string.IsNullOrWhiteSpace(filePathOrGlob)) + { + return false; + } + if (IsLegalFileGlobal(filePathOrGlob)) + { + return true; + } + if (IsLegalFilePath(filePathOrGlob, out invalidIndex)) + { + return true; + } + return false; + } + + + [GeneratedRegex(@"^(\*\*\/|.*\/)$")] + private static partial Regex LegalPathGlobRegex(); + + [GeneratedRegex(@"^.*[\*\?\[\]].*(\.[a-zA-Z0-9]+)?$")] + private static partial Regex LegalFileGlobRegex(); + + [GeneratedRegex(@"^[a-zA-Z]+(\.[a-zA-Z]+)*$")] + private static partial Regex NamespaceRegex(); } } diff --git a/Core/Meta/JsonContext.cs b/Core/Meta/JsonContext.cs new file mode 100644 index 00000000..81209a2d --- /dev/null +++ b/Core/Meta/JsonContext.cs @@ -0,0 +1,84 @@ + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using Core.Exceptions; +using Core.Generators; +using Core.Lexer.Tokenization.Models; +using Core.Logging; +using Core.Meta.Extensions; + +namespace Core.Meta; +[JsonSourceGenerationOptions( + JsonSerializerDefaults.Web, + AllowTrailingCommas = true, + DefaultBufferSize = 10, + WriteIndented = true, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + UseStringEnumConverter = true, + Converters = [typeof(SpanExceptionConverter), typeof(ExceptionConverter), typeof(BebopConfigConverter)])] +[JsonSerializable(typeof(BebopConfig))] +[JsonSerializable(typeof(TempoServices))] +[JsonSerializable(typeof(GeneratorConfig))] +[JsonSerializable(typeof(WatchOptions))] +[JsonSerializable(typeof(LogFormatter))] +[JsonSerializable(typeof(CompilerOutput))] +[JsonSerializable(typeof(GeneratedFile))] +[JsonSerializable(typeof(AuxiliaryFile))] +[JsonSerializable(typeof(SpanException))] +[JsonSerializable(typeof(Exception))] +[JsonSerializable(typeof(DiagnosticLogger.Diagnostic))] +[JsonSerializable(typeof(Span))] +public partial class JsonContext : JsonSerializerContext +{ +} + +class SpanExceptionConverter : JsonConverter +{ + public override SpanException Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + public override void Write(Utf8JsonWriter writer, SpanException value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteStartObject("span"); + writer.WriteString("fileName", value.Span.FileName); + writer.WriteNumber("startLine", value.Span.StartLine); + writer.WriteNumber("endLine", value.Span.EndLine); + writer.WriteNumber("startColumn", value.Span.StartColumn); + writer.WriteNumber("endColumn", value.Span.EndColumn); + writer.WriteNumber("lines", value.Span.Lines); + writer.WriteEndObject(); + writer.WriteNumber("errorCode", value.ErrorCode); + writer.WriteString("severity", value.Severity.ToString().ToCamelCase()); + writer.WriteString("message", value.Message); + writer.WriteEndObject(); + } +} + +class ExceptionConverter : JsonConverter +{ + public override Exception Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + public override void Write(Utf8JsonWriter writer, Exception value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + if (value is CompilerException compilerException) + { + writer.WriteNumber("errorCode", compilerException.ErrorCode); + } + writer.WriteString("message", value.Message); + if (value.InnerException is not null) + { + writer.WritePropertyName("innerException"); + JsonSerializer.Serialize(writer, value.InnerException, options); + } + writer.WriteEndObject(); + } +} \ No newline at end of file diff --git a/Core/Parser/SchemaParser.cs b/Core/Parser/SchemaParser.cs index f48c7078..57c58913 100644 --- a/Core/Parser/SchemaParser.cs +++ b/Core/Parser/SchemaParser.cs @@ -6,6 +6,7 @@ using System.Numerics; using System.Text; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using Core.Exceptions; using Core.IO; @@ -34,18 +35,13 @@ public class SchemaParser private readonly Tokenizer _tokenizer; private readonly Dictionary _definitions = new(); private readonly List _imports = new(); - - /// - /// Whether the RPC boilerplate has already been generated. - /// - private bool _rpcBoilerplateGenerated = false; + /// /// A set of references to named types found in message/struct definitions: /// the left token is the type name, and the right token is the definition it's used in (used to report a helpful error). /// private readonly HashSet<(Token, Token)> _typeReferences = new(); private int _index; - private readonly string _nameSpace; private readonly List _errors = new(); private readonly List _warnings = new(); private List _tokens => _tokenizer.Tokens; @@ -98,22 +94,18 @@ private void CancelScope() /// Creates a new schema parser instance from some schema files on disk. /// /// The Bebop schema files that will be parsed - /// - public SchemaParser(List schemaPaths, string nameSpace) + public SchemaParser(IEnumerable schemaPaths) { _tokenizer = new Tokenizer(SchemaReader.FromSchemaPaths(schemaPaths)); - _nameSpace = nameSpace; } /// /// Creates a new schema parser instance and loads the schema into memory. /// /// A string representation of a schema. - /// - public SchemaParser(string textualSchema, string nameSpace) + public SchemaParser(string textualSchema) { _tokenizer = new Tokenizer(SchemaReader.FromTextualSchema(textualSchema)); - _nameSpace = nameSpace; } /// @@ -273,7 +265,7 @@ private string ConsumeBlockComments() /// Parse the current input files into an object. /// /// - public async Task Parse() + public BebopSchema Parse() { _index = 0; _errors.Clear(); @@ -296,7 +288,7 @@ public async Task Parse() try { - await _tokenizer.AddFile(fullPath); + _tokenizer.AddFile(fullPath); // Add the resolved path to known imports _imports.Add(fullPath); @@ -325,7 +317,6 @@ public async Task Parse() } } return new BebopSchema( - nameSpace: _nameSpace, definitions: _definitions, typeReferences: _typeReferences, parsingErrors: _errors, diff --git a/Laboratory/C++/run_test.sh b/Laboratory/C++/run_test.sh index ba517d1c..7d2f75b6 100755 --- a/Laboratory/C++/run_test.sh +++ b/Laboratory/C++/run_test.sh @@ -9,7 +9,7 @@ else # Linux or Mac bebopc="dotnet run --project ../../Compiler" fi -$bebopc --cpp "gen/$1.hpp" --files "../Schemas/Valid/$1.bop" +$bebopc --include "../Schemas/Valid/$1.bop" build --generator "cpp:gen/$1.hpp" >&2 echo "Timing C++ compiler:" time g++ -std=c++17 test/$1.cpp ./a.out diff --git a/Laboratory/Dart/test.sh b/Laboratory/Dart/test.sh index e70c0552..6e8597fe 100755 --- a/Laboratory/Dart/test.sh +++ b/Laboratory/Dart/test.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash mkdir -p gen -dotnet run --project ../../Compiler --files ../Schemas/Valid/{array_of_strings,const,jazz,request}.bop --dart gen/gen.dart +dotnet run --project ../../Compiler --trace -i ../Schemas/Valid/{array_of_strings,const,jazz,request}.bop build -g dart:gen/gen.dart pub run test diff --git a/Laboratory/Integration/run_test.js b/Laboratory/Integration/run_test.js index 106bb7ed..be67b493 100644 --- a/Laboratory/Integration/run_test.js +++ b/Laboratory/Integration/run_test.js @@ -4,7 +4,7 @@ const aout = process.platform === "win32" ? "a.exe" : "./a.out"; const py = process.platform === "win32" ? "py" : "python3"; shell.echo("Compiling schema..."); -if (shell.exec("dotnet run --project ../../Compiler --files schema.bop --cs schema.cs --ts schema.ts --cpp schema.hpp --rust Rust/src/schema.rs --py Python/src/schema.py").code !== 0) { +if (shell.exec("dotnet run --project ../../Compiler --include schema.bop build --generator 'cs:schema.cs' --generator 'ts:schema.ts' --generator 'cpp:schema.hpp' --generator 'rust:Rust/src/schema.rs' --generator 'py:Python/src/schema.py'").code !== 0) { shell.echo("Error: bebopc failed"); shell.exit(1); } diff --git a/Laboratory/Schemas/bebop.json b/Laboratory/Schemas/bebop.json index 8f14e25f..676635c5 100644 --- a/Laboratory/Schemas/bebop.json +++ b/Laboratory/Schemas/bebop.json @@ -1,18 +1,15 @@ { "inputDirectory": "./Valid", "exclude": ["./ShouldFail"], - "generators": [ - { - "alias": "cs", + "generators": { + "cs": { "outFile": "csoutput/models.cs" }, - { - "alias": "ts", + "ts": { "outFile": "tsoutput/models.ts" }, - { - "alias": "py", + "py": { "outFile": "pyoutput/models.py" } - ] + } } diff --git a/Laboratory/TypeScript/compile-schemas.ps1 b/Laboratory/TypeScript/compile-schemas.ps1 index 455e118c..a3458489 100644 --- a/Laboratory/TypeScript/compile-schemas.ps1 +++ b/Laboratory/TypeScript/compile-schemas.ps1 @@ -1 +1 @@ -dotnet run --project ..\..\Compiler --ts "test\generated\gen.ts" --files (gci ..\Schemas\Valid\*.bop) +dotnet run --project ..\..\Compiler --include (gci ..\Schemas\Valid\*.bop) --generator "ts:test\generated\gen.ts" diff --git a/Laboratory/TypeScript/compile-schemas.sh b/Laboratory/TypeScript/compile-schemas.sh index 0afb390c..d3011b55 100755 --- a/Laboratory/TypeScript/compile-schemas.sh +++ b/Laboratory/TypeScript/compile-schemas.sh @@ -1,4 +1,3 @@ #!/bin/bash -dotnet run --project ../../Compiler --ts "test/generated/gen.ts" --files ../Schemas/Valid/*.bop - +dotnet run --project ../../Compiler --trace --include ../Schemas/Valid/*.bop build --generator "ts:test/generated/gen.ts" \ No newline at end of file diff --git a/Repl/Pages/Index.razor b/Repl/Pages/Index.razor index 7b55c60b..5ce1940a 100644 --- a/Repl/Pages/Index.razor +++ b/Repl/Pages/Index.razor @@ -6,34 +6,36 @@
-
-
- +
+
+ +
+ +
+ +
-
- +
+ + + + @(Core.Meta.ReservedWords.CompilerName) @(DotEnv.Generated.Environment.Version)
-
- -
- - - - @(Core.Meta.ReservedWords.CompilerName) @(DotEnv.Generated.Environment.Version) -
@code { - private StandaloneCodeEditor _schemaEditor = null!; - private StandaloneCodeEditor _previewEditor = null!; + private StandaloneCodeEditor _schemaEditor = null!; + private StandaloneCodeEditor _previewEditor = null!; string _selectedGenerator = "ts"; string SelectedGenerator { @@ -61,45 +63,47 @@ } } - private StandaloneEditorConstructionOptions SchemaConstructionOptions(StandaloneCodeEditor editor) -{ - return new StandaloneEditorConstructionOptions - { - AutomaticLayout = true, - Theme = "vs-dark", - Language = "bebop", - Value = "struct Point { int32 x; int32 y; }", - FontLigatures = true, - FormatOnType = true, - FontFamily = "Fira Code", - FontSize = 16, - Minimap = new EditorMinimapOptions() { - Enabled = false - } - }; -} + private StandaloneEditorConstructionOptions SchemaConstructionOptions(StandaloneCodeEditor editor) + { + return new StandaloneEditorConstructionOptions + { + AutomaticLayout = true, + Theme = "vs-dark", + Language = "bebop", + Value = "struct Point { int32 x; int32 y; }", + FontLigatures = true, + FormatOnType = true, + FontFamily = "Fira Code", + FontSize = 16, + Minimap = new EditorMinimapOptions() + { + Enabled = false + } + }; + } private StandaloneEditorConstructionOptions PreviewConstructionOptions(StandaloneCodeEditor editor) -{ - return new StandaloneEditorConstructionOptions - { - AutomaticLayout = true, - Theme = "vs-dark", - Language = "text", - Value = "Generated code will appear here...", - FontLigatures = true, - FontFamily = "Fira Code", - FontSize = 15, - Minimap = new EditorMinimapOptions() { - Enabled = false - } - }; -} + { + return new StandaloneEditorConstructionOptions + { + AutomaticLayout = true, + Theme = "vs-dark", + Language = "text", + Value = "Generated code will appear here...", + FontLigatures = true, + FontFamily = "Fira Code", + FontSize = 15, + Minimap = new EditorMinimapOptions() + { + Enabled = false + } + }; + } -protected override async Task OnInitializedAsync() -{ - await JS.InvokeVoidAsync("registerBebop"); -} + protected override async Task OnInitializedAsync() + { + await JS.InvokeVoidAsync("registerBebop"); + } } @@ -108,24 +112,24 @@ protected override async Task OnInitializedAsync() public static string GetExampleSchema() { const string exampleSchema = @"enum Instrument { - Sax = 0; - Trumpet = 1; - Clarinet = 2; +Sax = 0; +Trumpet = 1; +Clarinet = 2; } readonly struct Musician { - string name; - Instrument plays; +string name; +Instrument plays; } message Song { - 1 -> string title; - 2 -> uint16 year; - 3 -> Musician[] performers; +1 -> string title; +2 -> uint16 year; +3 -> Musician[] performers; } struct Library { - map[guid, Song] songs; +map[guid, Song] songs; }"; return exampleSchema; } @@ -143,100 +147,112 @@ struct Library { } [JSInvokable] - public static async Task CompileSchema(string textualSchema, string generatorAlias) + public static Repl.CompilerOutput CompileSchema(string textualSchema, string generatorAlias) { + textualSchema = textualSchema?.Trim() ?? string.Empty; + generatorAlias = generatorAlias?.Trim() ?? string.Empty; if (string.IsNullOrWhiteSpace(textualSchema)) { return new Repl.CompilerOutput - { - IsOk = false, - Result = "No schema was provided" - }; + { + IsOk = false, + Result = "No schema was provided" + }; } if (string.IsNullOrWhiteSpace(generatorAlias)) { return new Repl.CompilerOutput - { - IsOk = false, - Result = "No code generator was specified" - }; + { + IsOk = false, + Result = "No code generator was specified" + }; } if (!GeneratorUtils.ImplementedGenerators.ContainsKey(generatorAlias)) { return new Repl.CompilerOutput - { - IsOk = false, - Result = $"The specified generator '{generatorAlias}' is not valid." - }; + { + IsOk = false, + Result = $"The specified generator '{generatorAlias}' is not valid." + }; } try { - var parser = new SchemaParser(textualSchema, string.Empty); - var schema = await parser.Parse(); + var parser = new SchemaParser(textualSchema); + var schema = parser.Parse(); _ = schema.Validate(); - var diagonstics = schema.Errors.Concat(schema.Warnings).ToList(); + var diagonstics = schema.Errors.Concat(schema.Warnings).ToList(); var hasErrorDiagonstics = diagonstics.Any((d) => d.Severity is Core.Exceptions.Severity.Error); + var generatorConfig = new GeneratorConfig(generatorAlias, "stdout"); + var generator = GeneratorUtils.ImplementedGenerators[generatorAlias](schema, generatorConfig); return new Repl.CompilerOutput - { - IsOk = !hasErrorDiagonstics, - Result = hasErrorDiagonstics ? string.Empty : GeneratorUtils.ImplementedGenerators[generatorAlias](schema).Compile(null, writeGeneratedNotice: false, emitBinarySchema: true), - Diagonstics = diagonstics - }; + { + IsOk = !hasErrorDiagonstics, + Result = hasErrorDiagonstics ? string.Empty : generator.Compile(), + Diagonstics = diagonstics + }; } catch (Exception e) { return new Repl.CompilerOutput - { - IsOk = false, - Result = e.ToString() - }; + { + IsOk = false, + Result = e.ToString() + }; } } private async Task ShowOutput() { - - var co = await CompileSchema(_schema, SelectedGenerator); - if (co.IsOk) { - var languageId = SelectedGenerator switch { - "ts" => "typescript", - "cs" => "csharp", - "go" => "go", - "java" => "java", - "cpp" => "cpp", - "rust" => "rust", - "dart" => "dart", - "py" => "python" - }; - var model = await _previewEditor.GetModel(); - await _previewEditor.UpdateOptions(new EditorUpdateOptions - { - ReadOnly = true - }); - await Global.SetModelLanguage(model,languageId); - await _previewEditor.SetValue(co.Result); - + + var co = CompileSchema(_schema, SelectedGenerator); + if (co.IsOk) + { + var languageId = SelectedGenerator switch + { + "ts" => "typescript", + "cs" => "csharp", + "go" => "go", + "java" => "java", + "cpp" => "cpp", + "rust" => "rust", + "dart" => "dart", + "py" => "python" + }; + var model = await _previewEditor.GetModel(); + await _previewEditor.UpdateOptions(new EditorUpdateOptions + { + ReadOnly = true + }); + await Global.SetModelLanguage(model, languageId); + await _previewEditor.SetValue(co.Result); + } await JS.InvokeVoidAsync("disableError", _previewEditor.Id); - if(co.Diagonstics is not null) { - var diagonstics = new List(); - foreach(var err in co.Diagonstics) { - var range = new BlazorMonaco.Range(err.Span.StartLine + 1, err.Span.StartColumn + 1, err.Span.EndLine + 1, err.Span.EndColumn + 1); - diagonstics.Add(new { - severity = err.Severity is Core.Exceptions.Severity.Error ? 8 : 4, - startLineNumber = range.StartLineNumber, - startColumn = range.StartColumn, - endLineNumber = range.EndLineNumber, - endColumn = range.EndColumn, - message = err.Message, + if (co.Diagonstics is not null) + { + var diagonstics = new List(); + foreach (var err in co.Diagonstics) + { + var range = new BlazorMonaco.Range(err.Span.StartLine + 1, err.Span.StartColumn + 1, err.Span.EndLine + 1, + err.Span.EndColumn + 1); + diagonstics.Add(new + { + severity = err.Severity is Core.Exceptions.Severity.Error ? 8 : 4, + startLineNumber = range.StartLineNumber, + startColumn = range.StartColumn, + endLineNumber = range.EndLineNumber, + endColumn = range.EndColumn, + message = err.Message, }); - } + } await JS.InvokeVoidAsync("setMarkers", _schemaEditor.Id, diagonstics.ToArray()); - } else { - await JS.InvokeVoidAsync("clearMarkers", _schemaEditor.Id); + } + else + { + await JS.InvokeVoidAsync("clearMarkers", _schemaEditor.Id); } } diff --git a/Repl/Repl.csproj b/Repl/Repl.csproj index 226213d9..b97972a0 100644 --- a/Repl/Repl.csproj +++ b/Repl/Repl.csproj @@ -22,10 +22,10 @@ - - - - + + + + diff --git a/Tools/bash/install.sh b/Tools/bash/install.sh index 2f58dea7..556404d6 100755 --- a/Tools/bash/install.sh +++ b/Tools/bash/install.sh @@ -383,13 +383,11 @@ fi # check if bebopc is already installed if [[ -x "$(which bebopc)" ]]; then - bebopc_version_output="$(bebopc --version 2>/dev/null)" - readonly bebopc_version_output - readonly bebopc_name_and_version="${bebopc_version_output%% (*}" - readonly installed_bebopc_version="${bebopc_name_and_version##* }" + installed_bebopc_version="$(bebopc --version 2>/dev/null)" + readonly installed_bebopc_version # exit when the remote version of bebopc is the same as the currently installed version if [[ "${installed_bebopc_version}" == "${BEBOPC_VERSION}" ]]; then - point "${ROCKET_UTF8} ${tty_underline}${tty_white}$bebopc_name_and_version${tty_reset} is already installed and up-to-date" + point "${ROCKET_UTF8} ${tty_underline}${tty_white}bebopc $installed_bebopc_version${tty_reset} is already installed and up-to-date" exit 0 fi # abort when the remote version is less than the installed version. @@ -397,7 +395,7 @@ if [[ -x "$(which bebopc)" ]]; then if version_lt "$(major_minor "${BEBOPC_VERSION}")" "$(major_minor "${installed_bebopc_version}")"; then abort "$( cat <, destination: impl AsRef, con println!("cargo:rerun-if-changed={}", schema.to_str().unwrap()); let mut cmd = Command::new(compiler_path); - if config.skip_generated_notice { - cmd.arg("--skip-generated-notice"); - } let output = cmd - .arg("--files") + .arg("-i") .arg(schema) - .arg("--rust") - .arg(destination.to_str().unwrap()) + .arg("build") + .arg("--generator") + .arg(format!( + "rust:{},noEmitNotice={}", + destination.to_str().unwrap(), + config.skip_generated_notice + )) .output() .expect("Could not run bebopc"); diff --git a/Tools/cmake/Bebop.cmake b/Tools/cmake/Bebop.cmake index a6a0e911..3882c806 100644 --- a/Tools/cmake/Bebop.cmake +++ b/Tools/cmake/Bebop.cmake @@ -1,81 +1,85 @@ -set(BEBOP_RELEASES_URL https://github.com/RainwayApp/bebop/releases/download - CACHE STRING "Public location of Bebop binary releases" FORCE) +set(BEBOP_RELEASES_URL https://github.com/betwixt-labs/bebop/releases/download + CACHE STRING "Public location of Bebop binary releases" FORCE) -set(BEBOP_LANGUAGES cpp cs ts dart rust) +set(BEBOP_LANGUAGES cpp cs ts dart rust py) include(FetchContent) function(Bebop_Generate target_name) - set(_options) - set(_unaryargs VERSION LANGUAGE OUTPUT NAMESPACE) - set(_varargs BOPS) + set(_options) + set(_unaryargs VERSION LANGUAGE OUTPUT NAMESPACE OPTIONS) + set(_varargs BOPS) - cmake_parse_arguments(PARSE_ARGV 1 Bebop_Generate "${_options}" "${_unaryargs}" "${_varargs}") + cmake_parse_arguments(PARSE_ARGV 1 Bebop_Generate "${_options}" "${_unaryargs}" "${_varargs}") - if(NOT Bebop_Generate_BOPS) - message(SEND_ERROR "Error: Bebop_Generate was not given any BOPS as input") - endif() + if(NOT Bebop_Generate_BOPS) + message(SEND_ERROR "Error: Bebop_Generate was not given any BOPS as input") + endif() - if(NOT Bebop_Generate_VERSION) - message(SEND_ERROR "Error: Bebop_Generate must be pinned to a VERSION") - endif() - set(_bebopc_prefix "bebopc_${Bebop_Generate_VERSION}") + if(NOT Bebop_Generate_VERSION) + message(SEND_ERROR "Error: Bebop_Generate must be pinned to a VERSION") + endif() + set(_bebopc_prefix "bebopc_${Bebop_Generate_VERSION}") - if(NOT Bebop_Generate_LANGUAGE) - set(Bebop_Generate_LANGUAGE cpp) - endif() - string(TOLOWER "${Bebop_Generate_LANGUAGE}" Bebop_Generate_LANGUAGE) - list(FIND BEBOP_LANGUAGES ${Bebop_Generate_LANGUAGE} _i) - if(_i EQUAL -1) - message(SEND_ERROR "Error: Bebop_Generate was given an unknown LANGUAGE \"${Bebop_Generate_LANGUAGE}\"") - endif() + if(NOT Bebop_Generate_LANGUAGE) + set(Bebop_Generate_LANGUAGE cpp) + endif() + string(TOLOWER "${Bebop_Generate_LANGUAGE}" Bebop_Generate_LANGUAGE) + list(FIND BEBOP_LANGUAGES ${Bebop_Generate_LANGUAGE} _i) + if(_i EQUAL -1) + message(SEND_ERROR "Error: Bebop_Generate was given an unknown LANGUAGE \"${Bebop_Generate_LANGUAGE}\"") + endif() - if(NOT Bebop_Generate_OUTPUT) - message(SEND_ERROR "Error: Bebop_Generate not given an OUTPUT path") - endif() + if(NOT Bebop_Generate_OUTPUT) + message(SEND_ERROR "Error: Bebop_Generate not given an OUTPUT path") + endif() - if(Bebop_Generate_NAMESPACE) - set(_namespace_args --namespace "${Bebop_Generate_NAMESPACE}") - else() - set(_namespace_args) - endif() - - set(_bebopc_executable_name "bebopc") + set(_bebopc_executable_name "bebopc") - - string( TOLOWER "${CMAKE_HOST_SYSTEM_PROCESSOR}" _system_processor ) - - if (_system_processor STREQUAL "amd64") - set(_system_processor "x64") - endif() + string(TOLOWER "${CMAKE_HOST_SYSTEM_PROCESSOR}" _system_processor) + + if(_system_processor STREQUAL "amd64") + set(_system_processor "x64") + elseif(_system_processor STREQUAL "arm64") + set(_system_processor "arm64") + endif() - if(NOT ${_bebopc_prefix}_POPULATED) - if(CMAKE_HOST_WIN32) - string(APPEND _bebopc_executable_name ".exe") - set(_bebopc_zip "bebopc-windows-${_system_processor}.zip") - elseif(CMAKE_HOST_APPLE) - set(_bebopc_zip "bebopc-macos-${_system_processor}.zip") - else() - set(_bebopc_zip "bebopc-linux-${_system_processor}.zip") - endif() - - set(_bebopc_zip_url "${BEBOP_RELEASES_URL}/${Bebop_Generate_VERSION}/${_bebopc_zip}") + if(NOT ${_bebopc_prefix}_POPULATED) + if(CMAKE_HOST_WIN32) + string(APPEND _bebopc_executable_name ".exe") + set(_bebopc_zip "bebopc-windows-${_system_processor}.zip") + elseif(CMAKE_HOST_APPLE) + set(_bebopc_zip "bebopc-macos-${_system_processor}.zip") + else() + set(_bebopc_zip "bebopc-linux-${_system_processor}.zip") + endif() + + set(_bebopc_zip_url "${BEBOP_RELEASES_URL}/${Bebop_Generate_VERSION}/${_bebopc_zip}") + + FetchContent_Declare(${_bebopc_prefix} + URL "${_bebopc_zip_url}" + ) + FetchContent_Populate(${_bebopc_prefix}) + endif() + set(_bebopc "${${_bebopc_prefix}_SOURCE_DIR}/${_bebopc_executable_name}") + + set(_includeArgs --include) + foreach(_bop IN LISTS Bebop_Generate_BOPS) + list(APPEND _includeArgs ${_bop}) + endforeach() - FetchContent_Declare(${_bebopc_prefix} - URL "${_bebopc_zip_url}" - ) - FetchContent_Populate(${_bebopc_prefix}) - endif() - set(_bebopc "${${_bebopc_prefix}_SOURCE_DIR}/${_bebopc_executable_name}") + set(_generatorArgs "--generator" "${Bebop_Generate_LANGUAGE}:${Bebop_Generate_OUTPUT}") + if(Bebop_Generate_OPTIONS) + string(APPEND _generatorArgs ",${Bebop_Generate_OPTIONS}") + endif() - add_custom_command( - OUTPUT ${Bebop_Generate_OUTPUT} - COMMAND "${_bebopc}" - "--${Bebop_Generate_LANGUAGE}" "${Bebop_Generate_OUTPUT}" - ${_namespace_args} - --files ${Bebop_Generate_BOPS} - DEPENDS ${BEBOP_COMPILER} ${Bebop_Generate_BOPS} - ) + add_custom_command( + OUTPUT ${Bebop_Generate_OUTPUT} + COMMAND "${_bebopc}" + ${_includeArgs} build + ${_generatorArgs} + DEPENDS ${BEBOP_COMPILER} ${Bebop_Generate_BOPS} + ) - add_custom_target(${target_name} DEPENDS ${Bebop_Generate_OUTPUT}) -endfunction() \ No newline at end of file + add_custom_target(${target_name} DEPENDS ${Bebop_Generate_OUTPUT}) +endfunction() diff --git a/Tools/cmake/CMakeLists.txt b/Tools/cmake/CMakeLists.txt index ad26a2ac..96be25fc 100644 --- a/Tools/cmake/CMakeLists.txt +++ b/Tools/cmake/CMakeLists.txt @@ -12,7 +12,7 @@ file(GLOB bop_files CONFIGURE_DEPENDS ../../Laboratory/Schemas/Valid/*.bop) bebop_generate(generate_bop_hpp VERSION "v${BEBOPC_VERSION}" LANGUAGE cpp - NAMESPACE rw::bop OUTPUT ${bop_hpp} BOPS ${bop_files} + OPTIONS "namespace=rw::bop" ) diff --git a/Tools/ps/install.ps1 b/Tools/ps/install.ps1 index 98c0708c..77c9ad3a 100644 --- a/Tools/ps/install.ps1 +++ b/Tools/ps/install.ps1 @@ -164,7 +164,7 @@ function Test-BebopcInstalled { function Test-BebopcVersion { $compilerPath = "$env:PROGRAMDATA\bebop\bebopc.exe" - $installedVersion = ([string](& "$compilerPath" --version)).Split(' ')[1].Trim() + $installedVersion = ([string](& "$compilerPath" --version)).Trim() $remoteVersion = $bebopcVersion if ([System.Version]$installedVersion -gt [System.Version]$remoteVersion) { # Installed version is greater than the version to install diff --git a/Tools/vs/bebop-tools.nuspec b/Tools/vs/bebop-tools.nuspec index eb90ac2d..f7217d1b 100644 --- a/Tools/vs/bebop-tools.nuspec +++ b/Tools/vs/bebop-tools.nuspec @@ -10,7 +10,7 @@ false Apache-2.0 https://licenses.nuget.org/Apache-2.0 - https://github.com/RainwayApp/bebop + https://github.com/betwixt-labs/bebop The Bebop compiler for managed C# projects and native C++ projects. Add this package to a project that contains .bop files to be compiled to code. diff --git a/Tools/vs/build/bebop-tools.targets b/Tools/vs/build/bebop-tools.targets index 9d831bd8..fc242dc6 100644 --- a/Tools/vs/build/bebop-tools.targets +++ b/Tools/vs/build/bebop-tools.targets @@ -19,6 +19,7 @@ MSBuild + 9.0 @@ -44,6 +45,7 @@ + @@ -62,7 +64,7 @@