chore: Fix null reference complaints under .NET 8.0

Signed-off-by: Jon Skeet <jonskeet@google.com>
This commit is contained in:
Jon Skeet 2023-11-15 10:25:16 +00:00 committed by Jon Skeet
parent 623e51f387
commit 0a80c56826
9 changed files with 51 additions and 31 deletions

View File

@ -1,4 +1,4 @@
// Copyright (c) Cloud Native Foundation. // Copyright (c) Cloud Native Foundation.
// Licensed under the Apache 2.0 license. // Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information. // See LICENSE file in the project root for full license information.
@ -8,6 +8,7 @@ using Amqp.Types;
using CloudNative.CloudEvents.Core; using CloudNative.CloudEvents.Core;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Net.Mime; using System.Net.Mime;
@ -145,7 +146,7 @@ namespace CloudNative.CloudEvents.Amqp
} }
} }
private static bool HasCloudEventsContentType(Message message, out string? contentType) private static bool HasCloudEventsContentType(Message message, [NotNullWhen(true)] out string? contentType)
{ {
contentType = message.Properties.ContentType?.ToString(); contentType = message.Properties.ContentType?.ToString();
return MimeUtilities.IsCloudEventsContentType(contentType); return MimeUtilities.IsCloudEventsContentType(contentType);
@ -249,4 +250,4 @@ namespace CloudNative.CloudEvents.Amqp
return applicationProperties; return applicationProperties;
} }
} }
} }

View File

@ -12,6 +12,8 @@
<PackageReference Include="AMQPNetLite" Version="2.4.2" /> <PackageReference Include="AMQPNetLite" Version="2.4.2" />
<PackageReference Include="AMQPNetLite.Serialization" Version="2.4.2" /> <PackageReference Include="AMQPNetLite.Serialization" Version="2.4.2" />
<ProjectReference Include="..\CloudNative.CloudEvents\CloudNative.CloudEvents.csproj" /> <ProjectReference Include="..\CloudNative.CloudEvents\CloudNative.CloudEvents.csproj" />
<!-- Source-only package with nullable reference annotations. -->
<PackageReference Include="Nullable" Version="1.3.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -185,9 +185,9 @@ namespace CloudNative.CloudEvents.Avro
// will fail and that's okay since the type is useless without the proper schema. // will fail and that's okay since the type is useless without the proper schema.
using var sr = new StreamReader(typeof(AvroEventFormatter) using var sr = new StreamReader(typeof(AvroEventFormatter)
.Assembly .Assembly
.GetManifestResourceStream("CloudNative.CloudEvents.Avro.AvroSchema.json")); .GetManifestResourceStream("CloudNative.CloudEvents.Avro.AvroSchema.json")!);
return (RecordSchema) Schema.Parse(sr.ReadToEnd()); return (RecordSchema) Schema.Parse(sr.ReadToEnd());
} }
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) Cloud Native Foundation. // Copyright (c) Cloud Native Foundation.
// Licensed under the Apache 2.0 license. // Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information. // See LICENSE file in the project root for full license information.
@ -345,7 +345,7 @@ namespace CloudNative.CloudEvents.NewtonsoftJson
{ {
throw new ArgumentException($"Structured mode property '{DataBase64PropertyName}' must be a string, when present."); throw new ArgumentException($"Structured mode property '{DataBase64PropertyName}' must be a string, when present.");
} }
cloudEvent.Data = Convert.FromBase64String((string?) dataBase64Token); cloudEvent.Data = Convert.FromBase64String((string) dataBase64Token!);
} }
/// <summary> /// <summary>
@ -498,7 +498,7 @@ namespace CloudNative.CloudEvents.NewtonsoftJson
/// </summary> /// </summary>
/// <param name="data">The CloudEvent to infer the data content from. Must not be null.</param> /// <param name="data">The CloudEvent to infer the data content from. Must not be null.</param>
/// <returns>The inferred data content type, or null if no inference is performed.</returns> /// <returns>The inferred data content type, or null if no inference is performed.</returns>
protected override string? InferDataContentType(object data) => data is byte[]? null : JsonMediaType; protected override string? InferDataContentType(object data) => data is byte[] ? null : JsonMediaType;
/// <summary> /// <summary>
/// Encodes structured mode data within a CloudEvent, writing it to the specified <see cref="JsonWriter"/>. /// Encodes structured mode data within a CloudEvent, writing it to the specified <see cref="JsonWriter"/>.
@ -524,7 +524,14 @@ namespace CloudNative.CloudEvents.NewtonsoftJson
} }
else else
{ {
ContentType dataContentType = new ContentType(GetOrInferDataContentType(cloudEvent)); string? dataContentTypeText = GetOrInferDataContentType(cloudEvent);
// This would only happen in a derived class which overrides GetOrInferDataContentType further...
// This class infers application/json for anything other than byte arrays.
if (dataContentTypeText is null)
{
throw new ArgumentException("Data content type cannot be inferred");
}
ContentType dataContentType = new ContentType(dataContentTypeText);
if (IsJsonMediaType(dataContentType.MediaType)) if (IsJsonMediaType(dataContentType.MediaType))
{ {
writer.WritePropertyName(DataPropertyName); writer.WritePropertyName(DataPropertyName);
@ -710,4 +717,4 @@ namespace CloudNative.CloudEvents.NewtonsoftJson
protected override void DecodeStructuredModeDataBase64Property(JToken dataBase64Token, CloudEvent cloudEvent) => protected override void DecodeStructuredModeDataBase64Property(JToken dataBase64Token, CloudEvent cloudEvent) =>
throw new ArgumentException($"Data unexpectedly represented using '{DataBase64PropertyName}' within structured mode CloudEvent."); throw new ArgumentException($"Data unexpectedly represented using '{DataBase64PropertyName}' within structured mode CloudEvent.");
} }
} }

View File

@ -56,7 +56,7 @@ namespace CloudNative.CloudEvents
throw new ArgumentException($"The {nameof(CloudEventFormatterAttribute)} on type {targetType} has no converter type specified.", nameof(targetType)); throw new ArgumentException($"The {nameof(CloudEventFormatterAttribute)} on type {targetType} has no converter type specified.", nameof(targetType));
} }
object instance; object? instance;
try try
{ {
instance = Activator.CreateInstance(formatterType); instance = Activator.CreateInstance(formatterType);

View File

@ -65,7 +65,7 @@ namespace CloudNative.CloudEvents.Core
public static MemoryStream AsStream(ReadOnlyMemory<byte> memory) public static MemoryStream AsStream(ReadOnlyMemory<byte> memory)
{ {
var segment = GetArraySegment(memory); var segment = GetArraySegment(memory);
return new MemoryStream(segment.Array, segment.Offset, segment.Count, false); return new MemoryStream(segment.Array!, segment.Offset, segment.Count, false);
} }
/// <summary> /// <summary>
@ -79,7 +79,7 @@ namespace CloudNative.CloudEvents.Core
// TODO: If we introduce an additional netstandard2.1 target, we can use encoding.GetString(memory.Span) // TODO: If we introduce an additional netstandard2.1 target, we can use encoding.GetString(memory.Span)
var segment = GetArraySegment(memory); var segment = GetArraySegment(memory);
return encoding.GetString(segment.Array, segment.Offset, segment.Count); return encoding.GetString(segment.Array!, segment.Offset, segment.Count);
} }
/// <summary> /// <summary>
@ -92,7 +92,7 @@ namespace CloudNative.CloudEvents.Core
{ {
Validation.CheckNotNull(destination, nameof(destination)); Validation.CheckNotNull(destination, nameof(destination));
var segment = GetArraySegment(source); var segment = GetArraySegment(source);
await destination.WriteAsync(segment.Array, segment.Offset, segment.Count).ConfigureAwait(false); await destination.WriteAsync(segment.Array!, segment.Offset, segment.Count).ConfigureAwait(false);
} }
/// <summary> /// <summary>
@ -108,13 +108,14 @@ namespace CloudNative.CloudEvents.Core
var segment = GetArraySegment(memory); var segment = GetArraySegment(memory);
// We probably don't actually need to check the offset: if the count is the same as the length, // We probably don't actually need to check the offset: if the count is the same as the length,
// I can't see how the offset can be non-zero. But it doesn't *hurt* as a check. // I can't see how the offset can be non-zero. But it doesn't *hurt* as a check.
return segment.Offset == 0 && segment.Count == segment.Array.Length return segment.Offset == 0 && segment.Count == segment.Array!.Length
? segment.Array ? segment.Array
: memory.ToArray(); : memory.ToArray();
} }
// Note: when this returns, the Array property of the returned segment is guaranteed not to be null.
private static ArraySegment<byte> GetArraySegment(ReadOnlyMemory<byte> memory) => private static ArraySegment<byte> GetArraySegment(ReadOnlyMemory<byte> memory) =>
MemoryMarshal.TryGetArray(memory, out var segment) MemoryMarshal.TryGetArray(memory, out var segment) && segment.Array is not null
? segment ? segment
: new ArraySegment<byte>(memory.ToArray()); : new ArraySegment<byte>(memory.ToArray());
} }

View File

@ -1,8 +1,9 @@
// Copyright 2021 Cloud Native Foundation. // Copyright 2021 Cloud Native Foundation.
// Licensed under the Apache 2.0 license. // Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information. // See LICENSE file in the project root for full license information.
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Net.Mime; using System.Net.Mime;
using System.Text; using System.Text;
@ -57,7 +58,7 @@ namespace CloudNative.CloudEvents.Core
var header = new MediaTypeHeaderValue(contentType.MediaType); var header = new MediaTypeHeaderValue(contentType.MediaType);
foreach (string parameterName in contentType.Parameters.Keys) foreach (string parameterName in contentType.Parameters.Keys)
{ {
header.Parameters.Add(new NameValueHeaderValue(parameterName, contentType.Parameters[parameterName].ToString())); header.Parameters.Add(new NameValueHeaderValue(parameterName, contentType.Parameters[parameterName]));
} }
return header; return header;
} }
@ -76,7 +77,7 @@ namespace CloudNative.CloudEvents.Core
/// </summary> /// </summary>
/// <param name="contentType">The content type to check. May be null, in which case the result is false.</param> /// <param name="contentType">The content type to check. May be null, in which case the result is false.</param>
/// <returns>true if the given content type denotes a (non-batch) CloudEvent; false otherwise</returns> /// <returns>true if the given content type denotes a (non-batch) CloudEvent; false otherwise</returns>
public static bool IsCloudEventsContentType(string? contentType) => public static bool IsCloudEventsContentType([NotNullWhen(true)] string? contentType) =>
contentType is string && contentType is string &&
contentType.StartsWith(MediaType, StringComparison.InvariantCultureIgnoreCase) && contentType.StartsWith(MediaType, StringComparison.InvariantCultureIgnoreCase) &&
!contentType.StartsWith(BatchMediaType, StringComparison.InvariantCultureIgnoreCase); !contentType.StartsWith(BatchMediaType, StringComparison.InvariantCultureIgnoreCase);
@ -86,7 +87,7 @@ namespace CloudNative.CloudEvents.Core
/// </summary> /// </summary>
/// <param name="contentType">The content type to check. May be null, in which case the result is false.</param> /// <param name="contentType">The content type to check. May be null, in which case the result is false.</param>
/// <returns>true if the given content type represents a CloudEvent batch; false otherwise</returns> /// <returns>true if the given content type represents a CloudEvent batch; false otherwise</returns>
public static bool IsCloudEventsBatchContentType(string? contentType) => public static bool IsCloudEventsBatchContentType([NotNullWhen(true)] string? contentType) =>
contentType is string && contentType.StartsWith(BatchMediaType, StringComparison.InvariantCultureIgnoreCase); contentType is string && contentType.StartsWith(BatchMediaType, StringComparison.InvariantCultureIgnoreCase);
} }
} }

View File

@ -1,10 +1,11 @@
// Copyright (c) Cloud Native Foundation. // Copyright (c) Cloud Native Foundation.
// Licensed under the Apache 2.0 license. // Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information. // See LICENSE file in the project root for full license information.
using CloudNative.CloudEvents.Core; using CloudNative.CloudEvents.Core;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
@ -130,7 +131,7 @@ namespace CloudNative.CloudEvents.Http
return ToCloudEventInternalAsync(httpRequestMessage.Headers, httpRequestMessage.Content, formatter, extensionAttributes, nameof(httpRequestMessage)); return ToCloudEventInternalAsync(httpRequestMessage.Headers, httpRequestMessage.Content, formatter, extensionAttributes, nameof(httpRequestMessage));
} }
private static async Task<CloudEvent> ToCloudEventInternalAsync(HttpHeaders headers, HttpContent content, private static async Task<CloudEvent> ToCloudEventInternalAsync(HttpHeaders headers, HttpContent? content,
CloudEventFormatter formatter, IEnumerable<CloudEventAttribute>? extensionAttributes, string paramName) CloudEventFormatter formatter, IEnumerable<CloudEventAttribute>? extensionAttributes, string paramName)
{ {
Validation.CheckNotNull(formatter, nameof(formatter)); Validation.CheckNotNull(formatter, nameof(formatter));
@ -142,7 +143,7 @@ namespace CloudNative.CloudEvents.Http
} }
else else
{ {
string? versionId = MaybeGetVersionId(headers) ?? MaybeGetVersionId(content.Headers); string? versionId = MaybeGetVersionId(headers) ?? MaybeGetVersionId(content?.Headers);
if (versionId is null) if (versionId is null)
{ {
throw new ArgumentException($"Request does not represent a CloudEvent. It has neither a {HttpUtilities.SpecVersionHttpHeader} header, nor a suitable content type.", nameof(paramName)); throw new ArgumentException($"Request does not represent a CloudEvent. It has neither a {HttpUtilities.SpecVersionHttpHeader} header, nor a suitable content type.", nameof(paramName));
@ -151,7 +152,8 @@ namespace CloudNative.CloudEvents.Http
?? throw new ArgumentException($"Unknown CloudEvents spec version '{versionId}'", paramName); ?? throw new ArgumentException($"Unknown CloudEvents spec version '{versionId}'", paramName);
var cloudEvent = new CloudEvent(version, extensionAttributes); var cloudEvent = new CloudEvent(version, extensionAttributes);
foreach (var header in headers.Concat(content.Headers)) var allHeaders = content is null ? headers : headers.Concat(content.Headers);
foreach (var header in allHeaders)
{ {
string? attributeName = HttpUtilities.GetAttributeNameFromHeaderName(header.Key); string? attributeName = HttpUtilities.GetAttributeNameFromHeaderName(header.Key);
if (attributeName is null || attributeName == CloudEventsSpecVersion.SpecVersionAttribute.Name) if (attributeName is null || attributeName == CloudEventsSpecVersion.SpecVersionAttribute.Name)
@ -231,7 +233,7 @@ namespace CloudNative.CloudEvents.Http
return ToCloudEventBatchInternalAsync(httpRequestMessage.Content, formatter, extensionAttributes, nameof(httpRequestMessage)); return ToCloudEventBatchInternalAsync(httpRequestMessage.Content, formatter, extensionAttributes, nameof(httpRequestMessage));
} }
private static async Task<IReadOnlyList<CloudEvent>> ToCloudEventBatchInternalAsync(HttpContent content, private static async Task<IReadOnlyList<CloudEvent>> ToCloudEventBatchInternalAsync(HttpContent? content,
CloudEventFormatter formatter, IEnumerable<CloudEventAttribute>? extensionAttributes, string paramName) CloudEventFormatter formatter, IEnumerable<CloudEventAttribute>? extensionAttributes, string paramName)
{ {
Validation.CheckNotNull(formatter, nameof(formatter)); Validation.CheckNotNull(formatter, nameof(formatter));
@ -331,16 +333,16 @@ namespace CloudNative.CloudEvents.Http
} }
private static ByteArrayContent ToByteArrayContent(ReadOnlyMemory<byte> content) => private static ByteArrayContent ToByteArrayContent(ReadOnlyMemory<byte> content) =>
MemoryMarshal.TryGetArray(content, out var segment) MemoryMarshal.TryGetArray(content, out var segment) && segment.Array is not null
? new ByteArrayContent(segment.Array, segment.Offset, segment.Count) ? new ByteArrayContent(segment.Array, segment.Offset, segment.Count)
// TODO: Just throw? // TODO: Just throw?
: new ByteArrayContent(content.ToArray()); : new ByteArrayContent(content.ToArray());
// TODO: This would include "application/cloudeventsarerubbish" for example... // TODO: This would include "application/cloudeventsarerubbish" for example...
private static bool HasCloudEventsContentType(HttpContent content) => private static bool HasCloudEventsContentType([NotNullWhen(true)] HttpContent? content) =>
MimeUtilities.IsCloudEventsContentType(content?.Headers?.ContentType?.MediaType); MimeUtilities.IsCloudEventsContentType(content?.Headers?.ContentType?.MediaType);
private static bool HasCloudEventsBatchContentType(HttpContent content) => private static bool HasCloudEventsBatchContentType([NotNullWhen(true)] HttpContent? content) =>
MimeUtilities.IsCloudEventsBatchContentType(content?.Headers?.ContentType?.MediaType); MimeUtilities.IsCloudEventsBatchContentType(content?.Headers?.ContentType?.MediaType);
private static string? MaybeGetVersionId(HttpHeaders? headers) => private static string? MaybeGetVersionId(HttpHeaders? headers) =>
@ -348,4 +350,4 @@ namespace CloudNative.CloudEvents.Http
? headers.GetValues(HttpUtilities.SpecVersionHttpHeader).First() ? headers.GetValues(HttpUtilities.SpecVersionHttpHeader).First()
: null; : null;
} }
} }

View File

@ -179,7 +179,7 @@ namespace CloudNative.CloudEvents.Http
} }
else else
{ {
string versionId = httpListenerRequest.Headers[HttpUtilities.SpecVersionHttpHeader]; string? versionId = httpListenerRequest.Headers[HttpUtilities.SpecVersionHttpHeader];
if (versionId is null) if (versionId is null)
{ {
throw new ArgumentException($"Request does not represent a CloudEvent. It has neither a {HttpUtilities.SpecVersionHttpHeader} header, nor a suitable content type.", nameof(httpListenerRequest)); throw new ArgumentException($"Request does not represent a CloudEvent. It has neither a {HttpUtilities.SpecVersionHttpHeader} header, nor a suitable content type.", nameof(httpListenerRequest));
@ -191,12 +191,18 @@ namespace CloudNative.CloudEvents.Http
var headers = httpListenerRequest.Headers; var headers = httpListenerRequest.Headers;
foreach (var key in headers.AllKeys) foreach (var key in headers.AllKeys)
{ {
// It would be highly unusual for either the key or the value to be null, but
// the contract claims it's possible. Skip it if so.
if (key is null || headers[key] is not string headerValue)
{
continue;
}
string? attributeName = HttpUtilities.GetAttributeNameFromHeaderName(key); string? attributeName = HttpUtilities.GetAttributeNameFromHeaderName(key);
if (attributeName is null || attributeName == CloudEventsSpecVersion.SpecVersionAttribute.Name) if (attributeName is null || attributeName == CloudEventsSpecVersion.SpecVersionAttribute.Name)
{ {
continue; continue;
} }
string attributeValue = HttpUtilities.DecodeHeaderValue(headers[key]); string attributeValue = HttpUtilities.DecodeHeaderValue(headerValue);
cloudEvent.SetAttributeFromString(attributeName, attributeValue); cloudEvent.SetAttributeFromString(attributeName, attributeValue);
} }