diff --git a/.github/scripts/check-package.py b/.github/scripts/check-package.py
index 8098e27..a4c2d14 100644
--- a/.github/scripts/check-package.py
+++ b/.github/scripts/check-package.py
@@ -10,6 +10,7 @@
names = package.namelist()
assert "lib/netstandard2.0/DimensionalQuantities.dll" in names
assert "analyzers/dotnet/cs/DimensionalQuantities.Roslyn.dll" in names
+ assert "analyzers/dotnet/cs/DimensionalQuantities.Roslyn.CodeFixes.dll" in names
assert not any("Microsoft.CodeAnalysis" in name for name in names)
specification = ET.fromstring(package.read("DimensionalQuantities.nuspec"))
assert specification.find(".//{*}version").text == version
diff --git a/src/DimensionalQuantities.IntegrationTests/GeneratedQuantityTests/PackagedGeneratorTests.cs b/src/DimensionalQuantities.IntegrationTests/GeneratedQuantityTests/PackagedGeneratorTests.cs
index db31c7c..b3b1cee 100644
--- a/src/DimensionalQuantities.IntegrationTests/GeneratedQuantityTests/PackagedGeneratorTests.cs
+++ b/src/DimensionalQuantities.IntegrationTests/GeneratedQuantityTests/PackagedGeneratorTests.cs
@@ -51,6 +51,7 @@ public static double MultiplyVoltageCurrent(double volts, double amps)
""");
ProjectCreator creator = ProjectCreator.Templates.SdkCsproj(path: consumerProject, targetFramework: "netstandard2.0")
.Property("LangVersion", "preview")
+ .Property("WarningsAsErrors", "CS8032;CS8785;AD0001")
.ItemPackageReference(package)
.Save();
@@ -175,6 +176,7 @@ private ProjectCreator CreateProject(string name, string source, params Package[
File.WriteAllText(Path.Combine(directory, name + ".cs"), source);
ProjectCreator creator = ProjectCreator.Templates.SdkCsproj(path: Path.Combine(directory, name + ".csproj"), targetFramework: "netstandard2.0")
.Property("LangVersion", "preview")
+ .Property("WarningsAsErrors", "CS8032;CS8785;AD0001")
.Property("Version", "1.0.0")
.Property("PackageOutputPath", Path.Combine(directory, "packages"))
.Property("CopyLocalLockFileAssemblies", "true");
diff --git a/src/DimensionalQuantities.Roslyn.CodeFixes/DimensionalQuantities.Roslyn.CodeFixes.csproj b/src/DimensionalQuantities.Roslyn.CodeFixes/DimensionalQuantities.Roslyn.CodeFixes.csproj
new file mode 100644
index 0000000..7df797a
--- /dev/null
+++ b/src/DimensionalQuantities.Roslyn.CodeFixes/DimensionalQuantities.Roslyn.CodeFixes.csproj
@@ -0,0 +1,12 @@
+
+
+ netstandard2.0
+ true
+ false
+
+
+
+
+
+
+
diff --git a/src/DimensionalQuantities.Roslyn.CodeFixes/QuantityAliasCodeFixProvider.cs b/src/DimensionalQuantities.Roslyn.CodeFixes/QuantityAliasCodeFixProvider.cs
new file mode 100644
index 0000000..cfe1c29
--- /dev/null
+++ b/src/DimensionalQuantities.Roslyn.CodeFixes/QuantityAliasCodeFixProvider.cs
@@ -0,0 +1,73 @@
+using System.Collections.Immutable;
+using System.Composition;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Operations;
+
+namespace DimensionalQuantities.Roslyn.CodeFixes;
+
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(QuantityAliasCodeFixProvider))]
+[Shared]
+public sealed class QuantityAliasCodeFixProvider : CodeFixProvider
+{
+ public override ImmutableArray FixableDiagnosticIds => ["DQ0003"];
+
+ public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
+
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ Diagnostic diagnostic = context.Diagnostics[0];
+ if (!diagnostic.Properties.TryGetValue("AliasName", out string? alias) || alias is null or "")
+ {
+ return;
+ }
+
+ SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+ SemanticModel? model = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
+ if (root is null || model is null
+ || root.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf() is not GenericNameSyntax generic)
+ {
+ return;
+ }
+
+ NameSyntax name = GetOutermostName(generic);
+ if (name.Ancestors().OfType()
+ .Any(invocation => model.GetOperation(invocation, context.CancellationToken) is INameOfOperation))
+ {
+ return;
+ }
+
+ ITypeSymbol? originalType = model.GetTypeInfo(name, context.CancellationToken).Type;
+ TypeSyntax replacement = SyntaxFactory.ParseTypeName(alias);
+ ITypeSymbol? replacementType = model.GetSpeculativeTypeInfo(name.SpanStart, replacement,
+ SpeculativeBindingOption.BindAsTypeOrNamespace).Type;
+ if (originalType is null || !SymbolEqualityComparer.Default.Equals(originalType, replacementType))
+ {
+ return;
+ }
+
+ context.RegisterCodeFix(CodeAction.Create($"Use {alias}", cancellationToken => ReplaceAsync(context.Document, name, replacement, cancellationToken),
+ $"UseAlias:{alias}"), diagnostic);
+ }
+
+ private static NameSyntax GetOutermostName(NameSyntax name)
+ {
+ while (name.Parent is NameSyntax parent)
+ {
+ name = parent;
+ }
+
+ return name;
+ }
+
+ private static async Task ReplaceAsync(Document document, NameSyntax original,
+ TypeSyntax replacement, CancellationToken cancellationToken)
+ {
+ SyntaxNode root = (await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false))!;
+ SyntaxNode updated = root.ReplaceNode(original, replacement.WithTriviaFrom(original));
+ return document.WithSyntaxRoot(updated);
+ }
+}
diff --git a/src/DimensionalQuantities.Roslyn.Tests/Analyzers/QuantityEncodingAnalyzerTests.cs b/src/DimensionalQuantities.Roslyn.Tests/Analyzers/QuantityEncodingAnalyzerTests.cs
new file mode 100644
index 0000000..36010d3
--- /dev/null
+++ b/src/DimensionalQuantities.Roslyn.Tests/Analyzers/QuantityEncodingAnalyzerTests.cs
@@ -0,0 +1,72 @@
+using System.Collections.Immutable;
+using DimensionalQuantities.Roslyn.Analyzers;
+using Microsoft.CodeAnalysis;
+using Xunit;
+
+namespace DimensionalQuantities.Roslyn.Tests.Analyzers;
+
+public sealed class QuantityEncodingAnalyzerTests
+{
+ private const string LengthType = "Quantity>";
+
+ [Fact]
+ public async Task RawQuantitySuggestsReadableAlias()
+ {
+ string source = $"using DimensionalQuantities.TypeSystem; public class Consumer {{ public {LengthType} Value {{ get; }} }}";
+ Diagnostic result = Assert.Single(await AnalyzerTestCompilation.Analyze(source, new QuantityEncodingAnalyzer()));
+ Assert.Multiple(
+ () => Assert.Equal("DQ0003", result.Id),
+ () => Assert.Contains("Length", result.GetMessage()),
+ () => Assert.Equal(LengthType, source.Substring(result.Location.SourceSpan.Start, result.Location.SourceSpan.Length)));
+ }
+
+ [Theory]
+ [InlineData("global::DimensionalQuantities.TypeSystem.Fraction")]
+ [InlineData("global::DimensionalQuantities.TypeSystem.Fraction")]
+ public async Task NonCanonicalEncodingExplainsDistinctTypeIdentity(string exponent)
+ {
+ string type = $"global::DimensionalQuantities.TypeSystem.Quantity>";
+ string source = $"using MyLength = {type}; public class Consumer {{ }}";
+ ImmutableArray results = await AnalyzerTestCompilation.Analyze(source, new QuantityEncodingAnalyzer());
+ Diagnostic result = Assert.Single(results);
+ Assert.Multiple(
+ () => Assert.Equal("DQ0004", result.Id),
+ () => Assert.Equal(DiagnosticSeverity.Warning, result.Severity));
+ }
+
+ [Theory]
+ [InlineData("Quantity")]
+ [InlineData("Quantity, TerminalHex0, TerminalHex0, TerminalHex0, TerminalHex0, TerminalHex0, TerminalHex0>>")]
+ [InlineData("Quantity, TerminalHex0, TerminalHex0, TerminalHex0, TerminalHex0, TerminalHex0, TerminalHex0>>")]
+ public async Task InvalidClosedEncodingIsDiagnosedWithoutAnalyzerCrash(string type)
+ {
+ string source = $"using DimensionalQuantities.TypeSystem; public class Consumer {{ public {type} Value {{ get; }} }}";
+ Diagnostic result = Assert.Single(await AnalyzerTestCompilation.Analyze(source, new QuantityEncodingAnalyzer(), generate: false));
+ Assert.Multiple(
+ () => Assert.Equal("DQ0005", result.Id),
+ () => Assert.Equal(DiagnosticSeverity.Error, result.Severity));
+ }
+
+ [Theory]
+ [InlineData("Quantity")]
+ [InlineData("Power")]
+ public async Task UnresolvedArgumentsDoNotProduceEncodingDiagnostics(string type)
+ {
+ string source = $"using DimensionalQuantities.TypeSystem; public class Consumer {{ public {type} Value {{ get; }} }}";
+ ImmutableArray result = await AnalyzerTestCompilation.Analyze(source, new QuantityEncodingAnalyzer(), generate: false);
+ Assert.Empty(result);
+ }
+
+ [Theory]
+ [InlineData("using MyLength = DimensionalQuantities.TypeSystem.Quantity>; public class Consumer { public MyLength Value { get; } }")]
+ [InlineData("public class Consumer { public Length Value { get; } }")]
+ [InlineData("using DimensionalQuantities.TypeSystem; public class Consumer { public Quantity Value { get; } }")]
+ [InlineData("public class Quantity { } public class Consumer { public Quantity Value { get; } }")]
+ public async Task DeliberateAliasesOpenGenericsAndUnrelatedTypesAreAllowed(string source)
+ {
+ ImmutableArray result = await AnalyzerTestCompilation.Analyze(source, new QuantityEncodingAnalyzer());
+ Assert.Empty(result);
+ }
+}
diff --git a/src/DimensionalQuantities.Roslyn.Tests/CodeFixes/QuantityAliasCodeFixProviderTests.cs b/src/DimensionalQuantities.Roslyn.Tests/CodeFixes/QuantityAliasCodeFixProviderTests.cs
new file mode 100644
index 0000000..e2e6779
--- /dev/null
+++ b/src/DimensionalQuantities.Roslyn.Tests/CodeFixes/QuantityAliasCodeFixProviderTests.cs
@@ -0,0 +1,94 @@
+using System.Collections.Immutable;
+using DimensionalQuantities.Roslyn.Analyzers;
+using DimensionalQuantities.Roslyn.CodeFixes;
+using DimensionalQuantities.Roslyn.Tests.Analyzers;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Text;
+using Xunit;
+
+namespace DimensionalQuantities.Roslyn.Tests.CodeFixes;
+
+public sealed class QuantityAliasCodeFixProviderTests
+{
+ private const string LengthType = "global::DimensionalQuantities.TypeSystem.Quantity>";
+
+ private const string LengthCurrentType = "global::DimensionalQuantities.TypeSystem.Quantity>";
+
+ [Theory]
+ [InlineData("public class Consumer { public TYPE Value { get; } }", "public class Consumer { public Length Value { get; } }")]
+ [InlineData("public class Consumer { public TYPE? Value { get; } }", "public class Consumer { public Length? Value { get; } }")]
+ [InlineData("public class Consumer { public /* measurement */ TYPE Value { get; } }", "public class Consumer { public /* measurement */ Length Value { get; } }")]
+ [InlineData("public class Consumer { public TYPE Value { get; } }", "public class Consumer { public LengthCurrent Value { get; } }", LengthCurrentType)]
+ public async Task FixUsesAliasAndPreservesSurroundingSource(string template, string expected, string? rawType = null)
+ {
+ string source = template.Replace("TYPE", rawType ?? LengthType);
+ using AdhocWorkspace workspace = new();
+ Document document = CreateDocument(workspace, source);
+ Compilation compilation = (await document.Project.GetCompilationAsync(TestContext.Current.CancellationToken))!;
+ Diagnostic diagnostic = Assert.Single(await compilation.WithAnalyzers([new QuantityEncodingAnalyzer()]).GetAnalyzerDiagnosticsAsync(TestContext.Current.CancellationToken));
+ List actions = [];
+ QuantityAliasCodeFixProvider provider = new();
+ await provider.RegisterCodeFixesAsync(new(document, diagnostic, (action, _) => actions.Add(action), TestContext.Current.CancellationToken));
+ CodeAction action = Assert.Single(actions);
+ ImmutableArray operations = await action.GetOperationsAsync(TestContext.Current.CancellationToken);
+ ApplyChangesOperation changes = Assert.Single(operations.OfType());
+ Document changed = changes.ChangedSolution.GetDocument(document.Id)!;
+ string result = (await changed.GetTextAsync(TestContext.Current.CancellationToken)).ToString();
+ CSharpCompilation finalCompilation = AnalyzerTestCompilation.Create(result);
+ Assert.Multiple(
+ () => Assert.Equal(expected, result),
+ () => Assert.DoesNotContain(finalCompilation.GetDiagnostics(TestContext.Current.CancellationToken), diagnostic => diagnostic.Severity == DiagnosticSeverity.Error));
+ }
+
+ [Fact]
+ public async Task NameOfDoesNotOfferFixThatChangesTheReturnedName()
+ {
+ string source = $"public class Consumer {{ public string Value() => nameof({LengthType}); }}";
+ using AdhocWorkspace workspace = new();
+ Document document = CreateDocument(workspace, source);
+ Compilation compilation = (await document.Project.GetCompilationAsync(TestContext.Current.CancellationToken))!;
+ Diagnostic diagnostic = Assert.Single(await compilation.WithAnalyzers([new QuantityEncodingAnalyzer()]).GetAnalyzerDiagnosticsAsync(TestContext.Current.CancellationToken));
+ List actions = [];
+ QuantityAliasCodeFixProvider provider = new();
+ await provider.RegisterCodeFixesAsync(new(document, diagnostic, (action, _) => actions.Add(action), TestContext.Current.CancellationToken));
+ Assert.Empty(actions);
+ }
+
+ [Fact]
+ public async Task ShadowedAliasDoesNotOfferUnsafeFix()
+ {
+ string source = $"namespace ConsumerNamespace {{ public class Length {{ }} public class Consumer {{ public {LengthType} Value {{ get; }} }} }}";
+ using AdhocWorkspace workspace = new();
+ Document document = CreateDocument(workspace, source);
+ Compilation compilation = (await document.Project.GetCompilationAsync(TestContext.Current.CancellationToken))!;
+ ImmutableArray diagnostics = await compilation.WithAnalyzers([new QuantityEncodingAnalyzer()]).GetAnalyzerDiagnosticsAsync(TestContext.Current.CancellationToken);
+ List actions = [];
+ QuantityAliasCodeFixProvider provider = new();
+ foreach (Diagnostic diagnostic in diagnostics.Where(diagnostic => diagnostic.Id == "DQ0003"))
+ {
+ await provider.RegisterCodeFixesAsync(new(document, diagnostic, (action, _) => actions.Add(action), TestContext.Current.CancellationToken));
+ }
+ Assert.Empty(actions);
+ }
+
+ private static Document CreateDocument(AdhocWorkspace workspace, string source)
+ {
+ CSharpCompilation compilation = AnalyzerTestCompilation.Create(source);
+ Project project = workspace.AddProject(ProjectInfo.Create(ProjectId.CreateNewId(), VersionStamp.Create(), "Consumer", "Consumer", LanguageNames.CSharp,
+ compilationOptions: compilation.Options, parseOptions: new CSharpParseOptions(LanguageVersion.Preview), metadataReferences: compilation.References));
+ Document userDocument = workspace.AddDocument(project.Id, "Consumer.cs", SourceText.From(source));
+ foreach (SyntaxTree tree in compilation.SyntaxTrees.Where(tree => tree.FilePath != "Consumer.cs"))
+ {
+ workspace.AddDocument(project.Id, Path.GetFileName(tree.FilePath), tree.GetText());
+ }
+ return workspace.CurrentSolution.GetDocument(userDocument.Id)!;
+ }
+}
diff --git a/src/DimensionalQuantities.Roslyn.Tests/DimensionalQuantities.Roslyn.Tests.csproj b/src/DimensionalQuantities.Roslyn.Tests/DimensionalQuantities.Roslyn.Tests.csproj
index bf1de2b..dfd83d3 100644
--- a/src/DimensionalQuantities.Roslyn.Tests/DimensionalQuantities.Roslyn.Tests.csproj
+++ b/src/DimensionalQuantities.Roslyn.Tests/DimensionalQuantities.Roslyn.Tests.csproj
@@ -13,8 +13,10 @@
+
+
diff --git a/src/DimensionalQuantities.Roslyn.Tests/Discovery/DiscoverySeedCollectorTests.cs b/src/DimensionalQuantities.Roslyn.Tests/Discovery/DiscoverySeedCollectorTests.cs
new file mode 100644
index 0000000..2c50818
--- /dev/null
+++ b/src/DimensionalQuantities.Roslyn.Tests/Discovery/DiscoverySeedCollectorTests.cs
@@ -0,0 +1,33 @@
+using System.Collections.Immutable;
+using DimensionalQuantities.Dimensions;
+using DimensionalQuantities.Roslyn.Discovery;
+using DimensionalQuantities.TypeSystem;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Xunit;
+
+namespace DimensionalQuantities.Roslyn.Tests.Discovery;
+
+public sealed class DiscoverySeedCollectorTests
+{
+ [Fact]
+ public void RawQuantityDeclarationRequestsItsDimension()
+ {
+ const string source = """
+ using DimensionalQuantities.TypeSystem;
+ public class Consumer
+ {
+ public Quantity> Value { get; }
+ }
+ """;
+ SyntaxTree tree = CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(LanguageVersion.Preview), cancellationToken: TestContext.Current.CancellationToken);
+ string[] assemblies = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!).Split(Path.PathSeparator);
+ IEnumerable references = assemblies.Select(path => MetadataReference.CreateFromFile(path))
+ .Append(MetadataReference.CreateFromFile(typeof(Quantity<>).Assembly.Location));
+ CSharpCompilation compilation = CSharpCompilation.Create("Consumer", [tree], references, new(OutputKind.DynamicallyLinkedLibrary));
+ ImmutableArray result = DiscoverySeedCollector.FindRequestedDimensions(compilation, [tree], TestContext.Current.CancellationToken);
+ Dimension expected = new(Length: 1, ElectricCurrent: 1);
+
+ Assert.Equal(expected, Assert.Single(result));
+ }
+}
diff --git a/src/DimensionalQuantities.Roslyn/AnalyzerReleases.Unshipped.md b/src/DimensionalQuantities.Roslyn/AnalyzerReleases.Unshipped.md
index 3f208ce..3a1a39c 100644
--- a/src/DimensionalQuantities.Roslyn/AnalyzerReleases.Unshipped.md
+++ b/src/DimensionalQuantities.Roslyn/AnalyzerReleases.Unshipped.md
@@ -4,3 +4,6 @@ Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
DQ0001 | DimensionalQuantities | Error | DimensionMismatchAnalyzer
DQ0002 | DimensionalQuantities | Error | DimensionMismatchAnalyzer
+DQ0003 | DimensionalQuantities | Info | QuantityEncodingAnalyzer
+DQ0004 | DimensionalQuantities | Warning | QuantityEncodingAnalyzer
+DQ0005 | DimensionalQuantities | Error | QuantityEncodingAnalyzer
diff --git a/src/DimensionalQuantities.Roslyn/Analyzers/CanonicalTypeEncoding.cs b/src/DimensionalQuantities.Roslyn/Analyzers/CanonicalTypeEncoding.cs
new file mode 100644
index 0000000..a9c2af1
--- /dev/null
+++ b/src/DimensionalQuantities.Roslyn/Analyzers/CanonicalTypeEncoding.cs
@@ -0,0 +1,41 @@
+using DimensionalQuantities.Numerics;
+using DimensionalQuantities.Roslyn.Discovery;
+using DimensionalQuantities.TypeSystem;
+using Microsoft.CodeAnalysis;
+
+namespace DimensionalQuantities.Roslyn.Analyzers;
+
+internal static class CanonicalTypeEncoding
+{
+ public static bool TryInspect(ITypeSymbol type, TypeSystemNumberReader reader, Compilation compilation,
+ out bool canonical, CancellationToken cancellationToken)
+ {
+ canonical = false;
+ if (!reader.TryRead(type, out Rational value, cancellationToken))
+ {
+ return false;
+ }
+
+ Type expectedType = TypeSystemNumberCodec.CreateType(value);
+ ITypeSymbol? expected = Resolve(expectedType, compilation);
+ canonical = expected is not null && SymbolEqualityComparer.Default.Equals(type, expected);
+ return true;
+ }
+
+ private static ITypeSymbol? Resolve(Type runtimeType, Compilation compilation)
+ {
+ if (!runtimeType.IsConstructedGenericType)
+ {
+ return compilation.GetTypeByMetadataName(runtimeType.FullName!);
+ }
+
+ INamedTypeSymbol? definition = compilation.GetTypeByMetadataName(runtimeType.GetGenericTypeDefinition().FullName!);
+ if (definition is null)
+ {
+ return null;
+ }
+
+ ITypeSymbol?[] arguments = runtimeType.GetGenericArguments().Select(argument => Resolve(argument, compilation)).ToArray();
+ return arguments.Any(argument => argument is null) ? null : definition.Construct(arguments!);
+ }
+}
diff --git a/src/DimensionalQuantities.Roslyn/Analyzers/QuantityEncodingAnalyzer.cs b/src/DimensionalQuantities.Roslyn/Analyzers/QuantityEncodingAnalyzer.cs
new file mode 100644
index 0000000..d302638
--- /dev/null
+++ b/src/DimensionalQuantities.Roslyn/Analyzers/QuantityEncodingAnalyzer.cs
@@ -0,0 +1,134 @@
+using System.Collections.Immutable;
+using DimensionalQuantities.Dimensions;
+using DimensionalQuantities.Roslyn.Discovery;
+using DimensionalQuantities.TypeSystem;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace DimensionalQuantities.Roslyn.Analyzers;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class QuantityEncodingAnalyzer : DiagnosticAnalyzer
+{
+ internal const string AliasNameProperty = "AliasName";
+
+ internal static readonly DiagnosticDescriptor ReadableAlias = new(
+ "DQ0003", "Use a readable quantity alias", "Use the readable alias '{0}' for this quantity",
+ "DimensionalQuantities", DiagnosticSeverity.Info, isEnabledByDefault: true);
+
+ internal static readonly DiagnosticDescriptor NonCanonical = new(
+ "DQ0004", "Noncanonical type-system number", "This encoding is noncanonical; equivalent values have distinct type identity",
+ "DimensionalQuantities", DiagnosticSeverity.Warning, isEnabledByDefault: true);
+
+ internal static readonly DiagnosticDescriptor Invalid = new(
+ "DQ0005", "Invalid type-system encoding", "This closed type-system encoding is invalid",
+ "DimensionalQuantities", DiagnosticSeverity.Error, isEnabledByDefault: true);
+
+ public override ImmutableArray SupportedDiagnostics => [ReadableAlias, NonCanonical, Invalid];
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterCompilationStartAction(start =>
+ {
+ INamedTypeSymbol? quantity = start.Compilation.GetTypeByMetadataName(typeof(Quantity<>).FullName!);
+ INamedTypeSymbol? dimension = start.Compilation.GetTypeByMetadataName(typeof(TypeSystem.Dimension<,,,,,,>).FullName!);
+ INamedTypeSymbol? power = start.Compilation.GetTypeByMetadataName(typeof(Power<>).FullName!);
+ if (quantity is null || dimension is null || power is null)
+ {
+ return;
+ }
+
+ TypeSystemNumberReader reader = new(start.Compilation);
+ start.RegisterSyntaxNodeAction(node => AnalyzeGeneric(node, reader, quantity, dimension, power), SyntaxKind.GenericName);
+ });
+ }
+
+ private static void AnalyzeGeneric(SyntaxNodeAnalysisContext context, TypeSystemNumberReader reader,
+ INamedTypeSymbol quantity, INamedTypeSymbol dimension, INamedTypeSymbol power)
+ {
+ GenericNameSyntax syntax = (GenericNameSyntax)context.Node;
+ if (context.SemanticModel.GetTypeInfo(syntax, context.CancellationToken).Type is not INamedTypeSymbol type
+ || IsIncomplete(type))
+ {
+ return;
+ }
+
+ if (SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, quantity))
+ {
+ AnalyzeQuantity(context, reader, syntax, type, dimension);
+ }
+ else if (SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, power))
+ {
+ AnalyzePower(context, reader, syntax, type);
+ }
+ }
+
+ private static void AnalyzeQuantity(SyntaxNodeAnalysisContext context, TypeSystemNumberReader reader,
+ GenericNameSyntax syntax, INamedTypeSymbol quantity, INamedTypeSymbol dimensionType)
+ {
+ if (quantity.TypeArguments[0] is not INamedTypeSymbol encodedDimension
+ || !SymbolEqualityComparer.Default.Equals(encodedDimension.OriginalDefinition, dimensionType))
+ {
+ Report(context, Invalid, syntax);
+ return;
+ }
+
+ bool canonical = true;
+ foreach (ITypeSymbol exponent in encodedDimension.TypeArguments)
+ {
+ if (!CanonicalTypeEncoding.TryInspect(exponent, reader, context.SemanticModel.Compilation,
+ out bool exponentCanonical, context.CancellationToken))
+ {
+ Report(context, Invalid, syntax);
+ return;
+ }
+
+ canonical &= exponentCanonical;
+ }
+
+ if (!canonical)
+ {
+ Report(context, NonCanonical, syntax);
+ return;
+ }
+
+ if (syntax.Ancestors().OfType().Any())
+ {
+ return;
+ }
+
+ QuantityTypeReader quantityReader = new(context.SemanticModel.Compilation);
+ if (!quantityReader.TryRead(quantity, out Dimension dimension, context.CancellationToken) || dimension == default)
+ {
+ return;
+ }
+
+ string alias = DimensionNames.Get(dimension);
+ ImmutableDictionary properties = ImmutableDictionary.Empty.Add(AliasNameProperty, alias);
+ context.ReportDiagnostic(Diagnostic.Create(ReadableAlias, syntax.GetLocation(), properties, alias));
+ }
+
+ private static void AnalyzePower(SyntaxNodeAnalysisContext context, TypeSystemNumberReader reader,
+ GenericNameSyntax syntax, INamedTypeSymbol power)
+ {
+ if (!CanonicalTypeEncoding.TryInspect(power.TypeArguments[0], reader, context.SemanticModel.Compilation,
+ out bool canonical, context.CancellationToken))
+ {
+ Report(context, Invalid, syntax);
+ }
+ else if (!canonical)
+ {
+ Report(context, NonCanonical, syntax);
+ }
+ }
+
+ private static bool IsIncomplete(ITypeSymbol type) => type is ITypeParameterSymbol or IErrorTypeSymbol
+ || type is INamedTypeSymbol named && (named.IsUnboundGenericType || named.TypeArguments.Any(IsIncomplete));
+
+ private static void Report(SyntaxNodeAnalysisContext context, DiagnosticDescriptor descriptor, GenericNameSyntax syntax) =>
+ context.ReportDiagnostic(Diagnostic.Create(descriptor, syntax.GetLocation()));
+}
diff --git a/src/DimensionalQuantities.Roslyn/Discovery/DiscoverySeedCollector.cs b/src/DimensionalQuantities.Roslyn/Discovery/DiscoverySeedCollector.cs
index 793444f..85507fb 100644
--- a/src/DimensionalQuantities.Roslyn/Discovery/DiscoverySeedCollector.cs
+++ b/src/DimensionalQuantities.Roslyn/Discovery/DiscoverySeedCollector.cs
@@ -69,7 +69,7 @@ private static ImmutableArray FindMetadataDimensions(SyntaxNode root,
foreach (ExpressionSyntax expression in root.DescendantNodes().OfType())
{
cancellationToken.ThrowIfCancellationRequested();
- if (expression is InvocationExpressionSyntax or MemberAccessExpressionSyntax or ElementAccessExpressionSyntax
+ if (expression is InvocationExpressionSyntax or MemberAccessExpressionSyntax or ElementAccessExpressionSyntax or GenericNameSyntax
|| expression is IdentifierNameSyntax name && !SyntaxFacts.IsInTypeOnlyContext(name))
{
dimensions.AddRange(metadata.Discover(expression, cancellationToken));
diff --git a/src/DimensionalQuantities.Roslyn/Discovery/TypeSystemNumberReader.cs b/src/DimensionalQuantities.Roslyn/Discovery/TypeSystemNumberReader.cs
index 7425b01..073b73a 100644
--- a/src/DimensionalQuantities.Roslyn/Discovery/TypeSystemNumberReader.cs
+++ b/src/DimensionalQuantities.Roslyn/Discovery/TypeSystemNumberReader.cs
@@ -32,8 +32,19 @@ public bool TryRead(ITypeSymbol? symbol, out Rational value, CancellationToken c
return false;
}
- value = TypeSystemNumberCodec.Parse(type!);
- return true;
+ try
+ {
+ value = TypeSystemNumberCodec.Parse(type!);
+ return true;
+ }
+ catch (DivideByZeroException)
+ {
+ return false;
+ }
+ catch (ArgumentException)
+ {
+ return false;
+ }
}
private bool TryGetRuntimeType(ITypeSymbol? symbol, out Type? type, CancellationToken cancellationToken)
diff --git a/src/DimensionalQuantities.slnx b/src/DimensionalQuantities.slnx
index 420d2ce..ddef88c 100644
--- a/src/DimensionalQuantities.slnx
+++ b/src/DimensionalQuantities.slnx
@@ -4,6 +4,7 @@
+
diff --git a/src/DimensionalQuantities/DimensionalQuantities.csproj b/src/DimensionalQuantities/DimensionalQuantities.csproj
index 547ee04..bb64c9f 100644
--- a/src/DimensionalQuantities/DimensionalQuantities.csproj
+++ b/src/DimensionalQuantities/DimensionalQuantities.csproj
@@ -13,6 +13,8 @@
+