Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case

# C++ files
[*.cpp,*.h,*.hpp,*.cc,*.hh,*.cxx,*.hxx]
[*.{cpp,h,hpp,cc,hh,cxx,hxx}]
indent_style = tab

# Naming convention rules (note: currently need to be ordered from more to less specific)
Expand Down
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@ All values are stored in SI base units, so operators read `.Value` directly. Sup
### File headers

```csharp
// Copyright (c) ktsu.dev
// All rights reserved.
// Licensed under the MIT license.
// Copyright (c) 2023-2026 ktsu-dev contributors
```

The text comes from `COPYRIGHT.md`; ktsu.Sdk syncs `.editorconfig`'s `file_header_template` from that file on every build, and `IDE0073` enforces it. If `COPYRIGHT.md` changes, update `GeneratorBase.WriteHeaderTo` to match — generator output is committed source, so a drift there shows up as a diff rather than a build error (`.g.cs` is exempt from `IDE0073`). `SourceGeneratorTests` asserts the emitted header, so the drift fails a test instead.

Generator-emitted files additionally carry `// <auto-generated />`.

### Validation and error handling
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<PackageVersion Include="Polyfill" Version="11.2.0" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.11" />
<PackageVersion Include="System.Memory" Version="4.6.3" />
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
<PackageVersion Include="System.Numerics.Vectors" Version="4.6.1" />
<!-- Source generator packages -->
<PackageVersion Include="ktsu.CodeBlocker" Version="1.2.8" />
Expand Down
7 changes: 5 additions & 2 deletions Semantics.Color/Color.Operations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,15 @@ public double ContrastRatio(Color other)
public AccessibilityLevel AccessibilityLevelAgainst(Color background, bool largeText = false)
{
double contrast = ContrastRatio(background);
if (contrast >= (largeText ? 4.5 : 7.0))
double enhancedThreshold = largeText ? 4.5 : 7.0;
double minimumThreshold = largeText ? 3.0 : 4.5;

if (contrast >= enhancedThreshold)
{
return AccessibilityLevel.AAA;
}

return contrast >= (largeText ? 3.0 : 4.5) ? AccessibilityLevel.AA : AccessibilityLevel.Fail;
return contrast >= minimumThreshold ? AccessibilityLevel.AA : AccessibilityLevel.Fail;
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion Semantics.Color/Color.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,5 @@ public readonly partial record struct Color(double R, double G, double B, double
/// <returns>A float vector of the linear RGB channels.</returns>
public Vector3 ToLinearVector3() => new((float)R, (float)G, (float)B);

internal static double Clamp01(double value) => value < 0.0 ? 0.0 : value > 1.0 ? 1.0 : value;
internal static double Clamp01(double value) => Math.Clamp(value, 0.0, 1.0);
}
2 changes: 1 addition & 1 deletion Semantics.Color/Hsl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ public Srgb ToSrgb()
/// <returns>The adjusted color.</returns>
public Hsl OffsetHue(double degrees) => this with { H = NormalizeHue(H + degrees) };

private static double Clamp01(double value) => value < 0.0 ? 0.0 : value > 1.0 ? 1.0 : value;
private static double Clamp01(double value) => Math.Clamp(value, 0.0, 1.0);

internal static double NormalizeHue(double h)
{
Expand Down
4 changes: 4 additions & 0 deletions Semantics.Color/Semantics.Color.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
<ItemGroup>
<PackageReference Include="Polyfill" PrivateAssets="all" />
<PackageReference Include="System.Numerics.Vectors" Condition="'$(TargetFramework)' == 'netstandard2.0'" />
<!-- Required by KTSU0001 (ktsu.Sdk 2.27.0+): the downlevel targets must reference the packages
supplying the framework types they use rather than picking them up transitively. -->
<PackageReference Include="System.Memory" Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netstandard2.1'" />
<PackageReference Include="System.Threading.Tasks.Extensions" Condition="'$(TargetFramework)' == 'netstandard2.0'" />
</ItemGroup>

</Project>
4 changes: 4 additions & 0 deletions Semantics.Music/Semantics.Music.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

<ItemGroup>
<PackageReference Include="Polyfill" PrivateAssets="all" />
<!-- Required by KTSU0001 (ktsu.Sdk 2.27.0+): the downlevel targets must reference the packages
supplying the framework types they use rather than picking them up transitively. -->
<PackageReference Include="System.Memory" Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netstandard2.1'" />
<PackageReference Include="System.Threading.Tasks.Extensions" Condition="'$(TargetFramework)' == 'netstandard2.0'" />
</ItemGroup>

</Project>
20 changes: 10 additions & 10 deletions Semantics.Paths/Implementations/RelativeDirectoryPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,16 +140,6 @@ public AbsoluteDirectoryPath AsAbsolute()
return AbsoluteDirectoryPath.Create<AbsoluteDirectoryPath>(absolutePath);
}

/// <summary>
/// Explicitly implements IRelativePath.AsAbsolute() to return the base AbsolutePath type.
/// </summary>
/// <returns>An <see cref="AbsolutePath"/> representing this absolute path.</returns>
AbsolutePath IRelativePath.AsAbsolute()
{
string absolutePath = Path.GetFullPath(WeakString);
return AbsolutePath.Create<AbsolutePath>(absolutePath);
}

/// <summary>
/// Converts this relative directory path to an absolute directory path using the specified base directory.
/// </summary>
Expand All @@ -167,6 +157,16 @@ public AbsoluteDirectoryPath AsAbsolute(AbsoluteDirectoryPath baseDirectory)
return AbsoluteDirectoryPath.Create<AbsoluteDirectoryPath>(absolutePath);
}

/// <summary>
/// Explicitly implements IRelativePath.AsAbsolute() to return the base AbsolutePath type.
/// </summary>
/// <returns>An <see cref="AbsolutePath"/> representing this absolute path.</returns>
AbsolutePath IRelativePath.AsAbsolute()
{
string absolutePath = Path.GetFullPath(WeakString);
return AbsolutePath.Create<AbsolutePath>(absolutePath);
}

/// <summary>
/// Converts this relative directory path to a relative directory path using the specified base directory.
/// Since this is already a relative path, returns itself.
Expand Down
20 changes: 10 additions & 10 deletions Semantics.Paths/Implementations/RelativeFilePath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,6 @@ public AbsoluteFilePath AsAbsolute()
return AbsoluteFilePath.Create<AbsoluteFilePath>(absolutePath);
}

/// <summary>
/// Explicitly implements IRelativePath.AsAbsolute() to return the base AbsolutePath type.
/// </summary>
/// <returns>An <see cref="AbsolutePath"/> representing this absolute path.</returns>
AbsolutePath IRelativePath.AsAbsolute()
{
string absolutePath = Path.GetFullPath(WeakString);
return AbsolutePath.Create<AbsolutePath>(absolutePath);
}

/// <summary>
/// Converts this relative file path to an absolute file path using the specified base directory.
/// </summary>
Expand All @@ -101,6 +91,16 @@ public AbsoluteFilePath AsAbsolute(AbsoluteDirectoryPath baseDirectory)
return AbsoluteFilePath.Create<AbsoluteFilePath>(absolutePath);
}

/// <summary>
/// Explicitly implements IRelativePath.AsAbsolute() to return the base AbsolutePath type.
/// </summary>
/// <returns>An <see cref="AbsolutePath"/> representing this absolute path.</returns>
AbsolutePath IRelativePath.AsAbsolute()
{
string absolutePath = Path.GetFullPath(WeakString);
return AbsolutePath.Create<AbsolutePath>(absolutePath);
}

/// <summary>
/// Converts this relative file path to a relative file path using the specified base directory.
/// Since this is already a relative path, returns itself.
Expand Down
5 changes: 4 additions & 1 deletion Semantics.Paths/Semantics.Paths.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
<ItemGroup>
<PackageReference Include="Polyfill" PrivateAssets="all" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Condition="'$(TargetFramework)' == 'netstandard2.0'" />
<PackageReference Include="System.Memory" Condition="'$(TargetFramework)' == 'netstandard2.0'" />
<!-- KTSU0001 (ktsu.Sdk 2.27.0+) requires System.Memory on netstandard2.1 as well, and
System.Threading.Tasks.Extensions on netstandard2.0. -->
<PackageReference Include="System.Memory" Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netstandard2.1'" />
<PackageReference Include="System.Threading.Tasks.Extensions" Condition="'$(TargetFramework)' == 'netstandard2.0'" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Semantics.Strings\Semantics.Strings.csproj" />
Expand Down
5 changes: 5 additions & 0 deletions Semantics.SourceGenerators/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("ktsu.Semantics.Test")]
8 changes: 5 additions & 3 deletions Semantics.SourceGenerators/Templates/PropertyTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ namespace Semantics.SourceGenerators.Templates;

internal class PropertyTemplate : MemberTemplate
{
public static Action<CodeBlocker> AutoGet = (sw) => sw.Write("get;");
public static Action<CodeBlocker> AutoSet = (sw) => sw.Write("set;");
public static Action<CodeBlocker> AutoInit = (sw) => sw.Write("init;");
// Compared by reference in WriteTo to detect auto-property shorthand, so these must stay
// single fixed instances; readonly enforces that without changing the comparison.
public static readonly Action<CodeBlocker> AutoGet = (sw) => sw.Write("get;");
public static readonly Action<CodeBlocker> AutoSet = (sw) => sw.Write("set;");
public static readonly Action<CodeBlocker> AutoInit = (sw) => sw.Write("init;");

public Action<CodeBlocker>? GetterFactory { get; set; }
public Action<CodeBlocker>? SetterFactory { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
<ItemGroup>
<PackageReference Include="Polyfill" PrivateAssets="all" />
<PackageReference Include="System.Text.Json" />
<PackageReference Include="System.Memory" Condition="'$(TargetFramework)' == 'netstandard2.0'" />
<!-- KTSU0001 (ktsu.Sdk 2.27.0+) requires System.Memory on netstandard2.1 as well, and
System.Threading.Tasks.Extensions on netstandard2.0. -->
<PackageReference Include="System.Memory" Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netstandard2.1'" />
<PackageReference Include="System.Threading.Tasks.Extensions" Condition="'$(TargetFramework)' == 'netstandard2.0'" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Semantics.Strings\Semantics.Strings.csproj" />
Expand Down
5 changes: 4 additions & 1 deletion Semantics.Strings/Semantics.Strings.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
<PackageReference Include="Polyfill" PrivateAssets="all" />
<PackageReference Include="ktsu.RoundTripStringJsonConverter" />
<PackageReference Include="System.Text.Json" />
<PackageReference Include="System.Memory" Condition="'$(TargetFramework)' == 'netstandard2.0'" />
<!-- KTSU0001 (ktsu.Sdk 2.27.0+) requires System.Memory on netstandard2.1 as well, and
System.Threading.Tasks.Extensions on netstandard2.0. -->
<PackageReference Include="System.Memory" Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netstandard2.1'" />
<PackageReference Include="System.Threading.Tasks.Extensions" Condition="'$(TargetFramework)' == 'netstandard2.0'" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="ktsu.Semantics.Test" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public sealed class IsKebabCaseAttribute : NativeSemanticStringValidationAttribu
/// </summary>
private sealed class KebabCaseValidator : ValidationAdapter
{
private const string FailureMessage = "The value must be in kebab-case format.";

/// <summary>
/// Validates that a string is in kebab-case.
/// </summary>
Expand All @@ -40,27 +42,27 @@ protected override ValidationResult ValidateValue(string value)
}

// Cannot start or end with hyphen
if (value.StartsWith("-") || value.EndsWith("-"))
if (value.StartsWith('-') || value.EndsWith('-'))
{
return ValidationResult.Failure("The value must be in kebab-case format.");
return ValidationResult.Failure(FailureMessage);
}

// Cannot have consecutive hyphens
if (value.Contains("--"))
{
return ValidationResult.Failure("The value must be in kebab-case format.");
return ValidationResult.Failure(FailureMessage);
}

// No spaces, underscores, or other separators allowed (except hyphens)
if (value.Any(c => char.IsWhiteSpace(c) || c == '_'))
{
return ValidationResult.Failure("The value must be in kebab-case format.");
return ValidationResult.Failure(FailureMessage);
}

// All characters must be lowercase letters, digits, or hyphens
if (!value.All(c => char.IsLower(c) || char.IsDigit(c) || c == '-'))
{
return ValidationResult.Failure("The value must be in kebab-case format.");
return ValidationResult.Failure(FailureMessage);
}

return ValidationResult.Success();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public sealed class IsMacroCaseAttribute : NativeSemanticStringValidationAttribu
/// </summary>
private sealed class MacroCaseValidator : ValidationAdapter
{
private const string FailureMessage = "The value must be in MACRO_CASE format.";

/// <summary>
/// Validates that a string is in MACRO_CASE.
/// </summary>
Expand All @@ -41,27 +43,27 @@ protected override ValidationResult ValidateValue(string value)
}

// Cannot start or end with underscore
if (value.StartsWith("_") || value.EndsWith("_"))
if (value.StartsWith('_') || value.EndsWith('_'))
{
return ValidationResult.Failure("The value must be in MACRO_CASE format.");
return ValidationResult.Failure(FailureMessage);
}

// Cannot have consecutive underscores
if (value.Contains("__"))
{
return ValidationResult.Failure("The value must be in MACRO_CASE format.");
return ValidationResult.Failure(FailureMessage);
}

// No spaces, hyphens, or other separators allowed (except underscores)
if (value.Any(c => char.IsWhiteSpace(c) || c == '-'))
{
return ValidationResult.Failure("The value must be in MACRO_CASE format.");
return ValidationResult.Failure(FailureMessage);
}

// All characters must be uppercase letters, digits, or underscores
if (!value.All(c => char.IsUpper(c) || char.IsDigit(c) || c == '_'))
{
return ValidationResult.Failure("The value must be in MACRO_CASE format.");
return ValidationResult.Failure(FailureMessage);
}

return ValidationResult.Success();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
/// </summary>
private sealed class SentenceCaseValidator : ValidationAdapter
{
private const string FailureMessage = "The value must be in sentence case format.";

/// <summary>
/// Validates that a string is in sentence case.
/// </summary>
Expand All @@ -41,14 +43,14 @@

// Find the first letter in the string
char? firstLetter = value.FirstOrDefault(char.IsLetter);
if (firstLetter.HasValue && !char.IsUpper(firstLetter.Value))

Check warning on line 46 in Semantics.Strings/Validation/Attributes/Casing/IsSentenceCaseAttribute.cs

View workflow job for this annotation

GitHub Actions / Build, Test & Release

Change this condition so that it does not always evaluate to 'True'.

Check warning on line 46 in Semantics.Strings/Validation/Attributes/Casing/IsSentenceCaseAttribute.cs

View workflow job for this annotation

GitHub Actions / Build, Test & Release

Change this condition so that it does not always evaluate to 'True'.
{
return ValidationResult.Failure("The value must be in sentence case format.");
return ValidationResult.Failure(FailureMessage);
}

// Check that all other letters after the first are lowercase
bool foundFirstLetter = false;
foreach (char c in value)

Check warning on line 53 in Semantics.Strings/Validation/Attributes/Casing/IsSentenceCaseAttribute.cs

View workflow job for this annotation

GitHub Actions / Build, Test & Release

Loops should be simplified using the "Where" LINQ method

Check warning on line 53 in Semantics.Strings/Validation/Attributes/Casing/IsSentenceCaseAttribute.cs

View workflow job for this annotation

GitHub Actions / Build, Test & Release

Loops should be simplified using the "Where" LINQ method
{
if (char.IsLetter(c))
{
Expand All @@ -60,7 +62,7 @@

if (char.IsUpper(c))
{
return ValidationResult.Failure("The value must be in sentence case format.");
return ValidationResult.Failure(FailureMessage);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public sealed class IsSnakeCaseAttribute : NativeSemanticStringValidationAttribu
/// </summary>
private sealed class SnakeCaseValidator : ValidationAdapter
{
private const string FailureMessage = "The value must be in snake_case format.";

/// <summary>
/// Validates that a string is in snake_case.
/// </summary>
Expand All @@ -40,27 +42,27 @@ protected override ValidationResult ValidateValue(string value)
}

// Cannot start or end with underscore
if (value.StartsWith("_") || value.EndsWith("_"))
if (value.StartsWith('_') || value.EndsWith('_'))
{
return ValidationResult.Failure("The value must be in snake_case format.");
return ValidationResult.Failure(FailureMessage);
}

// Cannot have consecutive underscores
if (value.Contains("__"))
{
return ValidationResult.Failure("The value must be in snake_case format.");
return ValidationResult.Failure(FailureMessage);
}

// No spaces, hyphens, or other separators allowed (except underscores)
if (value.Any(c => char.IsWhiteSpace(c) || c == '-'))
{
return ValidationResult.Failure("The value must be in snake_case format.");
return ValidationResult.Failure(FailureMessage);
}

// All characters must be lowercase letters, digits, or underscores
if (!value.All(c => char.IsLower(c) || char.IsDigit(c) || c == '_'))
{
return ValidationResult.Failure("The value must be in snake_case format.");
return ValidationResult.Failure(FailureMessage);
}

return ValidationResult.Success();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
/// Use this attribute only when you specifically need string-based semantic validation.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
[Obsolete("Consider using System.DateTime directly instead of semantic string types. DateTime provides better type safety, performance, built-in comparison operations, and rich API for date/time operations.")]

Check warning on line 20 in Semantics.Strings/Validation/Attributes/FirstClassTypes/IsDateTimeAttribute.cs

View workflow job for this annotation

GitHub Actions / Build, Test & Release

Do not forget to remove this deprecated code someday.

Check warning on line 20 in Semantics.Strings/Validation/Attributes/FirstClassTypes/IsDateTimeAttribute.cs

View workflow job for this annotation

GitHub Actions / Build, Test & Release

Do not forget to remove this deprecated code someday.

Check warning on line 20 in Semantics.Strings/Validation/Attributes/FirstClassTypes/IsDateTimeAttribute.cs

View workflow job for this annotation

GitHub Actions / Build, Test & Release

Do not forget to remove this deprecated code someday.

Check warning on line 20 in Semantics.Strings/Validation/Attributes/FirstClassTypes/IsDateTimeAttribute.cs

View workflow job for this annotation

GitHub Actions / Build, Test & Release

Do not forget to remove this deprecated code someday.
public sealed class IsDateTimeAttribute : NativeSemanticStringValidationAttribute
{
/// <summary>
Expand All @@ -43,7 +43,9 @@
return ValidationResult.Success();
}

bool isValid = DateTime.TryParse(value, out _);
// CurrentCulture is stated explicitly rather than switching to InvariantCulture: it is
// what the provider-less overload already used, so validation behaviour is unchanged.
bool isValid = DateTime.TryParse(value, System.Globalization.CultureInfo.CurrentCulture, System.Globalization.DateTimeStyles.None, out _);
return isValid
? ValidationResult.Success()
: ValidationResult.Failure("The value must be a valid DateTime.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
/// Use this attribute only when you specifically need string-based semantic validation.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
[Obsolete("Consider using System.TimeSpan directly instead of semantic string types. TimeSpan provides better type safety, performance, built-in comparison operations, and rich API for time operations.")]

Check warning on line 20 in Semantics.Strings/Validation/Attributes/FirstClassTypes/IsTimeSpanAttribute.cs

View workflow job for this annotation

GitHub Actions / Build, Test & Release

Do not forget to remove this deprecated code someday.

Check warning on line 20 in Semantics.Strings/Validation/Attributes/FirstClassTypes/IsTimeSpanAttribute.cs

View workflow job for this annotation

GitHub Actions / Build, Test & Release

Do not forget to remove this deprecated code someday.
public sealed class IsTimeSpanAttribute : NativeSemanticStringValidationAttribute
{
/// <summary>
Expand All @@ -43,7 +43,9 @@
return ValidationResult.Success();
}

bool isValid = TimeSpan.TryParse(value, out _);
// CurrentCulture is stated explicitly rather than switching to InvariantCulture: it is
// what the provider-less overload already used, so validation behaviour is unchanged.
bool isValid = TimeSpan.TryParse(value, System.Globalization.CultureInfo.CurrentCulture, out _);
return isValid
? ValidationResult.Success()
: ValidationResult.Failure("The value must be a valid TimeSpan.");
Expand Down
Loading
Loading