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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ var converted = sourceString.As<SourceType, TargetType>();
- **SEM001** — a relationship in `dimensions.json` references a dimension that does not exist (typo or rename). The operator is silently dropped.
- **SEM002** — schema-level validation issue (missing `name`/`symbol`, empty `availableUnits`, duplicate type names, no vector forms declared).
- **SEM003** — a relationship's explicit `forms` list references a vector form not declared on a participating dimension. Use `forms` to constrain a relationship to specific vector forms (e.g. `crossProducts: [{ "other": "Length", "result": "Torque", "forms": [3] }]`); when omitted, the legacy "emit at every common form" behaviour is preserved.
- **SEM004** — a dimension's `availableUnits` array references a unit name that isn't declared anywhere in `units.json`. Without the diagnostic the generator silently emits an identity-conversion `From{Unit}` factory, which is wrong for any non-base unit; SEM004 catches the typo at build time.
- See `docs/physics-generator.md` for the full schema and an end-to-end "add a dimension" walk-through.

This file is the entry point. For deeper material:
Expand Down
1 change: 1 addition & 0 deletions Semantics.SourceGenerators/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ Rule ID | Category | Severity | Notes
SEM001 | Semantics.SourceGenerators | Warning | Reports relationships in dimensions.json that reference unknown dimension names.
SEM002 | Semantics.SourceGenerators | Warning | Reports schema-level validation issues in dimensions.json (missing fields, duplicate type names, etc).
SEM003 | Semantics.SourceGenerators | Warning | Reports a relationship whose explicit `forms` list references a vector form not declared on a participating dimension.
SEM004 | Semantics.SourceGenerators | Warning | Reports a `dimensions.json` `availableUnits` entry that doesn't match any unit declared in `units.json`.
60 changes: 60 additions & 0 deletions Semantics.SourceGenerators/Generators/QuantitiesGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ public class QuantitiesGenerator : GeneratorBase<DimensionsMetadata>
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);

private static readonly DiagnosticDescriptor UnknownUnitReference = new(
id: "SEM004",
title: "dimensions.json references a unit not declared in units.json",
messageFormat: "Unit '{0}' (referenced by dimension '{1}'.availableUnits) is not declared in units.json; the generated From{0} factory will use an identity conversion. Add the unit to units.json or fix the spelling.",
category: "Semantics.SourceGenerators",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);

public QuantitiesGenerator() : base("dimensions.json") { }

/// <summary>
Expand Down Expand Up @@ -140,6 +148,13 @@ private void GenerateInner(SourceProductionContext context, DimensionsMetadata m

Dictionary<string, UnitDefinition> unitMap = BuildUnitMap(units);

// Issue #58/#48 follow-up: surface dimensions.json availableUnits entries that
// don't exist in units.json. The generator's BuildToBaseExpression silently falls
// back to identity conversion in that case, which is wrong for any non-base unit
// — a typo (e.g. "Kilometres" vs "Kilometers") would silently produce a factory
// with no scale factor. SEM004 catches that at build time.
ReportUnknownUnitReferences(context, metadata, unitMap);

// Phase A: Build maps and collect operators
Dictionary<string, PhysicalDimension> dimensionMap = BuildDimensionMap(metadata);
Dictionary<string, int> typeFormMap = BuildTypeFormMap(metadata);
Expand Down Expand Up @@ -549,6 +564,51 @@ private static Dictionary<string, UnitDefinition> BuildUnitMap(UnitsMetadata uni
return map;
}

/// <summary>
/// Walks every <c>availableUnits</c> entry across the dimensions metadata and emits
/// <c>SEM004</c> for any unit name that doesn't appear in <paramref name="unitMap"/>.
/// Deduplicates by (unit, dimension) so a typo on a unit shared by many dimensions
/// reports once per offending dimension instead of per-form/overload.
/// </summary>
private static void ReportUnknownUnitReferences(
SourceProductionContext context,
DimensionsMetadata metadata,
Dictionary<string, UnitDefinition> unitMap)
{
// If units.json wasn't loaded the map is empty; treating every unit as "unknown"
// would flood the build log. The CombinedMetadata loader already supplies a
// non-null UnitsMetadata even when units.json is missing — check for that case
// and bail rather than report a useless wall of warnings.
if (unitMap.Count == 0)
{
return;
}

HashSet<string> seen = [];
foreach (PhysicalDimension dim in metadata.PhysicalDimensions)
{
foreach (string unitName in dim.AvailableUnits)
{
if (string.IsNullOrEmpty(unitName) || unitMap.ContainsKey(unitName))
{
continue;
}

string key = $"{dim.Name}::{unitName}";
if (!seen.Add(key))
{
continue;
}

context.ReportDiagnostic(Diagnostic.Create(
UnknownUnitReference,
Location.None,
unitName,
dim.Name));
}
}
}

/// <summary>
/// Emits one <c>From{Unit}</c> static factory per entry in <paramref name="availableUnits"/>.
/// The first unit is treated as the SI base unit (no conversion). Subsequent units use the
Expand Down
Loading