From 97171806666a7c7d32d8d90ba240ec1f32900c19 Mon Sep 17 00:00:00 2001 From: James McMahon Date: Sat, 24 Oct 2026 21:17:08 +0100 Subject: [PATCH] feat: diagnose alias collisions and invalid English exponents --- .../Generators/GenerationDiagnosticTests.cs | 190 ++++++++++++++++++ .../Generators/QuantityGeneratorTests.cs | 6 +- .../AnalyzerReleases.Unshipped.md | 2 + .../Discovery/EnglishExponentDiscovery.cs | 29 ++- .../Discovery/PlannedQuantity.cs | 4 + .../Discovery/QuantityDiscoveryCoordinator.cs | 4 +- .../Discovery/QuantityDiscoveryResult.cs | 3 +- .../Generators/AliasCollisionDiagnostics.cs | 101 ++++++++++ .../Generators/GenerationDiagnostics.cs | 39 ++++ .../Generators/QuantityGenerator.cs | 13 +- .../Generators/QuantitySourceEmitter.cs | 2 +- 11 files changed, 378 insertions(+), 15 deletions(-) create mode 100644 src/DimensionalQuantities.Roslyn.Tests/Generators/GenerationDiagnosticTests.cs create mode 100644 src/DimensionalQuantities.Roslyn/Generators/AliasCollisionDiagnostics.cs create mode 100644 src/DimensionalQuantities.Roslyn/Generators/GenerationDiagnostics.cs diff --git a/src/DimensionalQuantities.Roslyn.Tests/Generators/GenerationDiagnosticTests.cs b/src/DimensionalQuantities.Roslyn.Tests/Generators/GenerationDiagnosticTests.cs new file mode 100644 index 0000000..c175137 --- /dev/null +++ b/src/DimensionalQuantities.Roslyn.Tests/Generators/GenerationDiagnosticTests.cs @@ -0,0 +1,190 @@ +using System.Collections.Immutable; +using DimensionalQuantities.Roslyn.Generators; +using DimensionalQuantities.TypeSystem; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; + +namespace DimensionalQuantities.Roslyn.Tests.Generators; + +public sealed class GenerationDiagnosticTests +{ + [Fact] + public void GeneratedAliasWarnsWhenItShadowsAnImportedType() + { + const string source = """ + using Other; + namespace Other { public class Current { } } + public class Consumer { public Current Value { get; } } + """; + Diagnostic result = Assert.Single(Generate(source)); + Assert.Multiple( + () => Assert.Equal("DQ0006", result.Id), + () => Assert.Equal(DiagnosticSeverity.Warning, result.Severity), + () => Assert.Contains("Current", result.GetMessage()), + () => Assert.Equal("Consumer.cs", result.Location.SourceTree!.FilePath)); + } + + [Fact] + public void ImportedMetadataTypeShadowingIsDiagnosed() + { + CSharpCompilation library = CreateCompilation("namespace Other { public class Current { } }").WithAssemblyName("OtherLibrary"); + using MemoryStream bytes = new(); + Microsoft.CodeAnalysis.Emit.EmitResult emitted = library.Emit(bytes, cancellationToken: TestContext.Current.CancellationToken); + if (!emitted.Success) + { + throw new InvalidOperationException(string.Join(Environment.NewLine, emitted.Diagnostics)); + } + CSharpCompilation consumer = CreateCompilation("using Other; public class Consumer { public Current Value { get; } }") + .AddReferences(MetadataReference.CreateFromImage(bytes.ToArray())); + GeneratorDriver driver = CSharpGeneratorDriver.Create([new QuantityGenerator().AsSourceGenerator()], parseOptions: new CSharpParseOptions(LanguageVersion.Preview)); + driver = driver.RunGenerators(consumer, TestContext.Current.CancellationToken); + Diagnostic result = Assert.Single(driver.GetRunResult().Diagnostics); + + Assert.Equal("DQ0006", result.Id); + } + + [Fact] + public void CollisionWarningPreservesTheGeneratedAliasBinding() + { + const string source = "using Other; namespace Other { public class Current { } } public class Consumer { public Current Value { get; } }"; + CSharpCompilation original = CreateCompilation(source); + GeneratorDriver driver = CSharpGeneratorDriver.Create([new QuantityGenerator().AsSourceGenerator()], parseOptions: new CSharpParseOptions(LanguageVersion.Preview)); + driver.RunGeneratorsAndUpdateCompilation(original, out Compilation generated, out ImmutableArray diagnostics, TestContext.Current.CancellationToken); + IPropertySymbol property = (IPropertySymbol)generated.GetTypeByMetadataName("Consumer")!.GetMembers("Value").Single(); + INamedTypeSymbol result = ((INamedTypeSymbol)property.Type).OriginalDefinition; + INamedTypeSymbol expected = generated.GetTypeByMetadataName(typeof(Quantity<>).FullName!)!; + + Assert.Multiple( + () => Assert.Contains(diagnostics, diagnostic => diagnostic.Id == "DQ0006"), + () => Assert.True(SymbolEqualityComparer.Default.Equals(expected, result))); + } + + [Theory] + [InlineData("public class Current { } public class Consumer { }")] + [InlineData("public delegate void Current();")] + [InlineData("using Current = Other.Current; namespace Other { public class Current { } } public class Consumer { public Current Value { get; } }")] + public void ConflictingDeclarationsAndExplicitAliasesWarn(string source) + { + ImmutableArray results = Generate(source); + Assert.Contains(results, diagnostic => diagnostic.Id == "DQ0006" && diagnostic.Severity == DiagnosticSeverity.Warning); + } + + [Fact] + public void StaticMemberUseOfImportedTypeWarnsWhenShadowed() + { + const string source = """ + using Other; + namespace Other { public class Current { public static int Create() => 1; } } + public class Consumer { public int Value => Current.Create(); } + """; + Diagnostic result = Assert.Single(Generate(source)); + Assert.Equal("DQ0006", result.Id); + } + + [Fact] + public void UnusedSameNamedTypeInAnotherNamespaceDoesNotWarn() + { + const string source = "namespace Other { public class Current { } } public class Consumer { public Current Value { get; } }"; + Assert.Empty(Generate(source)); + } + + [Fact] + public void QualifiedReferenceToSameNamedTypeDoesNotWarn() + { + const string source = "namespace Other { public class Current { } } public class Consumer { public Other.Current Value { get; } }"; + Assert.Empty(Generate(source)); + } + + [Theory] + [InlineData("NumberTwoOverZero")] + [InlineData("NumberBanana")] + public void InvalidEnglishExponentExplainsUnresolvedRequest(string name) + { + string source = $$""" + using DimensionalQuantities.TypeSystem; + public class Consumer { public object Calculate(Length length) => Power<{{name}}>.Of(length); } + """; + Diagnostic result = Assert.Single(Generate(source)); + Assert.Multiple( + () => Assert.Equal("DQ0007", result.Id), + () => Assert.Equal(DiagnosticSeverity.Error, result.Severity), + () => Assert.Contains(name, result.GetMessage()), + () => Assert.Equal(name, source.Substring(result.Location.SourceSpan.Start, result.Location.SourceSpan.Length))); + } + + [Fact] + public void FileLocalAliasElsewhereDoesNotHideInvalidEnglishExponent() + { + const string source = "using DimensionalQuantities.TypeSystem; public class Consumer { public object Calculate(Length length) => Power.Of(length); }"; + CSharpCompilation compilation = CreateCompilation(source).AddSyntaxTrees(CSharpSyntaxTree.ParseText( + "using NumberBanana = System.Int32; public class OtherConsumer { }", + new CSharpParseOptions(LanguageVersion.Preview), "OtherConsumer.cs", cancellationToken: TestContext.Current.CancellationToken)); + GeneratorDriver driver = CSharpGeneratorDriver.Create([new QuantityGenerator().AsSourceGenerator()], parseOptions: new CSharpParseOptions(LanguageVersion.Preview)); + driver = driver.RunGenerators(compilation, TestContext.Current.CancellationToken); + Diagnostic diagnostic = Assert.Single(driver.GetRunResult().Diagnostics); + + Assert.Multiple( + () => Assert.Equal("DQ0007", diagnostic.Id), + () => Assert.Equal("Consumer.cs", diagnostic.Location.SourceTree!.FilePath)); + } + + [Theory] + [InlineData("using DimensionalQuantities.TypeSystem; public class Consumer { public object Calculate(Length length) => Power.Of(length); }")] + [InlineData("using DimensionalQuantities.TypeSystem; public class Consumer { public object Calculate(Length length) => Power.Of(length); }")] + [InlineData("public class Power { public static double Of(double value) => value; } public class Consumer { public object Calculate() => Power.Of(2); }")] + [InlineData("using NumberTwo = DimensionalQuantities.TypeSystem.TerminalHex2; using DimensionalQuantities.TypeSystem; public class Consumer { public object Calculate(Length length) => Power.Of(length); }")] + [InlineData("using DimensionalQuantities.TypeSystem; public class NumberBanana { } public class Consumer { public object Calculate(Length length) => Power.Of(length); }")] + public void BoundNamesAndUnrelatedRequestsDoNotProduceGenerationDiagnostics(string source) + { + Assert.Empty(Generate(source)); + } + + [Fact] + public void InvalidNumericEncodingDoesNotCrashGenerator() + { + const string source = """ + using DimensionalQuantities.TypeSystem; + public class Consumer + { + public object Calculate(Length length) => Power>.Of(length); + } + """; + ImmutableArray results = Generate(source); + Assert.DoesNotContain(results, diagnostic => diagnostic.Id == "CS8785"); + } + + [Fact] + public void CorrectingAnEnglishExponentRemovesItsDiagnostic() + { + const string source = "using DimensionalQuantities.TypeSystem; public class Consumer { public object Calculate(Length length) => Power.Of(length); }"; + CSharpCompilation original = CreateCompilation(source); + GeneratorDriver driver = CSharpGeneratorDriver.Create([new QuantityGenerator().AsSourceGenerator()], parseOptions: new CSharpParseOptions(LanguageVersion.Preview)); + driver = driver.RunGenerators(original, TestContext.Current.CancellationToken); + ImmutableArray initial = driver.GetRunResult().Diagnostics; + CSharpCompilation edited = CreateCompilation(source.Replace("NumberBanana", "NumberTwo")); + driver = driver.RunGenerators(edited, TestContext.Current.CancellationToken); + ImmutableArray result = driver.GetRunResult().Diagnostics; + + Assert.Multiple( + () => Assert.Contains(initial, diagnostic => diagnostic.Id == "DQ0007"), + () => Assert.Empty(result)); + } + + private static ImmutableArray Generate(string source) + { + GeneratorDriver driver = CSharpGeneratorDriver.Create([new QuantityGenerator().AsSourceGenerator()], parseOptions: new CSharpParseOptions(LanguageVersion.Preview)); + driver = driver.RunGenerators(CreateCompilation(source), TestContext.Current.CancellationToken); + return driver.GetRunResult().Diagnostics; + } + + private static CSharpCompilation CreateCompilation(string source) + { + 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)); + return CSharpCompilation.Create("Consumer", [CSharpSyntaxTree.ParseText(source, + new CSharpParseOptions(LanguageVersion.Preview), "Consumer.cs", cancellationToken: TestContext.Current.CancellationToken)], + references, new(OutputKind.DynamicallyLinkedLibrary)); + } +} diff --git a/src/DimensionalQuantities.Roslyn.Tests/Generators/QuantityGeneratorTests.cs b/src/DimensionalQuantities.Roslyn.Tests/Generators/QuantityGeneratorTests.cs index 6f2c6a7..674b1c7 100644 --- a/src/DimensionalQuantities.Roslyn.Tests/Generators/QuantityGeneratorTests.cs +++ b/src/DimensionalQuantities.Roslyn.Tests/Generators/QuantityGeneratorTests.cs @@ -752,8 +752,8 @@ public static class Consumer [Theory] [InlineData("Two")] - [InlineData("NumberNotANumber")] - public void UnsupportedExponentNamesRemainCompilerErrors(string name) + [InlineData("NumberNotANumber", "DQ0007")] + public void UnsupportedExponentNamesRemainCompilerErrors(string name, params string[] expectedDiagnosticIds) { string source = $$""" using DimensionalQuantities.TypeSystem; @@ -767,7 +767,7 @@ public static class Consumer driver.RunGeneratorsAndUpdateCompilation(input, out Compilation output, out ImmutableArray diagnostics, TestContext.Current.CancellationToken); Assert.Multiple( - () => Assert.Empty(diagnostics), + () => Assert.Equal(expectedDiagnosticIds, diagnostics.Select(diagnostic => diagnostic.Id)), () => Assert.Contains(output.GetDiagnostics(TestContext.Current.CancellationToken), diagnostic => diagnostic.Id == "CS0246" && diagnostic.GetMessage().Contains(name, StringComparison.Ordinal))); } diff --git a/src/DimensionalQuantities.Roslyn/AnalyzerReleases.Unshipped.md b/src/DimensionalQuantities.Roslyn/AnalyzerReleases.Unshipped.md index 3a1a39c..ec46be7 100644 --- a/src/DimensionalQuantities.Roslyn/AnalyzerReleases.Unshipped.md +++ b/src/DimensionalQuantities.Roslyn/AnalyzerReleases.Unshipped.md @@ -7,3 +7,5 @@ DQ0002 | DimensionalQuantities | Error | DimensionMismatchAnalyzer DQ0003 | DimensionalQuantities | Info | QuantityEncodingAnalyzer DQ0004 | DimensionalQuantities | Warning | QuantityEncodingAnalyzer DQ0005 | DimensionalQuantities | Error | QuantityEncodingAnalyzer +DQ0006 | DimensionalQuantities | Warning | QuantityGenerator +DQ0007 | DimensionalQuantities | Error | QuantityGenerator diff --git a/src/DimensionalQuantities.Roslyn/Discovery/EnglishExponentDiscovery.cs b/src/DimensionalQuantities.Roslyn/Discovery/EnglishExponentDiscovery.cs index f78fab9..86bb8ee 100644 --- a/src/DimensionalQuantities.Roslyn/Discovery/EnglishExponentDiscovery.cs +++ b/src/DimensionalQuantities.Roslyn/Discovery/EnglishExponentDiscovery.cs @@ -19,11 +19,27 @@ public EnglishExponentDiscovery(SemanticModel semanticModel) public bool TryRead(InvocationExpressionSyntax invocation, out EnglishExponentAlias alias, CancellationToken cancellationToken = default) { - cancellationToken.ThrowIfCancellationRequested(); alias = default; + if (!TryGetUnresolvedRequest(invocation, out IdentifierNameSyntax? exponentName, cancellationToken) + || ExplicitAliasReservation.IsReserved(_semanticModel.Compilation, exponentName.Identifier.ValueText, cancellationToken) + || !TryParseExponent(exponentName, out Rational value)) + { + return false; + } + + Type encoded = TypeSystemNumberCodec.CreateType(value); + alias = new(exponentName.Identifier.ValueText, value, TypeSystemTypeNameFormatter.Format(encoded)); + return true; + } + + public bool TryGetUnresolvedRequest(InvocationExpressionSyntax invocation, [NotNullWhen(true)] out IdentifierNameSyntax? exponentName, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + exponentName = null; if (invocation.Expression is not MemberAccessExpressionSyntax { Name.Identifier.ValueText: "Of" } member || invocation.ArgumentList.Arguments.Count != 1 - || !TryGetExponentName(member.Expression, out IdentifierNameSyntax? exponentName)) + || !TryGetExponentName(member.Expression, out exponentName)) { return false; } @@ -39,18 +55,17 @@ public bool TryRead(InvocationExpressionSyntax invocation, out EnglishExponentAl string name = exponentName.Identifier.ValueText; if (exponentSymbol.Symbol is not null || !exponentSymbol.CandidateSymbols.IsEmpty || _semanticModel.GetAliasInfo(exponentName, cancellationToken) is not null - || !name.StartsWith("Number", StringComparison.Ordinal) - || ExplicitAliasReservation.IsReserved(_semanticModel.Compilation, name, cancellationToken) - || !EnglishRationalParser.TryParse(name.Substring("Number".Length), out Rational value)) + || !name.StartsWith("Number", StringComparison.Ordinal)) { return false; } - Type encoded = TypeSystemNumberCodec.CreateType(value); - alias = new(name, value, TypeSystemTypeNameFormatter.Format(encoded)); return true; } + public static bool TryParseExponent(IdentifierNameSyntax name, out Rational value) => + EnglishRationalParser.TryParse(name.Identifier.ValueText.Substring("Number".Length), out value); + private static bool TryGetExponentName(ExpressionSyntax receiver, [NotNullWhen(true)] out IdentifierNameSyntax? exponentName) { GenericNameSyntax? powerName = receiver switch diff --git a/src/DimensionalQuantities.Roslyn/Discovery/PlannedQuantity.cs b/src/DimensionalQuantities.Roslyn/Discovery/PlannedQuantity.cs index 3fe2669..76e7a30 100644 --- a/src/DimensionalQuantities.Roslyn/Discovery/PlannedQuantity.cs +++ b/src/DimensionalQuantities.Roslyn/Discovery/PlannedQuantity.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using DimensionalQuantities.Dimensions; +using DimensionalQuantities.Naming; using DimensionalQuantities.Units; namespace DimensionalQuantities.Roslyn.Discovery; @@ -7,4 +8,7 @@ namespace DimensionalQuantities.Roslyn.Discovery; internal sealed record PlannedQuantity(Dimension Dimension, ImmutableArray Units, bool IncludeNamedConversions) { public string Name => Units[0].QuantityName; + + public ImmutableArray Aliases => + [.. Units.Select(unit => unit.QuantityName).Append(DimensionNameGenerator.GetQuantityName(Dimension)).Distinct(StringComparer.Ordinal)]; } diff --git a/src/DimensionalQuantities.Roslyn/Discovery/QuantityDiscoveryCoordinator.cs b/src/DimensionalQuantities.Roslyn/Discovery/QuantityDiscoveryCoordinator.cs index 4af99c0..a6e779c 100644 --- a/src/DimensionalQuantities.Roslyn/Discovery/QuantityDiscoveryCoordinator.cs +++ b/src/DimensionalQuantities.Roslyn/Discovery/QuantityDiscoveryCoordinator.cs @@ -15,7 +15,7 @@ public static QuantityDiscoveryResult Discover(CSharpCompilation compilation, Im cancellationToken.ThrowIfCancellationRequested(); if (inputTrees.IsEmpty) { - return new(compilation, []); + return new(compilation, [], defaults, []); } ImmutableArray aliases = DiscoverySeedCollector.FindEnglishAliases(compilation, inputTrees, cancellationToken); @@ -41,7 +41,7 @@ public static QuantityDiscoveryResult Discover(CSharpCompilation compilation, Im if (!changed) { - return new(bound, sources); + return new(bound, sources, plan, aliases); } } diff --git a/src/DimensionalQuantities.Roslyn/Discovery/QuantityDiscoveryResult.cs b/src/DimensionalQuantities.Roslyn/Discovery/QuantityDiscoveryResult.cs index f722392..38886d4 100644 --- a/src/DimensionalQuantities.Roslyn/Discovery/QuantityDiscoveryResult.cs +++ b/src/DimensionalQuantities.Roslyn/Discovery/QuantityDiscoveryResult.cs @@ -4,4 +4,5 @@ namespace DimensionalQuantities.Roslyn.Discovery; -internal sealed record QuantityDiscoveryResult(CSharpCompilation Compilation, ImmutableArray Sources); +internal sealed record QuantityDiscoveryResult(CSharpCompilation Compilation, ImmutableArray Sources, + QuantityGenerationPlan Plan, ImmutableArray EnglishAliases); diff --git a/src/DimensionalQuantities.Roslyn/Generators/AliasCollisionDiagnostics.cs b/src/DimensionalQuantities.Roslyn/Generators/AliasCollisionDiagnostics.cs new file mode 100644 index 0000000..85c8366 --- /dev/null +++ b/src/DimensionalQuantities.Roslyn/Generators/AliasCollisionDiagnostics.cs @@ -0,0 +1,101 @@ +using System.Collections.Immutable; +using DimensionalQuantities.Roslyn.Discovery; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace DimensionalQuantities.Roslyn.Generators; + +internal static class AliasCollisionDiagnostics +{ + public static ImmutableArray Find(CSharpCompilation compilation, QuantityDiscoveryResult result, + ImmutableArray inputTrees, CancellationToken cancellationToken) + { + IEnumerable names = result.Plan.KnownQuantities.Concat(result.Plan.AdditionalQuantities) + .SelectMany(quantity => quantity.Aliases).Concat(result.EnglishAliases.Select(alias => alias.Name)); + HashSet generatedNames = new(names, StringComparer.Ordinal); + ImmutableArray.Builder diagnostics = ImmutableArray.CreateBuilder(); + CSharpCompilation prior = compilation.RemoveSyntaxTrees(compilation.SyntaxTrees.Except(inputTrees)); + foreach (SyntaxTree tree in inputTrees) + { + SyntaxNode root = tree.GetRoot(cancellationToken); + HashSet declaredAliases = FindExplicitAliases(root, generatedNames, diagnostics); + FindGlobalTypes(root, generatedNames, diagnostics); + FindDisplacedTypeUses(root, generatedNames, declaredAliases, prior.GetSemanticModel(tree), + result.Compilation.GetSemanticModel(tree), diagnostics, cancellationToken); + } + + return diagnostics.ToImmutable(); + } + + private static HashSet FindExplicitAliases(SyntaxNode root, HashSet generatedNames, + ImmutableArray.Builder diagnostics) + { + HashSet declaredAliases = []; + foreach (UsingDirectiveSyntax directive in root.DescendantNodes().OfType()) + { + if (directive.Alias is { Name: IdentifierNameSyntax name } + && generatedNames.Contains(name.Identifier.ValueText)) + { + declaredAliases.Add(name.Identifier.ValueText); + Add(diagnostics, name.Identifier, name.GetLocation()); + } + } + + return declaredAliases; + } + + private static void FindGlobalTypes(SyntaxNode root, HashSet generatedNames, + ImmutableArray.Builder diagnostics) + { + foreach (BaseTypeDeclarationSyntax declaration in root.DescendantNodes().OfType()) + { + if (declaration.Parent is CompilationUnitSyntax + && declaration is not TypeDeclarationSyntax { TypeParameterList.Parameters.Count: > 0 } + && generatedNames.Contains(declaration.Identifier.ValueText)) + { + Add(diagnostics, declaration.Identifier, declaration.Identifier.GetLocation()); + } + } + + foreach (DelegateDeclarationSyntax declaration in root.DescendantNodes().OfType()) + { + if (declaration.Parent is CompilationUnitSyntax && declaration.TypeParameterList is null + && generatedNames.Contains(declaration.Identifier.ValueText)) + { + Add(diagnostics, declaration.Identifier, declaration.Identifier.GetLocation()); + } + } + } + + private static void FindDisplacedTypeUses(SyntaxNode root, HashSet generatedNames, HashSet declaredAliases, + SemanticModel prior, SemanticModel generated, ImmutableArray.Builder diagnostics, CancellationToken cancellationToken) + { + foreach (IdentifierNameSyntax name in root.DescendantNodes().OfType()) + { + cancellationToken.ThrowIfCancellationRequested(); + if (!generatedNames.Contains(name.Identifier.ValueText) || declaredAliases.Contains(name.Identifier.ValueText) + || name.Parent is QualifiedNameSyntax or AliasQualifiedNameSyntax + || !IsTypeUse(name, prior, cancellationToken)) + { + continue; + } + + ITypeSymbol? before = prior.GetTypeInfo(name, cancellationToken).Type; + ITypeSymbol? after = generated.GetTypeInfo(name, cancellationToken).Type; + if (before is INamedTypeSymbol { Arity: 0 } and not IErrorTypeSymbol && after is not null + && !SymbolEqualityComparer.Default.Equals(before, after)) + { + Add(diagnostics, name.Identifier, name.GetLocation()); + } + } + } + + private static bool IsTypeUse(IdentifierNameSyntax name, SemanticModel model, CancellationToken cancellationToken) => + SyntaxFacts.IsInTypeOnlyContext(name) + || name.Parent is MemberAccessExpressionSyntax access && ReferenceEquals(access.Expression, name) + && model.GetSymbolInfo(name, cancellationToken).Symbol is INamedTypeSymbol; + + private static void Add(ImmutableArray.Builder diagnostics, SyntaxToken identifier, Location location) => + diagnostics.Add(Diagnostic.Create(GenerationDiagnostics.AliasCollision, location, identifier.ValueText)); +} diff --git a/src/DimensionalQuantities.Roslyn/Generators/GenerationDiagnostics.cs b/src/DimensionalQuantities.Roslyn/Generators/GenerationDiagnostics.cs new file mode 100644 index 0000000..e1c2d81 --- /dev/null +++ b/src/DimensionalQuantities.Roslyn/Generators/GenerationDiagnostics.cs @@ -0,0 +1,39 @@ +using System.Collections.Immutable; +using DimensionalQuantities.Roslyn.Discovery; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace DimensionalQuantities.Roslyn.Generators; + +internal static class GenerationDiagnostics +{ + public static readonly DiagnosticDescriptor AliasCollision = new( + "DQ0006", "Generated alias conflicts with a type name", "Generated alias '{0}' conflicts with a name in this source", + "DimensionalQuantities", DiagnosticSeverity.Warning, isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor InvalidEnglishExponent = new( + "DQ0007", "Invalid English exponent", "English exponent '{0}' cannot be resolved", + "DimensionalQuantities", DiagnosticSeverity.Error, isEnabledByDefault: true); + + public static ImmutableArray FindInvalidEnglishExponents(CSharpCompilation compilation, + ImmutableArray inputTrees, CancellationToken cancellationToken) + { + ImmutableArray.Builder diagnostics = ImmutableArray.CreateBuilder(); + foreach (SyntaxTree tree in inputTrees) + { + EnglishExponentDiscovery discovery = new(compilation.GetSemanticModel(tree)); + foreach (InvocationExpressionSyntax invocation in tree.GetRoot(cancellationToken).DescendantNodes().OfType()) + { + cancellationToken.ThrowIfCancellationRequested(); + if (discovery.TryGetUnresolvedRequest(invocation, out IdentifierNameSyntax? name, cancellationToken) + && !EnglishExponentDiscovery.TryParseExponent(name, out _)) + { + diagnostics.Add(Diagnostic.Create(InvalidEnglishExponent, name.GetLocation(), name.Identifier.ValueText)); + } + } + } + + return diagnostics.ToImmutable(); + } +} diff --git a/src/DimensionalQuantities.Roslyn/Generators/QuantityGenerator.cs b/src/DimensionalQuantities.Roslyn/Generators/QuantityGenerator.cs index 22b4dd6..55a51f7 100644 --- a/src/DimensionalQuantities.Roslyn/Generators/QuantityGenerator.cs +++ b/src/DimensionalQuantities.Roslyn/Generators/QuantityGenerator.cs @@ -30,8 +30,19 @@ public void Initialize(IncrementalGeneratorInitializationContext context) IncrementalValueProvider<(Compilation Left, ImmutableArray Right)> inputs = context.CompilationProvider.Combine(inputTrees.Collect()); context.RegisterSourceOutput(inputs, (output, input) => { - QuantityDiscoveryResult result = QuantityDiscoveryCoordinator.Discover((CSharpCompilation)input.Left, + CSharpCompilation original = (CSharpCompilation)input.Left; + foreach (Diagnostic diagnostic in GenerationDiagnostics.FindInvalidEnglishExponents(original, input.Right, output.CancellationToken)) + { + output.ReportDiagnostic(diagnostic); + } + + QuantityDiscoveryResult result = QuantityDiscoveryCoordinator.Discover(original, input.Right, defaultPlan, output.CancellationToken); + foreach (Diagnostic diagnostic in AliasCollisionDiagnostics.Find(original, result, input.Right, output.CancellationToken)) + { + output.ReportDiagnostic(diagnostic); + } + foreach (GeneratedSource source in result.Sources) { output.AddSource(source.HintName, SourceText.From(source.Text, Encoding.UTF8)); diff --git a/src/DimensionalQuantities.Roslyn/Generators/QuantitySourceEmitter.cs b/src/DimensionalQuantities.Roslyn/Generators/QuantitySourceEmitter.cs index d2facb9..653129c 100644 --- a/src/DimensionalQuantities.Roslyn/Generators/QuantitySourceEmitter.cs +++ b/src/DimensionalQuantities.Roslyn/Generators/QuantitySourceEmitter.cs @@ -33,7 +33,7 @@ private static QuantityDefinition DefineQuantity(PlannedQuantity quantity, int[] Dimension dimension = quantity.Dimension; IReadOnlyList units = quantity.Units; string typeName = $"global::DimensionalQuantities.TypeSystem.Quantity>"; - string[] aliases = units.Select(unit => unit.QuantityName).Append(DimensionNameGenerator.GetQuantityName(dimension)).Distinct(StringComparer.Ordinal).ToArray(); + string[] aliases = quantity.Aliases.ToArray(); string quantityName = units[0].QuantityName; UnitExpression canonicalExpression = units[0].Expression; ConversionDefinition genericConversion = new("FromPrefixedUnits", "AsPrefixedUnits", GetTermParameters(canonicalExpression), GetTermScaleExponent(canonicalExpression));