From bd8c09babbd839df2ff910747bc8de82122d28b1 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Sat, 14 Mar 2026 08:57:11 +0000 Subject: [PATCH] Add nullable annotations This will help clarify expectations about object model validation and make the project more accessible to downstream consumers. --- build/Project.csproj.props | 3 -- src/Firmware.cs | 17 ++++--- src/Interface.cs | 84 +++++++++++++++++--------------- src/TemplateBase.cs | 8 +-- tests/FirmwareGeneratorTests.cs | 21 ++++---- tests/InterfaceGeneratorTests.cs | 16 +++--- tests/MetadataSerializerTests.cs | 12 +++-- tests/PayloadMarshal.cs | 7 ++- tests/YamlStream.cs | 12 ++--- 9 files changed, 99 insertions(+), 81 deletions(-) diff --git a/build/Project.csproj.props b/build/Project.csproj.props index 1d004b4..c1df222 100644 --- a/build/Project.csproj.props +++ b/build/Project.csproj.props @@ -1,5 +1,2 @@ - - annotations - \ No newline at end of file diff --git a/src/Firmware.cs b/src/Firmware.cs index d3e78b0..6992162 100644 --- a/src/Firmware.cs +++ b/src/Firmware.cs @@ -169,7 +169,7 @@ public class PortPinInfo /// Specifies the microcontroller port where the pin is located. /// [YamlMember(Order = -1)] - public string Port; + public string Port = ""; /// /// Specifies the unique pin number in the defined port. @@ -357,12 +357,9 @@ class PortPinInfoTypeConverter(IDeserializer deserializer) : IYamlTypeConverter public IDeserializer Deserializer { get; } = deserializer ?? throw new ArgumentNullException(nameof(deserializer)); - public bool Accepts(Type type) - { - return type == typeof(PortPinInfo); - } + public bool Accepts(Type type) => type == typeof(PortPinInfo); - public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) { var portPin = Deserializer.Deserialize>(parser); if (portPin.TryGetValue(DirectionProperty, out object value)) @@ -381,7 +378,7 @@ public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeseria throw new YamlException($"Required property '{DirectionProperty}' not found when deserializing type '{typeof(PortPinInfo)}'."); } - public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer) + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) { throw new NotImplementedException(); } @@ -397,6 +394,9 @@ class LowerCaseEnumTypeConverter : IYamlTypeConverter public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) { + if (value is null) + return; + var scalarStyle = ScalarStyle.Any; var scalarValue = LowerCaseNamingConvention.Instance.Apply(value.ToString()); if (scalarValue == "off") @@ -415,6 +415,9 @@ class CamelCaseEnumTypeConverter : IYamlTypeConverter public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) { + if (value is null) + return; + emitter.Emit(new Scalar(CamelCaseNamingConvention.Instance.Apply(value.ToString()))); } } diff --git a/src/Interface.cs b/src/Interface.cs index b000b0c..c35e682 100644 --- a/src/Interface.cs +++ b/src/Interface.cs @@ -18,7 +18,7 @@ public class DeviceInfo /// /// Specifies the name of the device. /// - public string Device; + public string Device = ""; /// /// Specifies the unique identifier for this device type. @@ -28,12 +28,12 @@ public class DeviceInfo /// /// Specifies the version of the device firmware. /// - public HarpVersion FirmwareVersion; + public HarpVersion? FirmwareVersion; /// /// Specifies the version of the device hardware. /// - public HarpVersion HardwareTargets; + public HarpVersion? HardwareTargets; /// /// Specifies the collection of registers implementing the device function. @@ -151,7 +151,7 @@ public class RegisterInfo /// /// Specifies the name of the bit mask or group mask used to represent the payload value. /// - public string MaskType; + public string MaskType = ""; /// /// Specifies the minimum allowable value for the payload. @@ -171,7 +171,7 @@ public class RegisterInfo /// /// Specifies the name of the type used to represent the payload value in the high-level interface. /// - public string InterfaceType; + public string InterfaceType = ""; /// /// Specifies a custom converter which will be used to parse or format the payload value. @@ -187,7 +187,7 @@ public class RegisterInfo /// Specifies a collection of payload members describing the contents /// of the raw payload value. /// - public Dictionary PayloadSpec; + public Dictionary? PayloadSpec; /// /// Gets a value indicating whether a custom converter will be used to parse or @@ -229,7 +229,7 @@ public class PayloadMemberInfo /// /// Specifies the name of the bit mask or group mask used to represent this payload member. /// - public string MaskType; + public string MaskType = ""; /// /// Specifies the minimum allowable value for this payload member. @@ -249,7 +249,7 @@ public class PayloadMemberInfo /// /// Specifies the name of the type used to represent this payload member in the high-level interface. /// - public string InterfaceType; + public string InterfaceType = ""; /// /// Specifies a custom converter which will be used to parse or format this payload member. @@ -350,7 +350,7 @@ public class MaskValue /// /// Specifies a summary description of the mask value function. /// - public string Description; + public string Description = ""; } internal static partial class TemplateHelper @@ -478,7 +478,9 @@ public static string GetParseConversion(RegisterInfo register, string expression else if (register.InterfaceType == "string") return $"PayloadMarshal.ReadUtf8String({expression})"; else - return GetConversionToInterfaceType(register.InterfaceType ?? register.MaskType, expression); + return GetConversionToInterfaceType( + string.IsNullOrEmpty(register.InterfaceType) ? register.MaskType : register.InterfaceType, + expression); } public static string GetFormatConversion(RegisterInfo register, string expression) @@ -486,7 +488,10 @@ public static string GetFormatConversion(RegisterInfo register, string expressio if (register.PayloadSpec != null || register.InterfaceType == "string" || register.HasConverter) return $"FormatPayload({expression})"; else - return GetConversionFromInterfaceType(register.InterfaceType ?? register.MaskType, register.PayloadInterfaceType, expression); + return GetConversionFromInterfaceType( + string.IsNullOrEmpty(register.InterfaceType) ? register.MaskType : register.InterfaceType, + register.PayloadInterfaceType, + expression); } public static string GetConversionToInterfaceType(string interfaceType, string expression) @@ -608,7 +613,9 @@ public static string GetPayloadMemberParser( { return $"ParsePayload{name}({expression})"; } - return GetConversionToInterfaceType(member.InterfaceType ?? member.MaskType, expression); + return GetConversionToInterfaceType( + string.IsNullOrEmpty(member.InterfaceType) ? member.MaskType : member.InterfaceType, + expression); } public static string GetPayloadMemberAssignmentFormatter( @@ -661,7 +668,8 @@ public static string GetPayloadMemberValueFormatter( var isBoolean = member.InterfaceType == "bool" && !member.HasConverter; var payloadInterfaceType = GetInterfaceType(payloadType); - if (!string.IsNullOrEmpty(member.InterfaceType ?? member.MaskType)) + var memberInterfaceType = string.IsNullOrEmpty(member.InterfaceType) ? member.MaskType : member.InterfaceType; + if (!string.IsNullOrEmpty(memberInterfaceType)) { if (isBoolean) { @@ -697,10 +705,7 @@ class HarpVersionTypeConverter : IYamlTypeConverter { public static readonly HarpVersionTypeConverter Instance = new(); - public bool Accepts(Type type) - { - return type == typeof(HarpVersion); - } + public bool Accepts(Type type) => type == typeof(HarpVersion); public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) { @@ -708,12 +713,15 @@ public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeseria return HarpVersion.Parse(scalar.Value); } - public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer) + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) { + if (value is not HarpVersion version) + return; + var scalar = new Scalar( AnchorName.Empty, TagName.Empty, - ((HarpVersion)value).ToString(), + version.ToString(), ScalarStyle.DoubleQuoted, isPlainImplicit: false, isQuotedImplicit: true); @@ -727,12 +735,12 @@ class HexValueTypeConverter : IYamlTypeConverter public bool Accepts(Type type) => false; - public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) { return rootDeserializer(type); } - public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer) + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) { emitter.Emit(new Scalar(string.Format($"0x{value:X}"))); } @@ -750,17 +758,14 @@ class MaskValueTypeConverter : IYamlTypeConverter .WithTypeConverter(HexValueTypeConverter.Instance) .BuildValueSerializer(); - public bool Accepts(Type type) - { - return type == typeof(MaskValue); - } + public bool Accepts(Type type) => type == typeof(MaskValue); - public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) { - if (parser.TryConsume(out MappingStart _)) + if (parser.TryConsume(out var _)) { var maskValue = new MaskValue(); - while (!parser.TryConsume(out MappingEnd _)) + while (!parser.TryConsume(out var _)) { var key = parser.Consume(); var value = parser.Consume(); @@ -786,9 +791,11 @@ public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeseria } } - public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer) + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) { - var maskValue = (MaskValue)value; + if (value is not MaskValue maskValue) + return; + if (string.IsNullOrEmpty(maskValue.Description)) HexValueTypeConverter.Instance.WriteYaml(emitter, maskValue.Value, typeof(int), serializer); else @@ -800,17 +807,14 @@ class RegisterAccessTypeConverter : IYamlTypeConverter { public static readonly RegisterAccessTypeConverter Instance = new(); - public bool Accepts(Type type) - { - return type == typeof(RegisterAccess); - } + public bool Accepts(Type type) => type == typeof(RegisterAccess); - public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) { - if (parser.TryConsume(out SequenceStart _)) + if (parser.TryConsume(out var _)) { RegisterAccess value = 0; - while (parser.TryConsume(out Scalar scalar)) + while (parser.TryConsume(out var scalar)) { value |= (RegisterAccess)Enum.Parse(typeof(RegisterAccess), scalar.Value); } @@ -824,9 +828,11 @@ public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeseria } } - public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer) + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) { - var access = (RegisterAccess)value; + if (value is not RegisterAccess access) + return; + switch (access) { case RegisterAccess.Read: diff --git a/src/TemplateBase.cs b/src/TemplateBase.cs index 61f4fdd..aae5471 100644 --- a/src/TemplateBase.cs +++ b/src/TemplateBase.cs @@ -7,12 +7,12 @@ namespace Harp.Generators; internal abstract class TemplateBase { - private StringBuilder builder; - private CompilerErrorCollection errors; + private StringBuilder? builder; + private CompilerErrorCollection? errors; private string currentIndent = string.Empty; - private Stack indents; + private Stack? indents; - public virtual IDictionary Session { get; set; } + public virtual IDictionary? Session { get; set; } public string FileName { get; set; } = string.Empty; diff --git a/tests/FirmwareGeneratorTests.cs b/tests/FirmwareGeneratorTests.cs index 7de67c3..a7d364c 100644 --- a/tests/FirmwareGeneratorTests.cs +++ b/tests/FirmwareGeneratorTests.cs @@ -5,7 +5,7 @@ namespace Harp.Generators.Tests; [TestClass] public sealed class FirmwareGeneratorTests { - DirectoryInfo outputDirectory; + DirectoryInfo? outputDirectory; [TestInitialize] public void Initialize() @@ -49,14 +49,17 @@ public void FirmwareTemplate_GenerateAndBuild(string metadataFileName, params st } catch (AssertFailedException) { - outputDirectory.Create(); - File.WriteAllText(Path.Combine(outputDirectory.FullName, appOutputFileName), headers.App); - File.WriteAllText(Path.Combine(outputDirectory.FullName, appImplOutputFileName), implementation.App); - File.WriteAllText(Path.Combine(outputDirectory.FullName, appFuncsOutputFileName), headers.AppFuncs); - File.WriteAllText(Path.Combine(outputDirectory.FullName, appFuncsImplOutputFileName), implementation.AppFuncs); - File.WriteAllText(Path.Combine(outputDirectory.FullName, appRegsOutputFileName), headers.AppRegs); - File.WriteAllText(Path.Combine(outputDirectory.FullName, appRegsImplOutputFileName), implementation.AppRegs); - File.WriteAllText(Path.Combine(outputDirectory.FullName, interruptsOutputFileName), implementation.Interrupts); + if (outputDirectory is not null) + { + outputDirectory.Create(); + File.WriteAllText(Path.Combine(outputDirectory.FullName, appOutputFileName), headers.App); + File.WriteAllText(Path.Combine(outputDirectory.FullName, appImplOutputFileName), implementation.App); + File.WriteAllText(Path.Combine(outputDirectory.FullName, appFuncsOutputFileName), headers.AppFuncs); + File.WriteAllText(Path.Combine(outputDirectory.FullName, appFuncsImplOutputFileName), implementation.AppFuncs); + File.WriteAllText(Path.Combine(outputDirectory.FullName, appRegsOutputFileName), headers.AppRegs); + File.WriteAllText(Path.Combine(outputDirectory.FullName, appRegsImplOutputFileName), implementation.AppRegs); + File.WriteAllText(Path.Combine(outputDirectory.FullName, interruptsOutputFileName), implementation.Interrupts); + } throw; } } diff --git a/tests/InterfaceGeneratorTests.cs b/tests/InterfaceGeneratorTests.cs index a9eda41..4998b8c 100644 --- a/tests/InterfaceGeneratorTests.cs +++ b/tests/InterfaceGeneratorTests.cs @@ -5,8 +5,8 @@ namespace Harp.Generators.Tests; [TestClass] public sealed class InterfaceGeneratorTests { - DirectoryInfo outputDirectory; - string payloadExtensions; + DirectoryInfo? outputDirectory; + string payloadExtensions = ""; [TestInitialize] public void Initialize() @@ -24,7 +24,7 @@ public void DeviceTemplate_GenerateAndBuildWithoutErrors(string metadataFileName { metadataFileName = TestHelper.GetMetadataPath(metadataFileName); var deviceMetadata = TestHelper.ReadDeviceMetadata(metadataFileName); - var generator = new InterfaceGenerator(deviceMetadata, typeof(InterfaceGeneratorTests).Namespace); + var generator = new InterfaceGenerator(deviceMetadata, typeof(InterfaceGeneratorTests).Namespace ?? ""); var implementation = generator.GenerateImplementation(); var outputFileName = Path.GetFileNameWithoutExtension(metadataFileName); var deviceOutputFileName = $"{outputFileName}.cs"; @@ -32,16 +32,18 @@ public void DeviceTemplate_GenerateAndBuildWithoutErrors(string metadataFileName var customImplementation = TestHelper.GetManifestResourceText($"EmbeddedSources.{outputFileName}.cs"); try { - TestHelper.AssertNoGeneratorErrors(generator.Errors); CompilerTestHelper.CompileFromSource(implementation.Device, implementation.AsyncDevice, payloadExtensions, customImplementation); TestHelper.AssertExpectedOutput(implementation.Device, deviceOutputFileName); TestHelper.AssertExpectedOutput(implementation.AsyncDevice, asyncDeviceOutputFileName); } catch (AssertFailedException) { - outputDirectory.Create(); - File.WriteAllText(Path.Combine(outputDirectory.FullName, deviceOutputFileName), implementation.Device); - File.WriteAllText(Path.Combine(outputDirectory.FullName, asyncDeviceOutputFileName), implementation.AsyncDevice); + if (outputDirectory is not null) + { + outputDirectory.Create(); + File.WriteAllText(Path.Combine(outputDirectory.FullName, deviceOutputFileName), implementation.Device); + File.WriteAllText(Path.Combine(outputDirectory.FullName, asyncDeviceOutputFileName), implementation.AsyncDevice); + } throw; } } diff --git a/tests/MetadataSerializerTests.cs b/tests/MetadataSerializerTests.cs index f5bb566..83f6c2d 100644 --- a/tests/MetadataSerializerTests.cs +++ b/tests/MetadataSerializerTests.cs @@ -6,7 +6,7 @@ namespace Harp.Generators.Tests; [TestClass] public sealed class MetadataSerializerTests { - DirectoryInfo outputDirectory; + DirectoryInfo? outputDirectory; [TestInitialize] public void Initialize() @@ -51,9 +51,13 @@ public void Metadata_RoundTripSerializes(string metadataFileName, Type type) } catch (AssertFailedException) { - outputDirectory.Create(); - File.WriteAllText(Path.Combine(outputDirectory.FullName, Path.GetFileName(metadataFileName)), roundTripContents); - File.WriteAllText(Path.Combine(outputDirectory.FullName, $"{Path.GetFileNameWithoutExtension(metadataFileName)}_orig.yml"), metadataContents); + if (outputDirectory is not null) + { + outputDirectory.Create(); + var originalMetadataFileName = $"{Path.GetFileNameWithoutExtension(metadataFileName)}_orig.yml"; + File.WriteAllText(Path.Combine(outputDirectory.FullName, Path.GetFileName(metadataFileName)), roundTripContents); + File.WriteAllText(Path.Combine(outputDirectory.FullName, originalMetadataFileName), metadataContents); + } throw; } } diff --git a/tests/PayloadMarshal.cs b/tests/PayloadMarshal.cs index 83caaa0..e266ab7 100644 --- a/tests/PayloadMarshal.cs +++ b/tests/PayloadMarshal.cs @@ -9,13 +9,16 @@ internal static partial class PayloadMarshal { internal static HarpVersion ReadHarpVersion(ArraySegment segment) { - var major = segment.Array[segment.Offset]; - var minor = segment.Array[segment.Offset + 1]; + var major = segment.Array?[segment.Offset]; + var minor = segment.Array?[segment.Offset + 1]; return new HarpVersion(major, minor); } internal static void Write(ArraySegment segment, HarpVersion value) { + if (segment.Array is null) + return; + segment.Array[segment.Offset] = (byte)value.Major.GetValueOrDefault(); segment.Array[segment.Offset + 1] = (byte)value.Minor.GetValueOrDefault(); } diff --git a/tests/YamlStream.cs b/tests/YamlStream.cs index 87e571e..97bcdc7 100644 --- a/tests/YamlStream.cs +++ b/tests/YamlStream.cs @@ -55,8 +55,8 @@ internal class YamlSequenceNode : YamlNode internal YamlSequenceNode(IParser parser) { Start = parser.Consume(); - SequenceEnd end; - while (!parser.TryConsume(out end)) + SequenceEnd? end; + while (!parser.TryConsume(out end)) { Children.Add(Parse(parser)); } @@ -83,8 +83,8 @@ internal class YamlMappingNode : YamlNode internal YamlMappingNode(IParser parser) { Start = parser.Consume(); - MappingEnd end; - while (!parser.TryConsume(out end)) + MappingEnd? end; + while (!parser.TryConsume(out end)) { var key = new YamlScalarNode(parser); var value = Parse(parser); @@ -114,9 +114,9 @@ class YamlScalarNodeEqualityComparer : IEqualityComparer { public static readonly YamlScalarNodeEqualityComparer Default = new(); - public bool Equals(YamlScalarNode x, YamlScalarNode y) + public bool Equals(YamlScalarNode? x, YamlScalarNode? y) { - return x.Event.Value == y.Event.Value; + return x?.Event.Value == y?.Event.Value; } public int GetHashCode([DisallowNull] YamlScalarNode obj)