diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7a51ba6..d2aed10 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,12 +16,12 @@ jobs:
fail-fast: false
matrix:
configuration: [CSharp, VisualBasic]
- project: [Analyzers, CodeFixers, Extensions, HighPerformance, SourceGenerators, DynamicCast, Swagger]
+ project: [Analyzers, CodeFixers, Extensions, HighPerformance, SourceGenerators, DynamicCast, Kiota]
exclude:
- configuration: VisualBasic
project: DynamicCast
- configuration: VisualBasic
- project: Swagger
+ project: Kiota
env:
PROJECT: ${{ matrix.project }}
diff --git a/CompilerPlatform.slnx b/CompilerPlatform.slnx
index 75a6797..de535ba 100644
--- a/CompilerPlatform.slnx
+++ b/CompilerPlatform.slnx
@@ -9,7 +9,7 @@
-
+
@@ -26,7 +26,7 @@
-
+
diff --git a/Directory.Build.props b/Directory.Build.props
index a540c34..1f9add8 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,6 +1,8 @@
+
+
$(MSBuildThisFileDirectory)
$(RootDirectory)src
@@ -30,4 +32,5 @@
+
\ No newline at end of file
diff --git a/src/features/Riverside.CompilerPlatform.Features.Kiota/Kiota.props b/src/features/Riverside.CompilerPlatform.Features.Kiota/Kiota.props
new file mode 100644
index 0000000..aa54407
--- /dev/null
+++ b/src/features/Riverside.CompilerPlatform.Features.Kiota/Kiota.props
@@ -0,0 +1,67 @@
+
+
+
+ 7.3
+ 1.21.2
+ 1.21.2
+
+
+
+
+
+
+ <_Kiota_TargetFramework Include="$(TargetFramework)" Condition="'$(TargetFramework)' != ''" />
+ <_Kiota_TargetFramework Include="$(TargetFrameworks)" Condition="'$(TargetFrameworks)' != ''" />
+ <_Kiota_InvalidFramework Include="@(_Kiota_TargetFramework)"
+ Condition="!$([System.Text.RegularExpressions.Regex]::IsMatch('%(Identity)', '^netstandard2\.(0|1)$|^net462$|^net([8-9]|[1-9][0-9])(?:\.[0-9]+)?(?:[-.].*)?$'))" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/features/Riverside.CompilerPlatform.Features.Swagger/KiotaEngine.Enums.cs b/src/features/Riverside.CompilerPlatform.Features.Kiota/KiotaEngine.Enums.cs
similarity index 91%
rename from src/features/Riverside.CompilerPlatform.Features.Swagger/KiotaEngine.Enums.cs
rename to src/features/Riverside.CompilerPlatform.Features.Kiota/KiotaEngine.Enums.cs
index 46ee5ea..90663f7 100644
--- a/src/features/Riverside.CompilerPlatform.Features.Swagger/KiotaEngine.Enums.cs
+++ b/src/features/Riverside.CompilerPlatform.Features.Kiota/KiotaEngine.Enums.cs
@@ -1,4 +1,4 @@
-namespace Riverside.CompilerPlatform.Features.Swagger;
+namespace Riverside.CompilerPlatform.Features.Kiota;
partial class KiotaEngine
{
diff --git a/src/features/Riverside.CompilerPlatform.Features.Swagger/KiotaEngine.Methods.cs b/src/features/Riverside.CompilerPlatform.Features.Kiota/KiotaEngine.Methods.cs
similarity index 71%
rename from src/features/Riverside.CompilerPlatform.Features.Swagger/KiotaEngine.Methods.cs
rename to src/features/Riverside.CompilerPlatform.Features.Kiota/KiotaEngine.Methods.cs
index cf50361..ac5ead9 100644
--- a/src/features/Riverside.CompilerPlatform.Features.Swagger/KiotaEngine.Methods.cs
+++ b/src/features/Riverside.CompilerPlatform.Features.Kiota/KiotaEngine.Methods.cs
@@ -1,6 +1,7 @@
-using System.Text;
+using Riverside.CompilerPlatform.Helpers;
+using System.Text;
-namespace Riverside.CompilerPlatform.Features.Swagger;
+namespace Riverside.CompilerPlatform.Features.Kiota;
partial class KiotaEngine
{
@@ -13,19 +14,19 @@ partial class KiotaEngine
///
public override string ToString()
{
- var command = new StringBuilder().Append("kiota generate");
+ var command = new StringBuilder().Append("generate");
if (!string.IsNullOrWhiteSpace(Path))
{
- command.Append($" --openapi {Path}");
+ command.Append($" --openapi {SanitizationHelpers.EscapeArg(Path!)}");
}
if (!string.IsNullOrWhiteSpace(Manifest))
{
- command.Append($" --manifest {Manifest}");
+ command.Append($" --manifest {SanitizationHelpers.EscapeArg(Manifest!)}");
}
if (!string.IsNullOrWhiteSpace(Output))
{
- command.Append($" --output {Output}");
+ command.Append($" --output {SanitizationHelpers.EscapeArg(Output!)}");
}
command.Append($" --language {Language}");
if (!string.IsNullOrWhiteSpace(ClassName))
@@ -56,68 +57,68 @@ public override string ToString()
{
command.Append($" --additional-data {AdditionalData}");
}
- if (Serializer is not null)
+ if (Serializer is not null && Serializer.Length > 0)
{
var serializers = new StringBuilder().Append(" --serializer ");
foreach (var serializer in Serializer)
{
serializers.Append(serializer + "|");
}
- serializers.Remove(serializers.Length, 1); // remove final '|' char
+ serializers.Remove(serializers.Length - 1, 1); // remove final '|' char
command.Append(serializers.ToString());
}
- if (Deserializer is not null)
+ if (Deserializer is not null && Deserializer.Length > 0)
{
var deserializers = new StringBuilder().Append(" --deserializer ");
foreach (var deserializer in Deserializer)
{
deserializers.Append(deserializer + "|");
}
- deserializers.Remove(deserializers.Length, 1); // remove final '|' char
+ deserializers.Remove(deserializers.Length - 1, 1); // remove final '|' char
command.Append(deserializers.ToString());
}
if (CleanOutput is not null)
{
command.Append($" --clean-output {CleanOutput}");
}
- if (StructuredMimeTypes is not null)
+ if (StructuredMimeTypes is not null && StructuredMimeTypes.Length > 0)
{
var structuredMimeTypes = new StringBuilder().Append(" --structured-mime-types ");
foreach (var structuredMimeType in StructuredMimeTypes)
{
structuredMimeTypes.Append(structuredMimeType + "|");
}
- structuredMimeTypes.Remove(structuredMimeTypes.Length, 1); // remove final '|' char
+ structuredMimeTypes.Remove(structuredMimeTypes.Length - 1, 1); // remove final '|' char
command.Append(structuredMimeTypes.ToString());
}
- if (IncludePath is not null)
+ if (IncludePath is not null && IncludePath.Length > 0)
{
var includePaths = new StringBuilder().Append(" --include-path ");
foreach (var includePath in IncludePath)
{
includePaths.Append(includePath + "|");
}
- includePaths.Remove(includePaths.Length, 1); // remove final '|' char
+ includePaths.Remove(includePaths.Length - 1, 1); // remove final '|' char
command.Append(includePaths.ToString());
}
- if (ExcludePath is not null)
+ if (ExcludePath is not null && ExcludePath.Length > 0)
{
var excludePaths = new StringBuilder().Append(" --exclude-path ");
foreach (var excludePath in ExcludePath)
{
excludePaths.Append(excludePath + "|");
}
- excludePaths.Remove(excludePaths.Length, 1); // remove final '|' char
+ excludePaths.Remove(excludePaths.Length - 1, 1); // remove final '|' char
command.Append(excludePaths.ToString());
}
- if (DisableValidationRules is not null)
+ if (DisableValidationRules is not null && DisableValidationRules.Length > 0)
{
var disableValidationRules = new StringBuilder().Append(" --disable-validation-rules ");
foreach (var disableValidationRule in DisableValidationRules)
{
disableValidationRules.Append(disableValidationRule + "|");
}
- disableValidationRules.Remove(disableValidationRules.Length, 1); // remove final '|' char
+ disableValidationRules.Remove(disableValidationRules.Length - 1, 1); // remove final '|' char
command.Append(disableValidationRules.ToString());
}
if (ClearCache is not null)
diff --git a/src/features/Riverside.CompilerPlatform.Features.Swagger/KiotaEngine.cs b/src/features/Riverside.CompilerPlatform.Features.Kiota/KiotaEngine.cs
similarity index 93%
rename from src/features/Riverside.CompilerPlatform.Features.Swagger/KiotaEngine.cs
rename to src/features/Riverside.CompilerPlatform.Features.Kiota/KiotaEngine.cs
index d384f22..aff3c43 100644
--- a/src/features/Riverside.CompilerPlatform.Features.Swagger/KiotaEngine.cs
+++ b/src/features/Riverside.CompilerPlatform.Features.Kiota/KiotaEngine.cs
@@ -1,6 +1,6 @@
-using System.Text;
+using System.Diagnostics.CodeAnalysis;
-namespace Riverside.CompilerPlatform.Features.Swagger;
+namespace Riverside.CompilerPlatform.Features.Kiota;
///
/// Represents the configuration and options for generating code using the Kiota engine.
@@ -128,6 +128,7 @@ public partial class KiotaEngine
/// The target programming language for code generation.
/// The name of the root class to be generated. Can be null to use a default class name.
/// The access modifier to apply to generated types. Can be null to use the default accessibility.
+ /// The namespace for the generated client class. Can be null to use the default namespace.
/// The log level to use for diagnostic output during generation. Can be null to use the default log level.
/// Indicates whether to use a backing store for generated models. If null, the default behavior is used.
/// Indicates whether to exclude backward compatible code from the output. If null, the default behavior is used.
@@ -141,7 +142,8 @@ public partial class KiotaEngine
/// An array of validation rules to disable during generation. Can be null to enable all rules.
/// Indicates whether to clear the internal cache before generation. If null, the default behavior is used.
/// Indicates whether to disable SSL validation for network operations. If null, the default behavior is used.
- public KiotaEngine(string? d, string? a, string? o, GenerationLanguage l, string? c, Accessibility? tam, ConsoleLogLevel? ll, bool? b, bool? ebc, bool? ad, string[]? s, string[]? ds, bool? co, string[]? m, string[]? i, string[]? e, ValidationRules[]? dvr, bool? cc, bool? dsv)
+ [SetsRequiredMembers]
+ public KiotaEngine(string? d, string? a, string? o, GenerationLanguage l, string? c, Accessibility? tam, string? n, ConsoleLogLevel? ll, bool? b, bool? ebc, bool? ad, string[]? s, string[]? ds, bool? co, string[]? m, string[]? i, string[]? e, ValidationRules[]? dvr, bool? cc, bool? dsv)
{
Path = d;
Manifest = a;
@@ -149,6 +151,7 @@ public KiotaEngine(string? d, string? a, string? o, GenerationLanguage l, string
Language = l;
ClassName = c;
TypeAccessModifier = tam;
+ NamespaceName = n;
LogLevel = ll;
BackingStore = b;
ExcludeBackwardCompatible = ebc;
diff --git a/src/features/Riverside.CompilerPlatform.Features.Kiota/KiotaGenerator.cs b/src/features/Riverside.CompilerPlatform.Features.Kiota/KiotaGenerator.cs
new file mode 100644
index 0000000..177f3a9
--- /dev/null
+++ b/src/features/Riverside.CompilerPlatform.Features.Kiota/KiotaGenerator.cs
@@ -0,0 +1,193 @@
+using Riverside.CompilerPlatform.Extensions;
+using Riverside.CompilerPlatform.Helpers;
+using Riverside.CompilerPlatform.SourceGenerators;
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+
+namespace Riverside.CompilerPlatform.Features.Kiota;
+
+///
+/// Generates source code from OpenAPI specification files as part of the build process.
+///
+[Generator]
+public partial class KiotaGenerator : IncrementalGenerator
+{
+ private const string VersionProperty = "build_property.Kiota_Version";
+ private const string LanguageProperty = "build_property.KiotaGenerator_Language";
+ private const string ClassNameProperty = "build_property.KiotaGenerator_ClassName";
+ private const string NamespaceNameProperty = "build_property.KiotaGenerator_NamespaceName";
+ private const string TypeAccessModifierProperty = "build_property.KiotaGenerator_TypeAccessModifier";
+ private const string LogLevelProperty = "build_property.KiotaGenerator_LogLevel";
+ private const string BackingStoreProperty = "build_property.KiotaGenerator_BackingStore";
+ private const string ExcludeBackwardCompatibleProperty = "build_property.KiotaGenerator_ExcludeBackwardCompatible";
+ private const string AdditionalDataProperty = "build_property.KiotaGenerator_AdditionalData";
+ private const string SerializerProperty = "build_property.KiotaGenerator_Serializer";
+ private const string DeserializerProperty = "build_property.KiotaGenerator_Deserializer";
+ private const string CleanOutputProperty = "build_property.KiotaGenerator_CleanOutput";
+ private const string StructuredMimeTypesProperty = "build_property.KiotaGenerator_StructuredMimeTypes";
+ private const string IncludePathProperty = "build_property.KiotaGenerator_IncludePath";
+ private const string ExcludePathProperty = "build_property.KiotaGenerator_ExcludePath";
+ private const string DisableValidationRulesProperty = "build_property.KiotaGenerator_DisableValidationRules";
+ private const string ClearCacheProperty = "build_property.KiotaGenerator_ClearCache";
+ private const string DisableSSLValidationProperty = "build_property.KiotaGenerator_DisableSSLValidation";
+
+ private static readonly string ToolDirectory = Path.Combine(
+ Path.GetTempPath(), "Roslyn", "Advanced Compiler Services for .NET", "KiotaGenerator");
+
+ ///
+ protected override void OnBeforeGeneration(GeneratorContext context, CancellationToken cancellationToken)
+ {
+ var options = context.AnalyzerConfigOptions.GlobalOptions;
+
+ var specs = context.AdditionalTexts
+ .Where(at => at.Path.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase)
+ || at.Path.EndsWith(".yml", StringComparison.OrdinalIgnoreCase)
+ || at.Path.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
+ .ToImmutableArray();
+
+ if (specs.IsEmpty)
+ return;
+
+ var version = options.GetString(VersionProperty);
+
+ // Kiota engine args
+ var language = options.GetNullableEnum(LanguageProperty)
+ ?? KiotaEngine.GenerationLanguage.CSharp;
+ var className = options.GetString(ClassNameProperty);
+ var namespaceName = options.GetString(NamespaceNameProperty);
+ var typeAccessModifier = options.GetNullableEnum(TypeAccessModifierProperty);
+ var logLevel = options.GetNullableEnum(LogLevelProperty);
+ var backingStore = options.GetNullableBool(BackingStoreProperty);
+ var excludeBackwardCompatible = options.GetNullableBool(ExcludeBackwardCompatibleProperty);
+ var additionalData = options.GetNullableBool(AdditionalDataProperty);
+ var serializers = options.GetPipeSeparatedArray(SerializerProperty);
+ var deserializers = options.GetPipeSeparatedArray(DeserializerProperty);
+ var cleanOutput = options.GetNullableBool(CleanOutputProperty);
+ var structuredMimeTypes = options.GetPipeSeparatedArray(StructuredMimeTypesProperty);
+ var includePaths = options.GetPipeSeparatedArray(IncludePathProperty);
+ var excludePaths = options.GetPipeSeparatedArray(ExcludePathProperty);
+ var disableValidationRules = options.GetPipeSeparatedEnumArray(DisableValidationRulesProperty);
+ var clearCache = options.GetNullableBool(ClearCacheProperty);
+ var disableSSLValidation = options.GetNullableBool(DisableSSLValidationProperty);
+
+ string toolExecutable;
+ try
+ {
+ var (installed, installError) = NETCoreToolHelpers
+ .EnsureToolAsync("Microsoft.OpenApi.Kiota", ToolDirectory, version, commandName: "kiota")
+ .GetAwaiter().GetResult();
+
+ if (!installed)
+ {
+ CreateDiagnostic(
+ "KG0000",
+ "Microsoft Kiota installation failed",
+ installError ?? "Failed to install or locate the Microsoft Kiota tool.").Report(context);
+ return;
+ }
+
+ toolExecutable = NETCoreToolHelpers.GetExecutablePath(ToolDirectory, "kiota");
+ }
+ catch (Exception ex)
+ {
+ CreateDiagnostic("KG0000", "Microsoft Kiota installation failed", ex.Message).Report(context);
+ return;
+ }
+
+ foreach (var spec in specs)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var specPath = spec.Path;
+ if (!File.Exists(specPath))
+ continue;
+
+ var specFileName = Path.GetFileNameWithoutExtension(specPath);
+ var effectiveNamespace = namespaceName ?? SanitizationHelpers.Sanitize(specFileName);
+
+ var tempOut = DirectoryHelpers.CreateTemporary(
+ Path.Combine(Path.GetTempPath(), "Roslyn", "Advanced Compiler Services for .NET"));
+
+ try
+ {
+ var engine = new KiotaEngine(
+ d: specPath,
+ a: null,
+ o: tempOut,
+ l: language,
+ c: className,
+ n: effectiveNamespace,
+ tam: typeAccessModifier,
+ ll: logLevel,
+ b: backingStore,
+ ebc: excludeBackwardCompatible,
+ ad: additionalData,
+ s: serializers,
+ ds: deserializers,
+ co: cleanOutput,
+ m: structuredMimeTypes,
+ i: includePaths,
+ e: excludePaths,
+ dvr: disableValidationRules,
+ cc: clearCache,
+ dsv: disableSSLValidation);
+
+ var runResult = ProcessHelpers
+ .RunProcess(toolExecutable, engine.ToString(), TimeSpan.FromMinutes(2))
+ .GetAwaiter().GetResult();
+
+ if (runResult.ExitCode != 0)
+ {
+ CreateDiagnostic(
+ "KG0001",
+ "OpenAPI generation failed",
+ $"Microsoft Kiota failed for spec '{specPath}' with exit code {runResult.ExitCode}: {runResult.StandardError.ReplaceLineEndings(" ")}").Report(context);
+ DirectoryHelpers.TryDelete(tempOut);
+ continue;
+ }
+
+ var csFiles = Directory.EnumerateFiles(tempOut, "*.cs", SearchOption.AllDirectories).ToArray();
+ if (csFiles.Length == 0)
+ {
+ CreateDiagnostic(
+ "KG0002",
+ "No C# files generated",
+ $"Microsoft Kiota produced no C# files for spec '{specPath}'").Report(context);
+ DirectoryHelpers.TryDelete(tempOut);
+ continue;
+ }
+
+ foreach (var cs in csFiles)
+ {
+ try
+ {
+ var content = File.ReadAllText(cs, Encoding.UTF8);
+ var rel = Path.GetRelativePath(tempOut, cs)
+ .Replace(Path.DirectorySeparatorChar, '.')
+ .Replace(Path.AltDirectorySeparatorChar, '.');
+ var hintName = $"{SanitizationHelpers.Sanitize(engine.NamespaceName!)}.{SanitizationHelpers.Sanitize(rel)}";
+ AddSource(hintName, content);
+ }
+ catch (Exception ex)
+ {
+ CreateDiagnostic(
+ "KG0003",
+ "Failed to add generated file",
+ $"Failed to add '{cs}': {ex.Message}").Report(context);
+ }
+ }
+
+ DirectoryHelpers.TryDelete(tempOut);
+ }
+ catch (Exception ex)
+ {
+ CreateDiagnostic("KG9999", "OpenAPI generator exception", ex.ToString()).Report(context);
+ DirectoryHelpers.TryDelete(tempOut);
+ }
+ }
+ }
+}
diff --git a/src/features/Riverside.CompilerPlatform.Features.Kiota/Riverside.CompilerPlatform.Features.Kiota.csproj b/src/features/Riverside.CompilerPlatform.Features.Kiota/Riverside.CompilerPlatform.Features.Kiota.csproj
new file mode 100644
index 0000000..b3b6e17
--- /dev/null
+++ b/src/features/Riverside.CompilerPlatform.Features.Kiota/Riverside.CompilerPlatform.Features.Kiota.csproj
@@ -0,0 +1,42 @@
+
+
+
+ netstandard2.0
+ CSharp
+ false
+ $(NoWarn);NU5128
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/features/Riverside.CompilerPlatform.Features.Swagger/KiotaGenerator.cs b/src/features/Riverside.CompilerPlatform.Features.Swagger/KiotaGenerator.cs
deleted file mode 100644
index f4582d6..0000000
--- a/src/features/Riverside.CompilerPlatform.Features.Swagger/KiotaGenerator.cs
+++ /dev/null
@@ -1,156 +0,0 @@
-using Riverside.CompilerPlatform.SourceGenerators;
-using System.Threading;
-using System;
-using System.Linq;
-using System.Collections.Immutable;
-using System.IO;
-using System.Text;
-using Riverside.CompilerPlatform.Extensions;
-using Riverside.CompilerPlatform.Helpers;
-
-namespace Riverside.CompilerPlatform.Features.Swagger;
-
-///
-/// Generates source code from OpenAPI specification files as part of the build process.
-///
-[Generator]
-public partial class KiotaGenerator : IncrementalGenerator
-{
- private const string VersionProperty = "build_property.KiotaGenerator_Version";
- private const string OptionsProperty = "build_property.KiotaGenerator_Options";
- private const string LanguageProperty = "build_property.KiotaGenerator_Language";
- private const string AdditionalPropertiesProperty = "build_property.KiotaGenerator_AdditionalProperties";
-
- ///
- protected override void OnBeforeGeneration(GeneratorContext context, CancellationToken cancellationToken)
- {
- var optionsProvider = context.AnalyzerConfigOptions.GlobalOptions;
-
- // OpenAPI specs
- var specs = context.AdditionalTexts
- .Where(at => at.Path.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase)
- || at.Path.EndsWith(".yml", StringComparison.OrdinalIgnoreCase)
- || at.Path.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
- .ToImmutableArray();
-
- optionsProvider.TryGetValue(VersionProperty, out var version);
- optionsProvider.TryGetValue(OptionsProperty, out var cliOptions);
- optionsProvider.TryGetValue(LanguageProperty, out var language);
- optionsProvider.TryGetValue(AdditionalPropertiesProperty, out var additionalProps);
-
- version ??= string.Empty;
- language ??= "csharp";
-
- var jarPath = EnsureToolInstallation(version, context);
-
- if (string.IsNullOrWhiteSpace(jarPath))
- {
- IncrementalGenerator.CreateDiagnostic(
- "RS0000",
- "JAR not downloaded",
- "An error occured whilst downloading the JAR executable to generate the OpenAPI spec")
- .Report(context);
- }
-
- foreach (var spec in specs)
- {
- try
- {
- var specNamespace = SanitizationHelpers.Sanitize(Path.GetFileNameWithoutExtension(Path.GetFileName(spec.Path)));
-
- var specPath = spec.Path;
- if (!File.Exists(specPath))
- continue;
-
- var specFileName = Path.GetFileNameWithoutExtension(specPath);
-
- var tempOut = Path.Combine(Path.GetTempPath(), "Roslyn", "Advanced Compiler Services for .NET", Guid.NewGuid().ToString("N"));
- Directory.CreateDirectory(tempOut);
-
- if (!string.IsNullOrWhiteSpace(cliOptions))
- {
- argsBuilder.Append(" ");
- argsBuilder.Append(cliOptions);
- }
-
- if (!string.IsNullOrWhiteSpace(additionalProps))
- {
- argsBuilder.Append(" --additional-properties=");
- argsBuilder.Append(SanitizationHelpers.EscapeArg(additionalProps!));
- }
-
- var args = argsBuilder.ToString();
-
- var runResult = ProcessHelpers.RunProcess("java", args, TimeSpan.FromMinutes(2)).GetAwaiter().GetResult();
-
- if (runResult.ExitCode != 0)
- {
- CreateDiagnostic("RS0000", "OpenAPI generator failed", $"OpenAPI generator failed for spec '{spec.Path}' with exit code {runResult.ExitCode}: {runResult.StandardError.ReplaceLineEndings(" ")}").Report(context);
- TryDeleteDirectory(tempOut);
- continue;
- }
-
- var srcDir = Path.Combine(tempOut, "src", specNamespace);
- var csFiles = Directory.EnumerateFiles(srcDir, "*.cs", SearchOption.AllDirectories).ToArray();
- if (csFiles.Length == 0)
- {
- CreateDiagnostic("RS0000", "No C# files generated", $"OpenAPI generator produced no C# files for spec '{spec.Path}'").Report(context);
- TryDeleteDirectory(tempOut);
- continue;
- }
-
- foreach (var cs in csFiles)
- {
- try
- {
- var content = File.ReadAllText(cs, Encoding.UTF8);
- var rel = Path.GetRelativePath(tempOut, cs)
- .Replace(Path.DirectorySeparatorChar, '_')
- .Replace(Path.AltDirectorySeparatorChar, '_');
-
- var hintName = $"{SanitizationHelpers.Sanitize(specFileName)}_{SanitizationHelpers.Sanitize(rel)}";
- AddSource(hintName, content);
- }
- catch (Exception ex)
- {
- CreateDiagnostic("RS0000", "Failed to add generated file", $"Failed to add generated file '{cs}': {ex.Message}").Report(context);
- }
- }
-
- TryDeleteDirectory(tempOut);
-
- AddSource($"{SanitizationHelpers.Sanitize(specFileName)}_AnyOf", AnyOf_Polyfill(specNamespace + ".Model"));
- }
- catch (Exception ex)
- {
- CreateDiagnostic("RS9999", "OpenAPI generator exception", ex.ToString());
- }
- }
- }
-
- private static string? EnsureToolInstallation(string version, GeneratorContext context)
- {
- try
- {
- var baseDir = Path.Combine(Path.GetTempPath(), "Roslyn", "Advanced Compiler Services for .NET", "KiotaGenerator");
- Directory.CreateDirectory(baseDir);
-
- var jarPath = Path.Combine(baseDir, $"openapi-generator-cli-{version}.jar");
- if (File.Exists(jarPath))
- return jarPath;
-
- return jarPath;
- }
- catch (Exception ex)
- {
- CreateDiagnostic("RS0000", $"Failed to download OpenAPI generator JAR", $"Could not download version {version}: {ex.Message}").Report(context);
- return null;
- }
- }
-
- private static void TryDeleteDirectory(string path)
- {
- try { if (Directory.Exists(path)) Directory.Delete(path, true); }
- catch { }
- }
-}
diff --git a/src/features/Riverside.CompilerPlatform.Features.Swagger/Riverside.CompilerPlatform.Features.Swagger.csproj b/src/features/Riverside.CompilerPlatform.Features.Swagger/Riverside.CompilerPlatform.Features.Swagger.csproj
deleted file mode 100644
index f8ca381..0000000
--- a/src/features/Riverside.CompilerPlatform.Features.Swagger/Riverside.CompilerPlatform.Features.Swagger.csproj
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
- netstandard2.0
- CSharp
-
-
-
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers
-
-
-
-
diff --git a/src/roslyn/Riverside.CompilerPlatform.Extensions/Extensions/AnalyzerConfigOptionsExtensions.cs b/src/roslyn/Riverside.CompilerPlatform.Extensions/Extensions/AnalyzerConfigOptionsExtensions.cs
new file mode 100644
index 0000000..858f9d4
--- /dev/null
+++ b/src/roslyn/Riverside.CompilerPlatform.Extensions/Extensions/AnalyzerConfigOptionsExtensions.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Linq;
+
+namespace Riverside.CompilerPlatform.Extensions;
+
+///
+/// Provides extension methods for reading typed values from .
+///
+public static class AnalyzerConfigOptionsExtensions
+{
+ ///
+ /// Returns the string value for , or if the key is absent or whitespace.
+ ///
+ /// The analyser config options to read from.
+ /// The property key, typically prefixed with build_property..
+ /// The trimmed string value, or .
+ public static string? GetString(this AnalyzerConfigOptions options, string key)
+ {
+ options.TryGetValue(key, out var value);
+ return string.IsNullOrWhiteSpace(value) ? null : value;
+ }
+
+ ///
+ /// Returns a nullable for .
+ /// Returns when the key is absent, empty, or not a valid boolean string.
+ ///
+ /// The analyzer config options to read from.
+ /// The property key.
+ /// The parsed boolean, or .
+ public static bool? GetNullableBool(this AnalyzerConfigOptions options, string key)
+ {
+ var value = options.GetString(key);
+ return value is not null && bool.TryParse(value, out var result) ? result : null;
+ }
+
+ ///
+ /// Returns a nullable for , parsed case-insensitively.
+ /// Returns when the key is absent, empty, or does not map to a valid enum member.
+ ///
+ /// The enum type to parse into.
+ /// The analyser config options to read from.
+ /// The property key.
+ /// The parsed enum value, or .
+ public static TEnum? GetNullableEnum(this AnalyzerConfigOptions options, string key)
+ where TEnum : struct, Enum
+ {
+ var value = options.GetString(key);
+ return value is not null && Enum.TryParse(value, ignoreCase: true, out var result) ? result : null;
+ }
+
+ ///
+ /// Returns a [] by splitting 's value on the | character.
+ /// Empty or whitespace-only segments are discarded.
+ /// Returns when the key is absent, empty, or yields no usable segments.
+ ///
+ /// The analyser config options to read from.
+ /// The property key.
+ /// A non-empty trimmed array of segments, or .
+ public static string[]? GetPipeSeparatedArray(this AnalyzerConfigOptions options, string key)
+ {
+ var value = options.GetString(key);
+ if (value is null)
+ return null;
+ var parts = value.Split('|')
+ .Select(p => p.Trim())
+ .Where(p => p.Length > 0)
+ .ToArray();
+ return parts.Length > 0 ? parts : null;
+ }
+
+ ///
+ /// Returns a [] by splitting 's value on | and parsing each segment case-insensitively.
+ /// Segments that do not match a valid enum member are silently skipped.
+ ///
+ /// The enum type to parse each segment into.
+ /// The analyser config options to read from.
+ /// The property key.
+ /// A non-empty array of parsed enum values, or when the is absent, empty, or yields no valid members.
+ public static TEnum[]? GetPipeSeparatedEnumArray(this AnalyzerConfigOptions options, string key)
+ where TEnum : struct, Enum
+ {
+ var raw = options.GetPipeSeparatedArray(key);
+ if (raw is null)
+ return null;
+ var parsed = raw
+ .Select(s => (ok: Enum.TryParse(s, ignoreCase: true, out var v), val: v))
+ .Where(t => t.ok)
+ .Select(t => t.val)
+ .ToArray();
+ return parsed.Length > 0 ? parsed : null;
+ }
+}
diff --git a/src/roslyn/Riverside.CompilerPlatform.Extensions/Helpers/DirectoryHelpers.cs b/src/roslyn/Riverside.CompilerPlatform.Extensions/Helpers/DirectoryHelpers.cs
new file mode 100644
index 0000000..99a0167
--- /dev/null
+++ b/src/roslyn/Riverside.CompilerPlatform.Extensions/Helpers/DirectoryHelpers.cs
@@ -0,0 +1,33 @@
+using System;
+using System.IO;
+
+namespace Riverside.CompilerPlatform.Helpers;
+
+///
+/// Provides utility methods for common directory operations.
+///
+public static class DirectoryHelpers
+{
+ ///
+ /// Deletes and all of its contents recursively, suppressing any exception that occurs.
+ ///
+ /// The directory to delete.
+ public static void TryDelete(string path)
+ {
+ try { if (Directory.Exists(path)) Directory.Delete(path, recursive: true); }
+ catch { }
+ }
+
+ ///
+ /// Creates a uniquely named subdirectory under and returns its full path.
+ /// The subdirectory name is a compact with no formatting characters.
+ ///
+ /// The parent directory. Created if it does not already exist.
+ /// The full path of the newly created temporary directory.
+ public static string CreateTemporary(string basePath)
+ {
+ var path = Path.Combine(basePath, Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(path);
+ return path;
+ }
+}
diff --git a/src/roslyn/Riverside.CompilerPlatform.Extensions/Helpers/NETCoreToolHelpers.cs b/src/roslyn/Riverside.CompilerPlatform.Extensions/Helpers/NETCoreToolHelpers.cs
new file mode 100644
index 0000000..a80df77
--- /dev/null
+++ b/src/roslyn/Riverside.CompilerPlatform.Extensions/Helpers/NETCoreToolHelpers.cs
@@ -0,0 +1,80 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Riverside.CompilerPlatform.Helpers;
+
+///
+/// Provides helpers for installing and locating .NET tools installed to a specific tool-path directory.
+///
+public static class NETCoreToolHelpers
+{
+ ///
+ /// Returns the full path to the tool executable expected in .
+ /// Appends .exe on Windows; uses the bare name on all other platforms.
+ ///
+ /// The directory the tool was installed into via --tool-path.
+ /// The tool executable name (e.g. kiota).
+ /// The full path including the platform-appropriate extension.
+ public static string GetExecutablePath(string toolDirectory, string toolName)
+ => Path.Combine(
+ toolDirectory,
+ Environment.OSVersion.Platform == PlatformID.Win32NT ? toolName + ".exe" : toolName);
+
+ ///
+ /// Ensures the specified .NET tool is available in .
+ ///
+ ///
+ ///
+ /// - If the executable already exists and no specific is requested, the tool is reused immediately.
+ /// - Otherwise dotnet tool install is attempted. If it fails because the tool is already installed, dotnet tool update is tried instead.
+ /// - Installation succeeds when the executable is present after the above steps.
+ ///
+ ///
+ /// The NuGet package ID of the tool (e.g. Riverside.JsonBinder.Console).
+ /// The directory to install the tool into, passed to --tool-path.
+ ///
+ /// A specific version to pin. Pass to install or keep the latest.
+ ///
+ /// Maximum wait time per install or update process. Defaults to 5 minutes.
+ ///
+ /// The executable/command name to look for in (e.g. jsonbinder).
+ /// If , is used as the command name.
+ ///
+ ///
+ /// A tuple where Success is when the executable is available, and Error carries the captured stderr when installation fails.
+ ///
+ public static async Task<(bool Success, string? Error)> EnsureToolAsync(
+ string packageId,
+ string toolDirectory,
+ string? version = null,
+ TimeSpan? timeout = null,
+ string? commandName = null)
+ {
+ var exeName = string.IsNullOrWhiteSpace(commandName) ? packageId : commandName;
+ var exe = GetExecutablePath(toolDirectory, exeName!);
+ var effectiveTimeout = timeout ?? TimeSpan.FromMinutes(5);
+
+ Directory.CreateDirectory(toolDirectory);
+
+ if (File.Exists(exe) && string.IsNullOrWhiteSpace(version))
+ return (true, null);
+
+ var toolPathArg = $"--tool-path \"{toolDirectory}\"";
+ var versionArg = string.IsNullOrWhiteSpace(version) ? string.Empty : $" --version {version}";
+
+ var installResult = await ProcessHelpers.RunNETCoreCliAsync(
+ $"tool install {packageId} {toolPathArg}{versionArg}", effectiveTimeout);
+
+ if (installResult.ExitCode == 0)
+ return (true, null);
+
+ // install exits non-zero when the tool is already present; attempt an update instead
+ var updateResult = await ProcessHelpers.RunNETCoreCliAsync(
+ $"tool update {packageId} {toolPathArg}{versionArg}", effectiveTimeout);
+
+ return File.Exists(exe)
+ ? (true, null)
+ : (false, updateResult.StandardError);
+ }
+}
diff --git a/src/roslyn/Riverside.CompilerPlatform.Extensions/Helpers/ProcessHelpers.cs b/src/roslyn/Riverside.CompilerPlatform.Extensions/Helpers/ProcessHelpers.cs
index 4132700..9bb2dd5 100644
--- a/src/roslyn/Riverside.CompilerPlatform.Extensions/Helpers/ProcessHelpers.cs
+++ b/src/roslyn/Riverside.CompilerPlatform.Extensions/Helpers/ProcessHelpers.cs
@@ -49,8 +49,7 @@ public class ProcessOutput(int code, string stdout, string stderr)
/// A tuple containing the process exit code, the captured standard output, and the captured standard error.
/// If the process times out, the exit code is -1 and the standard error includes a timeout message.
///
- public static async Task
- RunProcess(string fileName, string arguments, TimeSpan timeout)
+ public static async Task RunProcess(string fileName, string arguments, TimeSpan timeout)
{
var psi = new ProcessStartInfo
{
@@ -82,4 +81,14 @@ public static async Task
return new(proc.ExitCode, outputSb.ToString(), errorSb.ToString());
}
+
+ ///
+ /// Runs a dotnet command asynchronously with the specified arguments and timeout,
+ /// capturing its exit code, standard output, and standard error.
+ ///
+ /// The arguments to pass after dotnet (e.g. tool install Riverside.JsonBinder.Console ...).
+ /// The maximum duration to wait before forcibly terminating the process.
+ /// A containing the exit code and captured streams.
+ public static Task RunNETCoreCliAsync(string arguments, TimeSpan timeout)
+ => RunProcess("dotnet", arguments, timeout);
}
diff --git a/src/roslyn/Riverside.CompilerPlatform.Extensions/Helpers/SanitizationHelpers.cs b/src/roslyn/Riverside.CompilerPlatform.Extensions/Helpers/SanitizationHelpers.cs
index 74aea88..a258a42 100644
--- a/src/roslyn/Riverside.CompilerPlatform.Extensions/Helpers/SanitizationHelpers.cs
+++ b/src/roslyn/Riverside.CompilerPlatform.Extensions/Helpers/SanitizationHelpers.cs
@@ -1,5 +1,4 @@
-using Riverside.Extensions.Accountability;
-using System;
+using System;
using System.IO;
using System.Linq;
using System.Text;
@@ -36,7 +35,6 @@ public static string Sanitize(string s)
/// The argument to escape.
/// A string containing the escaped argument, suitable for use in a command-line context.
/// Thrown if is null.
- [NotMyCode] // from the internet
public static string EscapeArg(string arg)
{
if (arg == null)
diff --git a/src/roslyn/Riverside.CompilerPlatform.SourceGenerators/IncrementalGenerator.cs b/src/roslyn/Riverside.CompilerPlatform.SourceGenerators/IncrementalGenerator.cs
index 0a46157..8d9f615 100644
--- a/src/roslyn/Riverside.CompilerPlatform.SourceGenerators/IncrementalGenerator.cs
+++ b/src/roslyn/Riverside.CompilerPlatform.SourceGenerators/IncrementalGenerator.cs
@@ -1,4 +1,5 @@
-using System;
+using Riverside.CompilerPlatform.Extensions;
+using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@@ -249,12 +250,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
if (!SuppressDiagnostics)
{
// Report exceptions that occur during generation
- sourceProductionContext.ReportDiagnostic(
- CreateDiagnostic(
- $"RS9999",
- "Source Generation Error",
- $"An error occurred during source generation: {ex.Message}",
- DiagnosticSeverity.Error));
+ CreateDiagnostic(
+ $"RS9999",
+ "Source Generation Error",
+ $"An error occurred during source generation: {ex.Message}",
+ DiagnosticSeverity.Error).Report(Context);
}
}
});
diff --git a/tests/Riverside.CompilerPlatform.Features.Tests/api-1.json b/tests/Riverside.CompilerPlatform.Features.Tests/Lapse.json
similarity index 100%
rename from tests/Riverside.CompilerPlatform.Features.Tests/api-1.json
rename to tests/Riverside.CompilerPlatform.Features.Tests/Lapse.json
diff --git a/tests/Riverside.CompilerPlatform.Features.Tests/Riverside.CompilerPlatform.Features.Tests.csproj b/tests/Riverside.CompilerPlatform.Features.Tests/Riverside.CompilerPlatform.Features.Tests.csproj
index dba8912..b186e42 100644
--- a/tests/Riverside.CompilerPlatform.Features.Tests/Riverside.CompilerPlatform.Features.Tests.csproj
+++ b/tests/Riverside.CompilerPlatform.Features.Tests/Riverside.CompilerPlatform.Features.Tests.csproj
@@ -3,25 +3,28 @@
VisualBasic;CSharp
netstandard2.0
+ 14.0
-
-
-
+
-
-
-
-
-
-
+
+
+
+
-
+
+
+
+
+
+
+