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
8 changes: 7 additions & 1 deletion .sonarlint/sonar-local.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@
This file wires the same analyzers into a local build. It is NOT imported
automatically - point MSBuild at it explicitly:

dotnet build -p:CustomBeforeMicrosoftCommonProps=$PWD/.sonarlint/sonar-local.props
dotnet build -p:CustomAfterMicrosoftCommonProps=$PWD/.sonarlint/sonar-local.props

Note "After", not "Before". This was CustomBeforeMicrosoftCommonProps, which reached
only Semantics.SourceGenerators - the one project declaring its SDK with the
<Project Sdk="..."> attribute form. Every ktsu.Sdk project uses <Project> with
<Sdk Name="..." /> elements, and the Before hook does not reach that form. The After
hook does, and is still early enough for restore to pick up the PackageReference below.

sonar-local.globalconfig (next to this file) raises the rules that CI reports but
that the analyzer package leaves off by default, so the local warning set matches
Expand Down
8 changes: 5 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,18 @@ Tests use MSTest. Generator output is emitted to `Semantics.Quantities/Generated
CI analyses this repository with the SonarCloud scanner, which injects the Sonar analyzers into the compilation. A plain `dotnet build` does **not** run them, so Sonar findings are invisible locally and only surface after a push — a ~10 minute round trip per attempt. To run the same analyzers:

```bash
dotnet build -p:CustomBeforeMicrosoftCommonProps=$PWD/.sonarlint/sonar-local.props
dotnet build -p:CustomAfterMicrosoftCommonProps=$PWD/.sonarlint/sonar-local.props
```

```powershell
dotnet build -p:CustomBeforeMicrosoftCommonProps=$PWD\.sonarlint\sonar-local.props
dotnet build -p:CustomAfterMicrosoftCommonProps=$PWD\.sonarlint\sonar-local.props
```

Note **`After`**, not `Before`. `CustomBeforeMicrosoftCommonProps` reaches only `Semantics.SourceGenerators`, the one project declaring its SDK with the `<Project Sdk="...">` attribute form; the `ktsu.Sdk` projects use `<Project>` with `<Sdk Name="..." />` elements, which that hook does not reach. `CustomAfterMicrosoftCommonProps` does reach them, and is still early enough for restore to pick up the analyzer `PackageReference`.

The opt-in lives in `.sonarlint/sonar-local.props` (analyzer package) and `.sonarlint/sonar-local.globalconfig` (rule severities — it raises the rules CI reports that the analyzer package ships disabled, and silences the ones CI's quality profile does not report). Nothing imports these automatically, so normal builds, the CI pipeline, and packaging are unaffected.

**Known limitation:** this currently only reaches `Semantics.SourceGenerators`, the one project declaring its SDK with the `<Project Sdk="...">` attribute form. The `ktsu.Sdk` projects use `<Project>` with `<Sdk Name="..." />` elements, and `CustomBeforeMicrosoftCommonProps` does not reach them. Findings in `Semantics.Strings`, `Paths`, `Music`, `Color` and `Quantities` still have to be read from SonarCloud.
**Caveat:** the globalconfig was calibrated against this repository's quality profile. If a project sits under a different profile the local rule set will not match it exactly, so treat a clean local run as strong evidence rather than proof.

## Project layout

Expand Down
5 changes: 5 additions & 0 deletions Semantics.Color/Oklab.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ public Oklch ToOklch()

// netstandard2.0 lacks Math.Cbrt; one Newton-Raphson refinement after a
// sign-aware Pow gives a correctly-rounded result on all target frameworks.
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Major Code Smell", "S1244:Do not check floating point equality with exact values",
Justification = "The exact comparison is the point: the Newton-Raphson step divides by x * x, " +
"and x is exactly zero only when value is. A tolerance band would send small-but-valid " +
"inputs down the shortcut and lose precision that the refinement exists to recover.")]
private static double Cbrt(double value)
{
if (value == 0.0)
Expand Down
9 changes: 6 additions & 3 deletions Semantics.Quantities/AudioEngineering/NormalizedParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,12 @@ public static NormalizedParameter<T> Skewed(T min, T max, T skew)
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="min"/> and <paramref name="max"/> are not both non-zero and of the same sign.</exception>
public static NormalizedParameter<T> Logarithmic(T min, T max)
{
double lo = double.CreateChecked(min);
double hi = double.CreateChecked(max);
if (lo == 0.0 || hi == 0.0 || Math.Sign(lo) != Math.Sign(hi))
// Via Math.Sign rather than a direct == 0.0: the zero check has to be exact (a logarithmic
// range is undefined at zero but perfectly well defined at 1e-9, so a tolerance band would
// reject legitimate ranges), and comparing signs says that without comparing floats.
int loSign = Math.Sign(double.CreateChecked(min));
int hiSign = Math.Sign(double.CreateChecked(max));
if (loSign == 0 || hiSign == 0 || loSign != hiSign)
{
throw new ArgumentOutOfRangeException(nameof(min), "A logarithmic range requires min and max to be non-zero and of the same sign.");
}
Expand Down
39 changes: 39 additions & 0 deletions Semantics.SourceGenerators/CodeGen/MetadataFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,45 @@ public Location FindLocation(string needle)
: Location.Create(path, new TextSpan(index, needle.Length), sourceText.Lines.GetLinePositionSpan(new TextSpan(index, needle.Length)));
}

/// <summary>
/// Finds <paramref name="needle"/> at or after the first occurrence of <paramref name="anchor"/>,
/// and returns a location covering it.
/// </summary>
/// <param name="anchor">Text that scopes the search — typically the entry the needle belongs to.</param>
/// <param name="needle">The text to find within that scope.</param>
/// <returns>
/// A location in the metadata file, falling back to the first unscoped match of
/// <paramref name="needle"/> and then to <see cref="Location.None"/>.
/// </returns>
/// <remarks>
/// The unscoped <see cref="FindLocation(string)"/> is enough when the needle is a typo, which by
/// definition occurs once. It is not enough when the needle is a name that is spelled correctly
/// in dozens of places and only wrong in one of them — there the first occurrence is somewhere
/// else entirely. Anchoring on the surrounding entry picks the right one.
/// </remarks>
public Location FindLocation(string anchor, string needle)
{
if (sourceText is null || string.IsNullOrEmpty(needle))
{
return Location.None;
}

int scope = string.IsNullOrEmpty(anchor) ? 0 : Text.IndexOf(anchor, StringComparison.Ordinal);
if (scope < 0)
{
return FindLocation(needle);
}

int index = Text.IndexOf(needle, scope, StringComparison.Ordinal);
if (index < 0)
{
return FindLocation(needle);
}

TextSpan span = new(index, needle.Length);
return Location.Create(path, span, sourceText.Lines.GetLinePositionSpan(span));
}

/// <summary>
/// Deserializes the file into <typeparamref name="T"/>.
/// </summary>
Expand Down
6 changes: 3 additions & 3 deletions Semantics.SourceGenerators/Generators/DimensionsGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,20 +127,20 @@ protected override void Generate(SourceProductionContext context, DimensionsMeta

// Emit per-dimension marker interfaces (I{Dim}Unit : IUnit) so generated
// quantity types can accept dimensionally-compatible units only.
foreach (PhysicalDimension dimension in sortedDimensions)
foreach (string dimensionName in sortedDimensions.Select(dimension => dimension.Name))
{
sourceFileTemplate.Classes.Add(new ClassTemplate
{
Comments =
{
Emit.SummaryOpen,
$"/// Marker interface implemented by every unit of the <c>{dimension.Name}</c> dimension.",
$"/// Marker interface implemented by every unit of the <c>{dimensionName}</c> dimension.",
"/// Generated quantities use this to make <c>In(...)</c> dimensionally type-safe at compile time.",
Emit.SummaryClose,
},
Kind = TypeKind.Interface,
Keywords = {Emit.Public},
Name = $"I{dimension.Name}Unit",
Name = $"I{dimensionName}Unit",
Interfaces = {"IUnit"},
});
}
Expand Down
Loading
Loading