Consider the following trivial C# code:
public class Type1
{
public const int I = 1;
public static void f(){}
}
and
using static Type1;
public class Type2
{
public static string X = nameof(f);
public static int Y = I;
}
My tool identifies all the dependencies between types. Most of the dependencies can be identified by examining the binary code. However, some dependencies exist strictly during compilation, not at runtime. For example, when depending on constant values (like Type1.I) or the nameof directive, which are inlined by the compiler. So nameof(f) turns into "f" and Y = I - to Y = 1.
Syntax analysis alone to identify these dependencies is not enough either. It is doable in trivial cases like that, but in general it is not feasible (unless implementing compiler from scratch).
Which leaves analysis using semantic model. This one is great. It recognizes dependencies on constant values just fine. And most of the nameof dependencies. But it fails when the nameof directive references something brought into the namespace with the using static directive.
Here is the full code that demonstrates it:
private const string CODE_1 = @"public class Type1
{
public const int I = 1;
public static void f(){}
}
";
private const string CODE_2 = @"using static Type1;
public class Type2
{
public static string X = nameof(f);
public static int Y = I;
}
";
private static readonly string[] s_netCoreReferences =
[
typeof(object).Assembly.Location,
Path.GetDirectoryName(typeof(object).Assembly.Location) + "\\System.Runtime.dll",
];
[Test]
public void NameOfOperationWithUsingStatic()
{
SyntaxTree[] syntaxTrees = [CSharpSyntaxTree.ParseText(CODE_1), CSharpSyntaxTree.ParseText(CODE_2)];
var compilation = CSharpCompilation.Create("temp", syntaxTrees,
s_netCoreReferences.Select(o => MetadataReference.CreateFromFile(o)).ToArray(),
new CSharpCompilationOptions(OutputKind.NetModule));
Assert.That(compilation.GetDiagnostics(), Is.Empty);
var model = compilation.GetSemanticModel(syntaxTrees[1]);
var syntaxNode1 = syntaxTrees[1]
.GetCompilationUnitRoot()
.DescendantNodes()
.OfType<IdentifierNameSyntax>()
.FirstOrDefault(o => o.Identifier.ValueText == "nameof");
var nameofOp = (INameOfOperation)model.GetOperation(syntaxNode1.Parent);
Assert.That(nameofOp.Argument.Type, Is.Null);
Assert.That(nameofOp.Argument.ChildOperations, Is.Empty);
var syntaxNode2 = syntaxTrees[1]
.GetCompilationUnitRoot()
.DescendantNodes()
.OfType<FieldDeclarationSyntax>()
.Where(o => o.Declaration.Variables[0].Identifier.ValueText == "Y")
.Select(o => o.Declaration.Variables[0].Initializer.Value)
.FirstOrDefault();
var fieldOp = (IFieldReferenceOperation)model.GetOperation(syntaxNode2);
Assert.That(fieldOp.Field.ContainingType.Name, Is.EqualTo("Type1"));
Assert.That(fieldOp.Field.Name, Is.EqualTo("I"));
}
Notice these asserts:
Assert.That(nameofOp.Argument.Type, Is.Null);
Assert.That(nameofOp.Argument.ChildOperations, Is.Empty);
It indicates that the INameOfOperation returned by the semantic model fails to identify that nameof references the function Type1.f.
The semantic model did work fine for the constant Type1.I, which is captured by the asserts:
Assert.That(fieldOp.Field.ContainingType.Name, Is.EqualTo("Type1"));
Assert.That(fieldOp.Field.Name, Is.EqualTo("I"));
My question is - what can be done about nameof in this particular scenario? I want to emphasize, the semantic model seems to work fine in all the other nameof uses cases.
EDIT 1
It seems that the goal I am after is unclear from my original post. I will try to phrase the question in a more focused way - how can we determine that the Type1.f function is a compile time dependency of Type2 using Roslyn? It should be a workable solution in general, not just for this trivial case. My post gives a solution that works reliably almost always - using the semantic model. But it misses that one case which is the subject of this post.
EDIT 2
Please, find below revised code that demonstrates that semantic model correctly handles qualified members and those referenced through using alias. Leaving only using static as not working:
private const string CODE_1 = @"public class Type1
{
public const int I = 1;
public static void f(){}
}
";
private const string CODE_2 = @"using static Type1;
using Type3 = Type1;
public class Type2
{
public static string X = nameof(f);
public static int Y = I;
public static string Z = nameof(Type1.f);
public static string T = nameof(Type3.f);
}
";
private static readonly string[] s_netCoreReferences =
[
typeof(object).Assembly.Location,
Path.GetDirectoryName(typeof(object).Assembly.Location) + "\\System.Runtime.dll",
];
[Test]
public void NameOfOperationWithUsingStatic()
{
SyntaxTree[] syntaxTrees = [CSharpSyntaxTree.ParseText(CODE_1), CSharpSyntaxTree.ParseText(CODE_2)];
var compilation = CSharpCompilation.Create("temp", syntaxTrees,
s_netCoreReferences.Select(o => MetadataReference.CreateFromFile(o)).ToArray(),
new CSharpCompilationOptions(OutputKind.NetModule));
Assert.That(compilation.GetDiagnostics(), Is.Empty);
var model = compilation.GetSemanticModel(syntaxTrees[1]);
var nameOfNodes = syntaxTrees[1]
.GetCompilationUnitRoot()
.DescendantNodes()
.OfType<IdentifierNameSyntax>()
.Where(o => o.Identifier.ValueText == "nameof")
.ToList();
var nameofOp = (INameOfOperation)model.GetOperation(nameOfNodes[0].Parent);
Assert.That(nameofOp.Syntax.ToString(), Is.EqualTo("nameof(f)"));
Assert.That(nameofOp.Argument.Type, Is.Null);
Assert.That(nameofOp.Argument.ChildOperations, Is.Empty);
nameofOp = (INameOfOperation)model.GetOperation(nameOfNodes[1].Parent);
Assert.That(nameofOp.Syntax.ToString(), Is.EqualTo("nameof(Type1.f)"));
Assert.That(nameofOp.Argument.Type, Is.Null);
Assert.That(nameofOp.Argument.ChildOperations, Has.Count.EqualTo(1));
var op = nameofOp.Argument.ChildOperations.First();
Assert.That(op.Type.Name, Is.EqualTo("Type1"));
nameofOp = (INameOfOperation)model.GetOperation(nameOfNodes[2].Parent);
Assert.That(nameofOp.Syntax.ToString(), Is.EqualTo("nameof(Type3.f)"));
Assert.That(nameofOp.Argument.Type, Is.Null);
Assert.That(nameofOp.Argument.ChildOperations, Has.Count.EqualTo(1));
op = nameofOp.Argument.ChildOperations.First();
Assert.That(op.Type.Name, Is.EqualTo("Type1"));
var syntaxNode = syntaxTrees[1]
.GetCompilationUnitRoot()
.DescendantNodes()
.OfType<FieldDeclarationSyntax>()
.Where(o => o.Declaration.Variables[0].Identifier.ValueText == "Y")
.Select(o => o.Declaration.Variables[0].Initializer.Value)
.FirstOrDefault();
var fieldOp = (IFieldReferenceOperation)model.GetOperation(syntaxNode);
Assert.That(fieldOp.Field.ContainingType.Name, Is.EqualTo("Type1"));
Assert.That(fieldOp.Field.Name, Is.EqualTo("I"));
}
Notice the two more nameof cases:
public static string Z = nameof(Type1.f);
public static string T = nameof(Type3.f);
where Type3 is a using alias. Both cases are recognized correctly.