Skip to content

Commit 96574ec

Browse files
feat: enable null reference type support (#2146)
* feat: enable NRT * fix: dereference of a possible null reference * chore: code cleanup * chore: convert to conditional expression * Update src/Microsoft.OpenApi/Models/OpenApiOperation.cs Co-authored-by: Vincent Biret <[email protected]> * Update src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs Co-authored-by: Vincent Biret <[email protected]> * chore: PR feedback * fix: resolve merge conflict errors * chore: remove deprecated code and tests; make param required * chore: code refactor and cleanup * chore: update public API * chore: address PR comments * fix: remove nullable Json node params * chore: use conditional compilation to make reference a required field * refactor: apply nullable to new changes * chore: bad merge Signed-off-by: Vincent Biret <[email protected]> * chore: simplifies filtering condition * Update src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs Co-authored-by: Vincent Biret <[email protected]> * chore: address more PR feedback * chore: add check for both null and empty strings * chore: revert to include check for empty strings * chore: cleanup * chore: clean up public API * Update src/Microsoft.OpenApi/Reader/Services/OpenApiRemoteReferenceCollector.cs Co-authored-by: Vincent Biret <[email protected]> * fix: resolve PR feedback * chore: add defensive programming * chore: resolve merge conflicts * chore: more refactoring * Update src/Microsoft.OpenApi/Reader/V2/OpenApiOperationDeserializer.cs Co-authored-by: Vincent Biret <[email protected]> * Update src/Microsoft.OpenApi/Reader/V2/OpenApiPathItemDeserializer.cs Co-authored-by: Vincent Biret <[email protected]> * Update src/Microsoft.OpenApi/Reader/V31/OpenApiServerVariableDeserializer.cs Co-authored-by: Vincent Biret <[email protected]> * Update src/Microsoft.OpenApi/Reader/V31/OpenApiSecurityRequirementDeserializer.cs Co-authored-by: Vincent Biret <[email protected]> * Update src/Microsoft.OpenApi/Reader/V2/OpenApiSecurityRequirementDeserializer.cs Co-authored-by: Vincent Biret <[email protected]> * Update src/Microsoft.OpenApi/Reader/V3/OpenApiServerVariableDeserializer.cs Co-authored-by: Vincent Biret <[email protected]> * Update src/Microsoft.OpenApi/Reader/V3/OpenApiSecurityRequirementDeserializer.cs Co-authored-by: Vincent Biret <[email protected]> * chore: another round of refactoring * chore: clean up nullability of params * fix: compiler errors * fix: remove redundant cast and update public API * chore: fix merge conflict issues * chore: apply copilot suggestion * Update src/Microsoft.OpenApi/Services/OpenApiWalker.cs --------- Signed-off-by: Vincent Biret <[email protected]> Co-authored-by: Vincent Biret <[email protected]>
1 parent 4dd593c commit 96574ec

File tree

194 files changed

+3113
-2805
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

194 files changed

+3113
-2805
lines changed

src/Microsoft.OpenApi.Hidi/Formatters/PowerShellFormatter.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public override void Visit(IOpenApiSchema schema)
5454

5555
public override void Visit(IOpenApiPathItem pathItem)
5656
{
57-
if (pathItem.Operations.TryGetValue(HttpMethod.Put, out var value) &&
57+
if (pathItem.Operations is not null && pathItem.Operations.TryGetValue(HttpMethod.Put, out var value) &&
5858
value.OperationId != null)
5959
{
6060
var operationId = value.OperationId;
@@ -150,7 +150,7 @@ private static string RemoveKeyTypeSegment(string operationId, IList<IOpenApiPar
150150
var segments = operationId.SplitByChar('.');
151151
foreach (var parameter in parameters)
152152
{
153-
var keyTypeExtension = parameter.Extensions.GetExtension("x-ms-docs-key-type");
153+
var keyTypeExtension = parameter.Extensions?.GetExtension("x-ms-docs-key-type");
154154
if (keyTypeExtension != null && operationId.Contains(keyTypeExtension, StringComparison.OrdinalIgnoreCase))
155155
{
156156
segments.Remove(keyTypeExtension);
@@ -179,15 +179,20 @@ private static void ResolveFunctionParameters(IList<IOpenApiParameter> parameter
179179

180180
private void AddAdditionalPropertiesToSchema(IOpenApiSchema schema)
181181
{
182-
if (schema is OpenApiSchema openApiSchema && !_schemaLoop.Contains(schema) && schema.Type.Equals(JsonSchemaType.Object))
182+
if (schema is OpenApiSchema openApiSchema
183+
&& !_schemaLoop.Contains(schema)
184+
&& schema.Type.Equals(JsonSchemaType.Object))
183185
{
184186
openApiSchema.AdditionalProperties = new OpenApiSchema() { Type = JsonSchemaType.Object };
185187

186188
/* Because 'additionalProperties' are now being walked,
187189
* we need a way to keep track of visited schemas to avoid
188190
* endlessly creating and walking them in an infinite recursion.
189191
*/
190-
_schemaLoop.Push(schema.AdditionalProperties);
192+
if (schema.AdditionalProperties is not null)
193+
{
194+
_schemaLoop.Push(schema.AdditionalProperties);
195+
}
191196
}
192197
}
193198

src/Microsoft.OpenApi.Hidi/OpenApiService.cs

Lines changed: 80 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public static async Task TransformOpenApiDocumentAsync(HidiOptions options, ILog
9494
// Load OpenAPI document
9595
var document = await GetOpenApiAsync(options, openApiFormat.GetDisplayName(), logger, options.MetadataVersion, cancellationToken).ConfigureAwait(false);
9696

97-
if (options.FilterOptions != null)
97+
if (options.FilterOptions != null && document is not null)
9898
{
9999
document = ApplyFilters(options, logger, apiDependency, postmanCollection, document);
100100
}
@@ -107,7 +107,11 @@ public static async Task TransformOpenApiDocumentAsync(HidiOptions options, ILog
107107
var walker = new OpenApiWalker(powerShellFormatter);
108108
walker.Walk(document);
109109
}
110-
await WriteOpenApiAsync(options, openApiFormat, openApiVersion, document, logger, cancellationToken).ConfigureAwait(false);
110+
if (document is not null)
111+
{
112+
// Write the OpenAPI document to the output file
113+
await WriteOpenApiAsync(options, openApiFormat, openApiVersion, document, logger, cancellationToken).ConfigureAwait(false);
114+
}
111115
}
112116
catch (TaskCanceledException)
113117
{
@@ -172,7 +176,7 @@ private static OpenApiDocument ApplyFilters(HidiOptions options, ILogger logger,
172176
options.FilterOptions.FilterByTags,
173177
requestUrls,
174178
document,
175-
logger);
179+
logger);
176180
if (predicate != null)
177181
{
178182
var stopwatch = new Stopwatch();
@@ -210,6 +214,7 @@ private static async Task WriteOpenApiAsync(HidiOptions options, OpenApiFormat o
210214

211215
var stopwatch = new Stopwatch();
212216
stopwatch.Start();
217+
213218
await document.SerializeAsync(writer, openApiVersion, cancellationToken).ConfigureAwait(false);
214219
stopwatch.Stop();
215220

@@ -219,9 +224,9 @@ private static async Task WriteOpenApiAsync(HidiOptions options, OpenApiFormat o
219224
}
220225

221226
// Get OpenAPI document either from OpenAPI or CSDL
222-
private static async Task<OpenApiDocument> GetOpenApiAsync(HidiOptions options, string format, ILogger logger, string? metadataVersion = null, CancellationToken cancellationToken = default)
227+
private static async Task<OpenApiDocument?> GetOpenApiAsync(HidiOptions options, string format, ILogger logger, string? metadataVersion = null, CancellationToken cancellationToken = default)
223228
{
224-
OpenApiDocument document;
229+
OpenApiDocument? document;
225230
Stream stream;
226231

227232
if (!string.IsNullOrEmpty(options.Csdl))
@@ -242,7 +247,7 @@ private static async Task<OpenApiDocument> GetOpenApiAsync(HidiOptions options,
242247

243248
document = await ConvertCsdlToOpenApiAsync(filteredStream ?? stream, format, metadataVersion, options.SettingsConfig, cancellationToken).ConfigureAwait(false);
244249
stopwatch.Stop();
245-
logger.LogTrace("{Timestamp}ms: Generated OpenAPI with {Paths} paths.", stopwatch.ElapsedMilliseconds, document.Paths.Count);
250+
logger.LogTrace("{Timestamp}ms: Generated OpenAPI with {Paths} paths.", stopwatch.ElapsedMilliseconds, document?.Paths.Count);
246251
}
247252
}
248253
else if (!string.IsNullOrEmpty(options.OpenApi))
@@ -370,7 +375,7 @@ private static MemoryStream ApplyFilterToCsdl(Stream csdlStream, string entitySe
370375

371376
if (result is null) return null;
372377

373-
return result.Diagnostic.Errors.Count == 0;
378+
return result.Diagnostic?.Errors.Count == 0;
374379
}
375380

376381
private static async Task<ReadResult> ParseOpenApiAsync(string openApiFile, bool inlineExternal, ILogger logger, Stream stream, CancellationToken cancellationToken = default)
@@ -407,7 +412,7 @@ private static async Task<ReadResult> ParseOpenApiAsync(string openApiFile, bool
407412
/// </summary>
408413
/// <param name="csdl">The CSDL stream.</param>
409414
/// <returns>An OpenAPI document.</returns>
410-
public static async Task<OpenApiDocument> ConvertCsdlToOpenApiAsync(Stream csdl, string format, string? metadataVersion = null, IConfiguration? settings = null, CancellationToken token = default)
415+
public static async Task<OpenApiDocument?> ConvertCsdlToOpenApiAsync(Stream csdl, string format, string? metadataVersion = null, IConfiguration? settings = null, CancellationToken token = default)
411416
{
412417
using var reader = new StreamReader(csdl);
413418
var csdlText = await reader.ReadToEndAsync(token).ConfigureAwait(false);
@@ -425,7 +430,7 @@ public static async Task<OpenApiDocument> ConvertCsdlToOpenApiAsync(Stream csdl,
425430
/// </summary>
426431
/// <param name="document"> The converted OpenApiDocument.</param>
427432
/// <returns> A valid OpenApiDocument instance.</returns>
428-
public static OpenApiDocument FixReferences(OpenApiDocument document, string format)
433+
public static OpenApiDocument? FixReferences(OpenApiDocument document, string format)
429434
{
430435
// This method is only needed because the output of ConvertToOpenApi isn't quite a valid OpenApiDocument instance.
431436
// So we write it out, and read it back in again to fix it up.
@@ -584,52 +589,54 @@ private static string GetInputPathExtension(string? openapi = null, string? csdl
584589

585590
var openApiFormat = options.OpenApiFormat ?? (!string.IsNullOrEmpty(options.OpenApi) ? GetOpenApiFormat(options.OpenApi, logger) : OpenApiFormat.Yaml);
586591
var document = await GetOpenApiAsync(options, openApiFormat.GetDisplayName(), logger, null, cancellationToken).ConfigureAwait(false);
587-
588-
using (logger.BeginScope("Creating diagram"))
592+
if (document is not null)
589593
{
590-
// If output is null, create a HTML file in the user's temporary directory
591-
var sourceUrl = (string.IsNullOrEmpty(options.OpenApi), string.IsNullOrEmpty(options.Csdl)) switch {
592-
(false, _) => options.OpenApi!,
593-
(_, false) => options.Csdl!,
594-
_ => throw new InvalidOperationException("No input file path or URL provided")
595-
};
596-
if (options.Output == null)
594+
using (logger.BeginScope("Creating diagram"))
597595
{
598-
var tempPath = Path.GetTempPath() + "/hidi/";
599-
if (!File.Exists(tempPath))
596+
// If output is null, create a HTML file in the user's temporary directory
597+
var sourceUrl = (string.IsNullOrEmpty(options.OpenApi), string.IsNullOrEmpty(options.Csdl)) switch
600598
{
601-
Directory.CreateDirectory(tempPath);
602-
}
603-
604-
var fileName = Path.GetRandomFileName();
605-
606-
var output = new FileInfo(Path.Combine(tempPath, fileName + ".html"));
607-
using (var file = new FileStream(output.FullName, FileMode.Create))
599+
(false, _) => options.OpenApi!,
600+
(_, false) => options.Csdl!,
601+
_ => throw new InvalidOperationException("No input file path or URL provided")
602+
};
603+
if (options.Output == null)
608604
{
609-
using var writer = new StreamWriter(file);
610-
WriteTreeDocumentAsHtml(sourceUrl, document, writer);
605+
var tempPath = Path.GetTempPath() + "/hidi/";
606+
if (!File.Exists(tempPath))
607+
{
608+
Directory.CreateDirectory(tempPath);
609+
}
610+
611+
var fileName = Path.GetRandomFileName();
612+
613+
var output = new FileInfo(Path.Combine(tempPath, fileName + ".html"));
614+
using (var file = new FileStream(output.FullName, FileMode.Create))
615+
{
616+
using var writer = new StreamWriter(file);
617+
WriteTreeDocumentAsHtml(sourceUrl, document, writer);
618+
}
619+
logger.LogTrace("Created Html document with diagram ");
620+
621+
// Launch a browser to display the output html file
622+
using var process = new Process();
623+
process.StartInfo.FileName = output.FullName;
624+
process.StartInfo.UseShellExecute = true;
625+
process.Start();
626+
627+
return output.FullName;
611628
}
612-
logger.LogTrace("Created Html document with diagram ");
613-
614-
// Launch a browser to display the output html file
615-
using var process = new Process();
616-
process.StartInfo.FileName = output.FullName;
617-
process.StartInfo.UseShellExecute = true;
618-
process.Start();
619-
620-
return output.FullName;
621-
}
622-
else // Write diagram as Markdown document to output file
623-
{
624-
using (var file = new FileStream(options.Output.FullName, FileMode.Create))
629+
else // Write diagram as Markdown document to output file
625630
{
631+
using var file = new FileStream(options.Output.FullName, FileMode.Create);
626632
using var writer = new StreamWriter(file);
627633
WriteTreeDocumentAsMarkdown(sourceUrl, document, writer);
634+
635+
logger.LogTrace("Created markdown document with diagram ");
636+
return options.Output.FullName;
628637
}
629-
logger.LogTrace("Created markdown document with diagram ");
630-
return options.Output.FullName;
631638
}
632-
}
639+
}
633640
}
634641
catch (TaskCanceledException)
635642
{
@@ -645,7 +652,7 @@ private static string GetInputPathExtension(string? openapi = null, string? csdl
645652
private static void LogErrors(ILogger logger, ReadResult result)
646653
{
647654
var context = result.Diagnostic;
648-
if (context.Errors.Count != 0)
655+
if (context is not null && context.Errors.Count != 0)
649656
{
650657
using (logger.BeginScope("Detected errors"))
651658
{
@@ -697,7 +704,7 @@ internal static void WriteTreeDocumentAsHtml(string sourceUrl, OpenApiDocument d
697704
</style>
698705
<body>
699706
""");
700-
writer.WriteLine("<h1>" + document.Info.Title + "</h1>");
707+
writer.WriteLine("<h1>" + document?.Info.Title + "</h1>");
701708
writer.WriteLine();
702709
writer.WriteLine($"<h3> API Description: <a href='{sourceUrl}'>{sourceUrl}</a></h3>");
703710

@@ -751,7 +758,7 @@ internal static async Task PluginManifestAsync(HidiOptions options, ILogger logg
751758

752759
cancellationToken.ThrowIfCancellationRequested();
753760

754-
if (options.FilterOptions != null)
761+
if (options.FilterOptions != null && document is not null)
755762
{
756763
document = ApplyFilters(options, logger, apiDependency, null, document);
757764
}
@@ -765,24 +772,31 @@ internal static async Task PluginManifestAsync(HidiOptions options, ILogger logg
765772
// Write OpenAPI to Output folder
766773
options.Output = new(Path.Combine(options.OutputFolder, "openapi.json"));
767774
options.TerseOutput = true;
768-
await WriteOpenApiAsync(options, OpenApiFormat.Json, OpenApiSpecVersion.OpenApi3_1, document, logger, cancellationToken).ConfigureAwait(false);
769-
770-
// Create OpenAIPluginManifest from ApiDependency and OpenAPI document
771-
var manifest = new OpenAIPluginManifest(document.Info?.Title ?? "Title", document.Info?.Title ?? "Title", "https://go.microsoft.com/fwlink/?LinkID=288890", document.Info?.Contact?.Email ?? "[email protected]", document.Info?.License?.Url.ToString() ?? "https://placeholderlicenseurl.com")
772-
{
773-
DescriptionForHuman = document.Info?.Description ?? "Description placeholder",
774-
Api = new("openapi", "./openapi.json"),
775-
Auth = new ManifestNoAuth(),
776-
};
777-
manifest.NameForModel = manifest.NameForHuman;
778-
manifest.DescriptionForModel = manifest.DescriptionForHuman;
779-
780-
// Write OpenAIPluginManifest to Output folder
781-
var manifestFile = new FileInfo(Path.Combine(options.OutputFolder, "ai-plugin.json"));
782-
using var file = new FileStream(manifestFile.FullName, FileMode.Create);
783-
using var jsonWriter = new Utf8JsonWriter(file, new() { Indented = true });
784-
manifest.Write(jsonWriter);
785-
await jsonWriter.FlushAsync(cancellationToken).ConfigureAwait(false);
775+
if (document is not null)
776+
{
777+
await WriteOpenApiAsync(options, OpenApiFormat.Json, OpenApiSpecVersion.OpenApi3_1, document, logger, cancellationToken).ConfigureAwait(false);
778+
779+
// Create OpenAIPluginManifest from ApiDependency and OpenAPI document
780+
var manifest = new OpenAIPluginManifest(document.Info.Title ?? "Title",
781+
document.Info.Title ?? "Title",
782+
"https://go.microsoft.com/fwlink/?LinkID=288890",
783+
document.Info?.Contact?.Email ?? "[email protected]",
784+
document.Info?.License?.Url?.ToString() ?? "https://placeholderlicenseurl.com")
785+
{
786+
DescriptionForHuman = document.Info?.Description ?? "Description placeholder",
787+
Api = new("openapi", "./openapi.json"),
788+
Auth = new ManifestNoAuth(),
789+
};
790+
manifest.NameForModel = manifest.NameForHuman;
791+
manifest.DescriptionForModel = manifest.DescriptionForHuman;
792+
793+
// Write OpenAIPluginManifest to Output folder
794+
var manifestFile = new FileInfo(Path.Combine(options.OutputFolder, "ai-plugin.json"));
795+
using var file = new FileStream(manifestFile.FullName, FileMode.Create);
796+
using var jsonWriter = new Utf8JsonWriter(file, new() { Indented = true });
797+
manifest.Write(jsonWriter);
798+
await jsonWriter.FlushAsync(cancellationToken).ConfigureAwait(false);
799+
}
786800
}
787801
}
788802
}

src/Microsoft.OpenApi.Readers/Microsoft.OpenApi.Readers.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<!-- https://github.com/dotnet/sourcelink/blob/main/docs/README.md#embeduntrackedsources -->
1212
<EmbedUntrackedSources>true</EmbedUntrackedSources>
1313
<NoWarn>NU5048</NoWarn>
14+
<Nullable>enable</Nullable>
1415
<PackageReadmeFile>README.md</PackageReadmeFile>
1516
</PropertyGroup>
1617

src/Microsoft.OpenApi.Readers/OpenApiYamlReader.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,11 @@ public static ReadResult Read(JsonNode jsonNode, OpenApiReaderSettings settings)
8484
}
8585

8686
/// <inheritdoc/>
87-
public T ReadFragment<T>(MemoryStream input,
87+
public T? ReadFragment<T>(MemoryStream input,
8888
OpenApiSpecVersion version,
8989
OpenApiDocument openApiDocument,
9090
out OpenApiDiagnostic diagnostic,
91-
OpenApiReaderSettings settings = null) where T : IOpenApiElement
91+
OpenApiReaderSettings? settings = null) where T : IOpenApiElement
9292
{
9393
if (input is null) throw new ArgumentNullException(nameof(input));
9494
JsonNode jsonNode;
@@ -110,7 +110,7 @@ public T ReadFragment<T>(MemoryStream input,
110110
}
111111

112112
/// <inheritdoc/>
113-
public static T ReadFragment<T>(JsonNode input, OpenApiSpecVersion version, OpenApiDocument openApiDocument, out OpenApiDiagnostic diagnostic, OpenApiReaderSettings settings = null) where T : IOpenApiElement
113+
public static T? ReadFragment<T>(JsonNode input, OpenApiSpecVersion version, OpenApiDocument openApiDocument, out OpenApiDiagnostic diagnostic, OpenApiReaderSettings? settings = null) where T : IOpenApiElement
114114
{
115115
return _jsonReader.ReadFragment<T>(input, version, openApiDocument, out diagnostic, settings);
116116
}

src/Microsoft.OpenApi/Exceptions/OpenApiException.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public OpenApiException(string message)
3333
/// </summary>
3434
/// <param name="message">The plain text error message for this exception.</param>
3535
/// <param name="innerException">The inner exception that is the cause of this exception to be thrown.</param>
36-
public OpenApiException(string message, Exception innerException)
36+
public OpenApiException(string message, Exception? innerException)
3737
: base(message, innerException)
3838
{
3939
}
@@ -46,6 +46,6 @@ public OpenApiException(string message, Exception innerException)
4646
/// a text/plain pointer as defined in https://tools.ietf.org/html/rfc5147
4747
/// Currently only line= is provided because using char= causes tests to break due to CR/LF and LF differences
4848
/// </summary>
49-
public string Pointer { get; set; }
49+
public string? Pointer { get; set; }
5050
}
5151
}

0 commit comments

Comments
 (0)