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
Original file line number Diff line number Diff line change
Expand Up @@ -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<ArithmeticOperation> 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<ArithmeticOperation> result = Analyse(compilation);

Assert.Empty(result);
}

private static ImmutableArray<ArithmeticOperation> Analyse(CSharpCompilation compilation)
{
SyntaxTree tree = compilation.SyntaxTrees.Single(candidate => candidate.FilePath == "Consumer.cs");
Expand All @@ -706,7 +739,7 @@ private static ImmutableArray<ArithmeticOperation> 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<PortableExecutableReference> references = assemblies.Select(path => MetadataReference.CreateFromFile(path))
Expand All @@ -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));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<double>(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<Diagnostic> 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ExpressionSyntax, QuantityOperand> _analyse;
private readonly Func<ExpressionSyntax, string, QuantityOperand> _analyseNamed;
private readonly CancellationToken _cancellationToken;
private readonly Dictionary<ISymbol, QuantityOperand> _resolved = new(SymbolEqualityComparer.Default);
private readonly HashSet<ISymbol> _inProgress = new(SymbolEqualityComparer.Default);

public QuantityDeclarationResolver(Compilation compilation, Func<ExpressionSyntax, QuantityOperand> analyse,
Func<ExpressionSyntax, string, QuantityOperand> 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;
}
}
Loading
Loading