diff --git a/src/DimensionalQuantities.Roslyn.Tests/Discovery/QuantityExpressionAnalyserTests.cs b/src/DimensionalQuantities.Roslyn.Tests/Discovery/QuantityExpressionAnalyserTests.cs index 0bdf820..09f49f8 100644 --- a/src/DimensionalQuantities.Roslyn.Tests/Discovery/QuantityExpressionAnalyserTests.cs +++ b/src/DimensionalQuantities.Roslyn.Tests/Discovery/QuantityExpressionAnalyserTests.cs @@ -697,6 +697,39 @@ object Test(bool choose, Length length, Current current, Time time) Assert.Equal(expected, result); } + [Theory] + [InlineData("public static LengthCurrent Stored = Length.FromMetres(2) * Current.FromAmps(3);")] + [InlineData("public static LengthCurrent Stored => Length.FromMetres(2) * Current.FromAmps(3);")] + [InlineData("public static LengthCurrent Stored { get => Length.FromMetres(2) * Current.FromAmps(3); }")] + [InlineData("public static LengthCurrent Stored { get; } = Length.FromMetres(2) * Current.FromAmps(3);")] + public void FollowsFieldAndPropertyDefinitionsAcrossFiles(string declaration) + { + const string source = "class Consumer { object Test(Time time) => Inputs.Stored * time; }"; + string memberSource = $$"""public class Inputs { {{declaration}} }"""; + CSharpCompilation compilation = Compile(source, memberSource); + ArithmeticOperation expected = new(ArithmeticOperationKind.Multiply, new(Length: 1, ElectricCurrent: 1), new(Time: 1), new(Length: 1, Time: 1, ElectricCurrent: 1)); + + ImmutableArray result = Analyse(compilation); + + Assert.Contains(expected, result); + } + + [Theory] + [InlineData("public static object Stored => Length.FromMetres(2) * Current.FromAmps(3);")] + [InlineData("public static Missing Stored => Length.FromMetres(2) * Current.FromAmps(3);")] + [InlineData("public static LengthTime Stored => Length.FromMetres(2) * Current.FromAmps(3);")] + [InlineData("public static LengthCurrent Stored => Other; public static LengthCurrent Other => Stored;")] + public void DoesNotInferUnrelatedMismatchedOrCyclicMemberDefinitions(string declaration) + { + const string source = "class Consumer { object Test(Time time) => Inputs.Stored * time; }"; + string memberSource = $$"""public class Inputs { {{declaration}} }"""; + CSharpCompilation compilation = Compile(source, memberSource); + + ImmutableArray result = Analyse(compilation); + + Assert.Empty(result); + } + private static ImmutableArray Analyse(CSharpCompilation compilation) { SyntaxTree tree = compilation.SyntaxTrees.Single(candidate => candidate.FilePath == "Consumer.cs"); @@ -706,7 +739,7 @@ private static ImmutableArray Analyse(CSharpCompilation com return analyser.Analyse(expressionBody.Expression, TestContext.Current.CancellationToken).ArithmeticOperations; } - private static CSharpCompilation Compile(string source) + private static CSharpCompilation Compile(string source, params string[] additionalSources) { string[] assemblies = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!).Split(Path.PathSeparator); IEnumerable references = assemblies.Select(path => MetadataReference.CreateFromFile(path)) @@ -715,7 +748,8 @@ private static CSharpCompilation Compile(string source) SyntaxTree knownQuantities = CSharpSyntaxTree.ParseText(QuantitySourceEmitter.Emit(plan.KnownQuantities), _parseOptions, path: "KnownQuantities.g.cs"); return CSharpCompilation.Create("DiscoveryConsumer", - [CSharpSyntaxTree.ParseText(source, _parseOptions, path: "Consumer.cs"), knownQuantities], references, + [CSharpSyntaxTree.ParseText(source, _parseOptions, path: "Consumer.cs"), knownQuantities, + .. additionalSources.Select((text, index) => CSharpSyntaxTree.ParseText(text, _parseOptions, path: $"Additional{index}.cs", cancellationToken: TestContext.Current.CancellationToken))], references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); } diff --git a/src/DimensionalQuantities.Roslyn.Tests/Generators/QuantityGeneratorTests.cs b/src/DimensionalQuantities.Roslyn.Tests/Generators/QuantityGeneratorTests.cs index b48d7ca..c7f0524 100644 --- a/src/DimensionalQuantities.Roslyn.Tests/Generators/QuantityGeneratorTests.cs +++ b/src/DimensionalQuantities.Roslyn.Tests/Generators/QuantityGeneratorTests.cs @@ -1220,6 +1220,46 @@ object Value(bool choose, Length length, Current current, Time time) () => Assert.NotNull(result.GetTypeByMetadataName("DimensionalQuantities.Generated.LengthTimeSquaredExtensions"))); } + [Theory] + [InlineData("public LengthCurrent Stored = Length.FromMetres(2) * Current.FromAmps(3);")] + [InlineData("public LengthCurrent Stored => Length.FromMetres(2) * Current.FromAmps(3);")] + [InlineData("public LengthCurrent Stored { get => Length.FromMetres(2) * Current.FromAmps(3); }")] + [InlineData("public LengthCurrent Stored { get; } = Length.FromMetres(2) * Current.FromAmps(3);")] + [InlineData("private LengthCurrent Seed = Length.FromMetres(2) * Current.FromAmps(3); public LengthCurrent Stored => Seed;")] + public void IntermediateFieldsAndPropertiesComposeWithArithmetic(string declaration) + { + string source = $$""" + public class Inputs { {{declaration}} } + public static class Consumer + { + public static double Value() => (new Inputs().Stored * Time.FromSeconds(4)).AsSiBaseUnit(); + } + """; + double result = EvaluateSource(source); + + Assert.Equal(24, result); + } + + [Fact] + public void EditingMemberDefinitionRemovesObsoleteDependentOperations() + { + const string source = "class Inputs { public static LengthCurrent Stored => Length.FromMetres(2) * Current.FromAmps(3); }"; + const string consumer = "class Consumer { object Value(Time time) => Inputs.Stored * time; }"; + string edited = source.Replace("LengthCurrent", "LengthTime", StringComparison.Ordinal).Replace("Current.FromAmps", "Time.FromSeconds", StringComparison.Ordinal); + SyntaxTree memberTree = CSharpSyntaxTree.ParseText(source, _parseOptions, path: "Inputs.cs", cancellationToken: TestContext.Current.CancellationToken); + CSharpCompilation input = CreateInputCompilation(consumer).AddSyntaxTrees(memberTree); + GeneratorDriver driver = CSharpGeneratorDriver.Create([new QuantityGenerator().AsSourceGenerator()], parseOptions: _parseOptions); + driver = driver.RunGeneratorsAndUpdateCompilation(input, out _, out _, TestContext.Current.CancellationToken); + SyntaxTree replacement = CSharpSyntaxTree.ParseText(edited, _parseOptions, path: "Inputs.cs", cancellationToken: TestContext.Current.CancellationToken); + driver.RunGeneratorsAndUpdateCompilation(input.ReplaceSyntaxTree(memberTree, replacement), out Compilation result, out ImmutableArray diagnostics, TestContext.Current.CancellationToken); + + Assert.Multiple( + () => Assert.Empty(diagnostics), + () => Assert.Empty(result.GetDiagnostics(TestContext.Current.CancellationToken).Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)), + () => Assert.Null(result.GetTypeByMetadataName("DimensionalQuantities.Generated.LengthTimeCurrentExtensions")), + () => Assert.NotNull(result.GetTypeByMetadataName("DimensionalQuantities.Generated.LengthTimeSquaredExtensions"))); + } + private static string[] ReadConvenienceMethods(Compilation compilation, Dimension dimension) { QuantityTypeReader reader = new(compilation); diff --git a/src/DimensionalQuantities.Roslyn/Discovery/QuantityDeclarationResolver.cs b/src/DimensionalQuantities.Roslyn/Discovery/QuantityDeclarationResolver.cs new file mode 100644 index 0000000..8bbc1f1 --- /dev/null +++ b/src/DimensionalQuantities.Roslyn/Discovery/QuantityDeclarationResolver.cs @@ -0,0 +1,99 @@ +using DimensionalQuantities.Naming; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace DimensionalQuantities.Roslyn.Discovery; + +internal sealed class QuantityDeclarationResolver +{ + private readonly Compilation _compilation; + private readonly Func _analyse; + private readonly Func _analyseNamed; + private readonly CancellationToken _cancellationToken; + private readonly Dictionary _resolved = new(SymbolEqualityComparer.Default); + private readonly HashSet _inProgress = new(SymbolEqualityComparer.Default); + + public QuantityDeclarationResolver(Compilation compilation, Func analyse, + Func analyseNamed, CancellationToken cancellationToken) + { + _compilation = compilation; + _analyse = analyse; + _analyseNamed = analyseNamed; + _cancellationToken = cancellationToken; + } + + public QuantityOperand Resolve(ISymbol symbol) + { + if (symbol is not ILocalSymbol and not IFieldSymbol and not IPropertySymbol) + { + return default; + } + + if (_resolved.TryGetValue(symbol, out QuantityOperand cached)) + { + return cached; + } + + if (!_inProgress.Add(symbol)) + { + return default; + } + + try + { + QuantityOperand result = AnalyseDeclaration(symbol); + _resolved.Add(symbol, result); + return result; + } + finally + { + _inProgress.Remove(symbol); + } + } + + public static bool MatchesExpectedName(string typeName, QuantityOperand inferred) => inferred.Kind == QuantityOperandKind.Quantity + && typeName == DimensionNameGenerator.GetQuantityName(inferred.Dimension); + + private QuantityOperand AnalyseDeclaration(ISymbol symbol) + { + SyntaxNode? syntax = symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(_cancellationToken); + return syntax switch + { + VariableDeclaratorSyntax variable when variable.Parent is VariableDeclarationSyntax declaration + && variable.Initializer is { Value: ExpressionSyntax initializer } + => AnalyseInitializer(declaration.Type, initializer), + PropertyDeclarationSyntax property when GetPropertyValue(property) is ExpressionSyntax value + => AnalyseInitializer(property.Type, value), + _ => default + }; + } + + private QuantityOperand AnalyseInitializer(TypeSyntax type, ExpressionSyntax initializer) + { + SemanticModel model = _compilation.GetSemanticModel(type.SyntaxTree); + if (type.IsVar && model.GetSymbolInfo(type, _cancellationToken).Symbol is null) + { + return _analyse(initializer); + } + + if (type is not IdentifierNameSyntax name + || model.GetTypeInfo(name, _cancellationToken).Type is not IErrorTypeSymbol + || model.GetSymbolInfo(name, _cancellationToken) is not { Symbol: null, CandidateSymbols.IsEmpty: true }) + { + return default; + } + + return _analyseNamed(initializer, name.Identifier.ValueText); + } + + private static ExpressionSyntax? GetPropertyValue(PropertyDeclarationSyntax property) + { + if (property.ExpressionBody is { Expression: ExpressionSyntax expression }) + { + return expression; + } + + AccessorDeclarationSyntax? getter = property.AccessorList?.Accessors.FirstOrDefault(accessor => accessor.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.GetAccessorDeclaration)); + return getter?.ExpressionBody?.Expression ?? property.Initializer?.Value; + } +} diff --git a/src/DimensionalQuantities.Roslyn/Discovery/QuantityExpressionAnalyser.cs b/src/DimensionalQuantities.Roslyn/Discovery/QuantityExpressionAnalyser.cs index 6bbc2dc..5524183 100644 --- a/src/DimensionalQuantities.Roslyn/Discovery/QuantityExpressionAnalyser.cs +++ b/src/DimensionalQuantities.Roslyn/Discovery/QuantityExpressionAnalyser.cs @@ -1,6 +1,5 @@ using System.Collections.Immutable; using DimensionalQuantities.Dimensions; -using DimensionalQuantities.Naming; using DimensionalQuantities.Numerics; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -27,22 +26,20 @@ public QuantityExpressionAnalysis Analyse(ExpressionSyntax expression, Cancellat private sealed class Analysis { private readonly SemanticModel _semanticModel; + private readonly Dictionary _semanticModels = []; private readonly QuantityTypeReader _typeReader; - private readonly PowerDiscovery _powerDiscovery; - private readonly PowerMethodDiscovery _powerMethodDiscovery; + private readonly QuantityDeclarationResolver _declarations; private readonly CancellationToken _cancellationToken; private readonly ImmutableArray.Builder _operations = ImmutableArray.CreateBuilder(); private readonly ImmutableArray.Builder _powerOperations = ImmutableArray.CreateBuilder(); private readonly ImmutableArray.Builder _powerMethodOperations = ImmutableArray.CreateBuilder(); - private readonly Dictionary _locals = new(SymbolEqualityComparer.Default); - private readonly HashSet _inProgress = new(SymbolEqualityComparer.Default); public Analysis(SemanticModel semanticModel, CancellationToken cancellationToken) { _semanticModel = semanticModel; + _semanticModels.Add(semanticModel.SyntaxTree, semanticModel); _typeReader = new(semanticModel.Compilation); - _powerDiscovery = new(semanticModel); - _powerMethodDiscovery = new(semanticModel); + _declarations = new(semanticModel.Compilation, AnalyseExpression, AnalyseNamedInitializer, cancellationToken); _cancellationToken = cancellationToken; } @@ -52,7 +49,7 @@ public QuantityExpressionAnalysis Analyse(ExpressionSyntax expression) return new(_operations.ToImmutable(), _powerOperations.ToImmutable(), _powerMethodOperations.ToImmutable()); } - private Operand AnalyseExpression(ExpressionSyntax expression) + private QuantityOperand AnalyseExpression(ExpressionSyntax expression) { _cancellationToken.ThrowIfCancellationRequested(); if (expression is ParenthesizedExpressionSyntax parenthesized) @@ -71,38 +68,38 @@ private Operand AnalyseExpression(ExpressionSyntax expression) } if (expression is InvocationExpressionSyntax invocation - && _powerDiscovery.TryRead(invocation, out Rational exponent, out string exponentTypeName, _cancellationToken)) + && new PowerDiscovery(GetSemanticModel(invocation)).TryRead(invocation, out Rational exponent, out string exponentTypeName, _cancellationToken)) { - Operand operand = AnalyseExpression(invocation.ArgumentList.Arguments[0].Expression); - if (operand.Kind != OperandKind.Quantity) + QuantityOperand operand = AnalyseExpression(invocation.ArgumentList.Arguments[0].Expression); + if (operand.Kind != QuantityOperandKind.Quantity) { return default; } Dimension result = operand.Dimension.Pow(exponent); _powerOperations.Add(new(operand.Dimension, exponent, result, exponentTypeName)); - return result == default ? new(OperandKind.Scalar) : new(OperandKind.Quantity, result); + return result == default ? new(QuantityOperandKind.Scalar) : new(QuantityOperandKind.Quantity, result); } if (expression is InvocationExpressionSyntax methodInvocation - && _powerMethodDiscovery.TryRead(methodInvocation, out Rational methodExponent, out string methodName, _cancellationToken)) + && new PowerMethodDiscovery(GetSemanticModel(methodInvocation)).TryRead(methodInvocation, out Rational methodExponent, out string methodName, _cancellationToken)) { MemberAccessExpressionSyntax access = (MemberAccessExpressionSyntax)methodInvocation.Expression; - Operand operand = AnalyseExpression(access.Expression); - if (operand.Kind != OperandKind.Quantity) + QuantityOperand operand = AnalyseExpression(access.Expression); + if (operand.Kind != QuantityOperandKind.Quantity) { return default; } Dimension result = operand.Dimension.Pow(methodExponent); _powerMethodOperations.Add(new(operand.Dimension, methodExponent, result, methodName)); - return result == default ? new(OperandKind.Scalar) : new(OperandKind.Quantity, result); + return result == default ? new(QuantityOperandKind.Scalar) : new(QuantityOperandKind.Quantity, result); } if (expression is BinaryExpressionSyntax binary && IsSupported(binary)) { - Operand left = AnalyseExpression(binary.Left); - Operand right = AnalyseExpression(binary.Right); + QuantityOperand left = AnalyseExpression(binary.Left); + QuantityOperand right = AnalyseExpression(binary.Right); if (binary.IsKind(SyntaxKind.AddExpression) || binary.IsKind(SyntaxKind.SubtractExpression)) { return AnalyseSum(binary, left, right); @@ -113,17 +110,17 @@ private Operand AnalyseExpression(ExpressionSyntax expression) if (expression is PrefixUnaryExpressionSyntax unary && unary.IsKind(SyntaxKind.UnaryMinusExpression)) { - Operand operand = AnalyseExpression(unary.Operand); - Operand bound = ClassifyLeaf(unary); - return bound.Kind != OperandKind.Unsupported ? bound : operand; + QuantityOperand operand = AnalyseExpression(unary.Operand); + QuantityOperand bound = ClassifyLeaf(unary); + return bound.Kind != QuantityOperandKind.Unsupported ? bound : operand; } return ClassifyLeaf(expression); } - private Operand AnalyseBranches(ExpressionSyntax expression, IEnumerable arms) + private QuantityOperand AnalyseBranches(ExpressionSyntax expression, IEnumerable arms) { - Operand common = default; + QuantityOperand common = default; bool hasValue = false; bool compatible = true; foreach (ExpressionSyntax arm in arms) @@ -133,20 +130,20 @@ private Operand AnalyseBranches(ExpressionSyntax expression, IEnumerable binary.IsKind(SyntaxKind.MultiplyExpression) || binary.IsKind(SyntaxKind.DivideExpression) || binary.IsKind(SyntaxKind.AddExpression) || binary.IsKind(SyntaxKind.SubtractExpression); - private enum OperandKind - { - Unsupported, - Scalar, - Quantity - } - - private readonly record struct Operand(OperandKind Kind, Dimension Dimension = default); } } diff --git a/src/DimensionalQuantities.Roslyn/Discovery/QuantityOperand.cs b/src/DimensionalQuantities.Roslyn/Discovery/QuantityOperand.cs new file mode 100644 index 0000000..01415ff --- /dev/null +++ b/src/DimensionalQuantities.Roslyn/Discovery/QuantityOperand.cs @@ -0,0 +1,5 @@ +using DimensionalQuantities.Dimensions; + +namespace DimensionalQuantities.Roslyn.Discovery; + +internal readonly record struct QuantityOperand(QuantityOperandKind Kind, Dimension Dimension = default); diff --git a/src/DimensionalQuantities.Roslyn/Discovery/QuantityOperandKind.cs b/src/DimensionalQuantities.Roslyn/Discovery/QuantityOperandKind.cs new file mode 100644 index 0000000..1cfefd5 --- /dev/null +++ b/src/DimensionalQuantities.Roslyn/Discovery/QuantityOperandKind.cs @@ -0,0 +1,8 @@ +namespace DimensionalQuantities.Roslyn.Discovery; + +internal enum QuantityOperandKind +{ + Unsupported, + Scalar, + Quantity +}