Getting method arguments with Roslyn - c#

I can get a list from the solution of all calls to a particuliar method using the following code:
var createCommandList = new List<MethodSymbol>();
INamedTypeSymbol interfaceSymbol =
(from p
in solution.Projects
select p.GetCompilation().GetTypeByMetadataName(
"BuySeasons.BsiServices.DataResource.IBsiDataConnection")
).FirstOrDefault();
foreach (ISymbol symbol in interfaceSymbol.GetMembers("CreateCommand"))
{
if (symbol.Kind == CommonSymbolKind.Method
&& symbol is MethodSymbol)
{
createCommandList.Add(symbol as MethodSymbol);
}
}
foreach (MethodSymbol methodSymbol in createCommandList)
{
foreach (ReferencedSymbol referenceSymbol
in methodSymbol.FindReferences(solution))
{
foreach (ReferenceLocation referenceLocation
in from l
in referenceSymbol.Locations
orderby l.Document.FilePath
select l)
{
if (referenceLocation.Location.GetLineSpan(false)
.StartLinePosition.Line ==
referenceLocation.Location.GetLineSpan(false)
.EndLinePosition.Line)
{
Debug.WriteLine("{0} {1} at {2} {3}/{4} - {5}",
methodSymbol.Name,
"(" + String.Join(",",
(from p
in methodSymbol.Parameters
select p.Type.Name + " " + p.Name).ToArray()
) + ")",
Path.GetFileName(referenceLocation.Location.GetLineSpan(false)
.Path),
referenceLocation.Location.GetLineSpan(false)
.StartLinePosition.Line,
referenceLocation.Location.GetLineSpan(false)
.StartLinePosition.Character,
referenceLocation.Location.GetLineSpan(false)
.EndLinePosition.Character));
}
else
{
throw new ApplicationException("Call spans multiple lines");
}
}
}
}
But this gives me a list of ReferencedSymbol. Although this gives me the file and line number that the method is called from I would also like to get the specific arguments that the method is called with. How can I either convert what I have or get the same information with Roslyn? (notice the I first load the solution with the Solution.Load method and then loop through to find out where the method is defined/declared (createCommandList)).

You are already using Roslyn here. When you have a referenceSymbol, you can get at the Method Declaration Syntax and then look down into the tree to get the Parameter list.
I've inserted a arguments variable that uses your referenceSymbol:
// Snip start
foreach (MethodSymbol methodSymbol in createCommandList)
{
foreach (ReferencedSymbol referenceSymbol
in methodSymbol.FindReferences(solution))
{
var arguments = referenceSymbol.Definition.DeclaringSyntaxNodes.First()
.DescendantNodes().OfType<ParameterSyntax>().ToList();
foreach (ReferenceLocation referenceLocation in
from l
in referenceSymbol.Locations
orderby l.Document.FilePath
select l)
{
// Snip end
When you perform a Debug output, you can then use that list of arguments to get the names.
My solution requires getting the First() of the DeclaringSyntaxNodes, which I don't like very much but cannot find another way to get at the Descendant Nodes of the Syntax Tree.

I have discovered another way of getting the Parameter list from a method that might work for others as well.
Say I have the following method that has two parameters:
public string DebugPage(string enabled, string show){
//do stuff
}
Then you get your nodes however you wish. For example this gives me a list of public methods:
IEnumerable<MethodDeclarationSyntax> methods = from m in root.DescendantNodes().OfType<MethodDeclarationSyntax>() where m.Modifiers.ToString().Contains("public") select m;
Then I can iterate through that list of methods to get at the method's ParameterList property which is exposed to make this operation really easy. By the end of this loop the list parameters will hold the names of each parameter in the method (in the example of the Debug method above it will hold the values enabled and show as expected):
var parameters = new List<string>();
foreach (var method in methods)
{
foreach (var n in method.ParameterList.Parameters)
{
var parameterName = n.Identifier.Text;
parameters.Add(parameterName);
}
}

You can search the syntax tree of the reference at the specific source location it occurs to find the node you are looking for. You'll need to use a call like DescendentNodes from the tree's root node and you'll probably need to request the language specific node type you are looking for. Once you have the node in the referecing tree you can then use that tree's semantic model to tell you other information about the arguments.

Related

Modifying function declaration parameter by adding attribute and default value in Roslyn c#

I'm modifying my earlier code analyser in C# using Roslyn and again I'm stuck with some changes that I don't exactly know how to apply.
Basing on: https://github.com/dotnet/roslyn/wiki/Getting-Started-C%23-Syntax-Analysis I've created some base to work with in this question: Finding all not inheriting C# classes with Roslyn and changing to inheriting from base object (java-like)
I've parsed and traversed tree to find all method declarations, and their parameters. Basing on VS Syntax Visualiser I've constructed this:
foreach (var c in root1.DescendantNodesAndSelf())
{
var methodDeclaration = c as MethodDeclarationSyntax;
if (methodDeclaration == null)
continue;
if (methodDeclaration.ParameterList != null) //Have parameters
{
foreach (var p in methodDeclaration.ParameterList.Parameters)
{
var parameter = p as ParameterSyntax;
String name, type;
name = parameter.GetLastToken().Value.ToString();
type = parameter.GetFirstToken().Value.ToString();
if (parameter == null)
continue;
if (name == "caller" && type == "string")
{
AttributeSyntax ats = SyntaxFactory.Attribute(SyntaxFactory.QualifiedName(SyntaxFactory.IdentifierName("System.Runtime.CompilerServices"),SyntaxFactory.IdentifierName("CallerMemberName")));
SeparatedSyntaxList<AttributeSyntax> ssl = new SeparatedSyntaxList<AttributeSyntax>();
ssl = ssl.Add(ats);
AttributeListSyntax als = SyntaxFactory.AttributeList(ssl);
var par1 = parameter.AddAttributeLists(als);
//ExpressionSyntax es = SyntaxFactory.AssignmentExpression(SyntaxKind.EqualsValueClause,null,SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression))
//SyntaxFactory.EqualsValueClause(es);
par1 = par1.AddModifiers();
root2 = root2.ReplaceNode(parameter, par1);
}
}
}
else //Don't have parameters
continue;
}
I'm trying to convert method declared like this:
private void testM3(string caller)
into
private void testM3([System.Runtime.CompilerServices.CallerMemberName] string caller = "")
And this part:
//ExpressionSyntax es = SyntaxFactory.AssignmentExpression(SyntaxKind.EqualsValueClause,null,SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression))
//SyntaxFactory.EqualsValueClause(es);
is my failed attempt to achieve creation of equals node.
From what I understand, this part:
AttributeSyntax ats = SyntaxFactory.Attribute(SyntaxFactory.QualifiedName(SyntaxFactory.IdentifierName("System.Runtime.CompilerServices"),SyntaxFactory.IdentifierName("CallerMemberName")));
SeparatedSyntaxList<AttributeSyntax> ssl = new SeparatedSyntaxList<AttributeSyntax>();
ssl = ssl.Add(ats);
AttributeListSyntax als = SyntaxFactory.AttributeList(ssl);
var par1 = parameter.AddAttributeLists(als);
will give me new parameter node in var par1 that includes attribute already, so I need to add default value setting. Please correct me if I'm wrong about this attribute, and I'd like to know how to build this equals expression node properly.
You have a two mistakes:
System.Runtime.CompilerServices.CallerMemberName is QualifiedName that contains System.Runtime.CompilerServices as QualifiedName and CallerMemberName as IdentifierName. System.Runtime.CompilerServices contains System.Runtime as QualifiedName and CompilerServices as IdentifierName. And finally System.Runtime contains two IdentifierName.
So you need to fix creation of AttributeSyntax as are shown in the code below:
AttributeSyntax ats = SyntaxFactory.Attribute(
SyntaxFactory.QualifiedName(
SyntaxFactory.QualifiedName(
SyntaxFactory.QualifiedName(SyntaxFactory.IdentifierName("System"), SyntaxFactory.IdentifierName("Runtime")),
SyntaxFactory.IdentifierName("CompilerServices")),
SyntaxFactory.IdentifierName("CallerMemberName")));
EqualsValueClause shouldn't contain AssignmentExpression, but should contains a some kind of LiteralExpression directly. In your case it's StringLiteralExpression:
var par1 = parameter
.AddAttributeLists(als)
.WithDefault(SyntaxFactory.EqualsValueClause(SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(""))));
By the way, you can use a some helpful method of Nodes, as example ParameterSyntax.WithDefault, to create a copy of Node (SyntaxTree is immutable in Roslyn) when you want to applay a small changes to existing node and then repace it.
If you take a look at the Roslyn Quoter for the following method you can obtain the code required to generate the desired code:
public void GetSomething([CallerMemberName] string test=""){
}
You will notice that the Default Value in the Parameter is constructed using the following method (the roslyn quoter usually omits the SyntaxFactory):
.WithDefault(SyntaxFactory.EqualsValueClause(
SyntaxFactory.LiteralExpression(
SyntaxKind.StringLiteralExpression,
SyntaxFactory.Literal("")
)
)
);
So in order to add your EqualsValueClause as a default you just have to replace the existing default by calling the above code on your parameter (instead of the uncommented code):
par1 = par1.WithDefault(SyntaxFactory.EqualsValueClause(
SyntaxFactory.LiteralExpression(
SyntaxKind.StringLiteralExpression,
SyntaxFactory.Literal("")
)
)
);

Roslyn get IdentifierName in ObjectCreationExpressionSyntax

Currently I am working on simple code analyse for c# with roslyn. I need to parse all document of all projects inside one solution and getting the declared used classes inside this document.
For example from:
class Program
{
static void Main(string[] args)
{
var foo = new Foo();
}
}
I want to get Program uses Foo.
I already parse all documents and get the declared class inside.
// all projects in solution
foreach (var project in _solution.Projects)
{
// all documents inside project
foreach (var document in project.Documents)
{
var syntaxRoot = await document.GetSyntaxRootAsync();
var model = await document.GetSemanticModelAsync();
var classes = syntaxRoot.DescendantNodes().OfType<ClassDeclarationSyntax>();
// all classes inside document
foreach (var classDeclarationSyntax in classes)
{
var symbol = model.GetDeclaredSymbol(classDeclarationSyntax);
var objectCreationExpressionSyntaxs = classDeclarationSyntax.DescendantNodes().OfType<ObjectCreationExpressionSyntax>();
// all object creations inside document
foreach (var objectCreationExpressionSyntax in objectCreationExpressionSyntaxs)
{
// TODO: Get the identifier value
}
}
}
}
The problem is to get the IdentifierName Foo. Using the debugger, I see objectCreationExpressionSyntax.Typegot the Identifier.Text got the value I need, but objectCreationExpressionSyntax.Type.Identifierseems to be private.
I could use the SymbolFinder to find all references of a Class in the solution. As I already need to parse all documents its should work without.
Maybe I am on the wrong path? How to get the identifier value?
You'll need to handle the different types of TypeSyntaxes. See here: http://sourceroslyn.io/#Microsoft.CodeAnalysis.CSharp/Syntax/TypeSyntax.cs,29171ac4ad60a546,references
What you see in the debugger is a SimpleNameSyntax, which does have a public Identifier property.
Update
var ns = objectCreationExpressionSyntax.Type as NameSyntax;
if (ns != null)
{
return ns.Identifier.ToString();
}
var pts = objectCreationExpressionSyntax.Type as PredefinedTypeSyntax;
if (pts != null)
{
return pts.Keyword.ToString();
}
...
All other subtypes would need to be handed. Note that ArrayType.ElementType is also a TypeSyntax, so you would most probably need to make this method recursive.
You can get the identifier from the syntax's Type property:
foreach (var objectCreationExpressionSyntax in objectCreationExpressionSyntaxs)
{
IdentifierNameSyntax ins = (IdentifierNameSyntax)objectCreationExpressionSyntax.Type;
var id = ins.Identifier;
Console.WriteLine(id.ValueText);
}
Strings can be misleading.
Let's say you have the expression new SomeClass(), and you get the string "SomeClass" out of it. How do you know if that refers to Namespace1.SomeClass or Namespace2.SomeClass ? What if there is a using SomeClass = Namespace3.SomeOtherType; declaration being used?
Fortunately, you don't have to do this analysis yourself. The compiler can bind the ObjectCreationExpressionSyntax to a symbol. You have your semantic model, use it.
foreach (var oce in objectCreationExpressionSyntaxs)
{
ITypeSymbol typeSymbol = model.GetTypeInfo(oce).Type;
// ...
}
You can compare this symbol with the symbols you get from model.GetDeclaredSymbol(classDeclarationSyntax), just make sure you use the Equals method, not the == operator.

Not properly Grouping items in a collection with LINQ

I've got a method that tries to Add n files to a repository, using SharpSVN. Any number of these files can throw an error, which I catch, and then move on to the next file and try to add that, and so on. I want to alert the user X times, where X is the number of different reasons. So if I added 5 files and 3 failed for one reason and 2 failed for a different reason, I want to present 2 errors. If they all fail for the same reason, 1 error. Five different reasons? Present 5 errors.
I made a class, FileException, that has two properties (Exception Ex, string FileName) and tried to implement a collection so I could group it on the Exception.
public void AddFiles(List<string> files)
{
var sb = new StringBuilder();
var args = new SvnAddArgs {Depth = SvnDepth.Children};
var exes = new Collection<FileException>();
foreach (var file in files)
{
try
{
//only here for testing purposes described below
if (file.Contains("png"))
throw new AccessViolationException();
SVNClient.Add(file, args);
}
catch (Exception ex)
{
exes.Add(new FileException(ex, file));
}
}
if (exes.Count > 1)
{
exes.GroupBy(s => s.Ex.GetType());
Unique<Log>.Instance.AddExceptions(exes);
}
else if (exes.Count == 1)
Unique<Log>.Instance.AddException(exes[0].Ex);
}
public void AddExceptions(Collection<FileException> e)
{
var sb = new StringBuilder();
var ex = e[0].Ex;
for(var i=0; i < e.Count;i++)
{
Logs.Add(new LogMessage(e[i].Ex));
sb.AppendLine(e[i].FileName);
WriteLogFile(new LogMessage(e[i].FileName, e[i].Ex));
if (ex.GetType() == e[i].Ex.GetType())
continue;
ShowLogError(new LogMessage(sb.ToString(), ex));
sb.Length = 0;
ex = e[i].Ex;
}
//Call ShowLogError if only 1 type of Exception in all of e
if (!string.IsNullOrEmpty(sb.ToString()))
ShowLogError(new LogMessage(sb.ToString(), ex));
}
public void ShowLogError(ILogMessage log)
{
//Extra formatting left out as its irrelevant to code sample
XtraMessageBox.Show(log.message, log.title);
}
So, what AddException is trying to do is store the first exception in ex in a sort of flag, and then iterate over its parameter and message the user when the current item is different than the flag item.
Test Case:
Add 3 files (fileA.cs, fileB.png, fileC.cs)
Expected Results:
ShowLogError() shows me fileA and fileC, since these threw the same error
ShowLogError() shows me fileB, which threw the hard-coded exception since it contains "png"
Actual Results:
ShowLogError() shows fileA and B, which threw 2 different exceptions
ShowLogError() shows fileC, which threw the same as fileA.
So I think there's a problem with how I'm using GroupBy() in the AddFiles method, but I'm totally clueless here. Oh, also, I get this note from Resharper on my GroupBy() statement: Return value of pure method is not used but I'm not really sure what is meant by "pure method".
Edit: I just tried using OrderBy instead of GroupBy, but I get the same results for the aforementioned test case.
Edit 2: Using my OrderBy instead of GroupBy, here is a screenshot from my Immediate Window:
![enter image description here][1]
Added highlighting just to make it easier to differentiate the three rows.
Should items 0 and 2 be together since their Ex is of the same type?
Edit Adding a screenshot of Gert Arnold's answer to show that it does not compile. I proposed an edit that does compile and it was removed. I appreciate his help, of course, but it wasn't 100% working.
When you've got your exes collection, basically all you have to do is:
var result = fileExceptions.GroupBy(e => e.Ex.GetType().Name)
.Select(g => new
{
g.Key, Files = string.Join("\r\n", g.Select(x => x.FileName).ToArray())
});
I believe that you are not capturing the results of of your "GroupBy" or "OrderBy" clauses.
What about this:
List<Type> types = exes.Select(x => x.GetType()).Distinct().ToList();
This gives you the unique types of FileExceptions that were added to the collection

Attempting to obtain properties / fields from an (anonymous class) object linq is creating

I'm having trouble figuring out what I'm doing wrong here. I have some LINQ that returns an IQuery object, and later in the code, I'm attempting to list out the attributes returned. This is best explained by this abbreviated code (the actual LINQ is a lot more complex and involves joins - the LINQ itself works fine):
public IQueryable<Object> FindAll()
{
var query = from user in _db.Users
select new
{
id = user.Id,
is_active = user.IsActive,
email = user.Email,
dob = user.Dob,
user.user_type,
};
return query;
}
Elsewhere in the code I have:
query.ConvertToCsv();
Although I have attempted to insert a .ToList() in that call as well.
The ConvertToCsv has:
public static string ConvertToCSV<TSource>(this IEnumerable<TSource> source)
{
StringBuilder sb = new StringBuilder();
var properties = typeof(TSource).GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
var enumerable = source as IList<TSource> ?? source.ToList();
if (!enumerable.Any()) return "";
string headerString = "";
foreach (var prop in properties)
{
headerString += (headerString.Length > 0 ? "," : "") + prop.Name;
}
sb.AppendLine(headerString);
foreach (TSource item in enumerable)
{
string line = string.Join(",", properties.Select(p => p.GetValue(item).ToCsvValue()).ToArray());
sb.AppendLine(line);
}
return sb.ToString();
}
Note I have also tried to pull out the property names with this code:
PropertyInfo[] pi = typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance);
var properties = pi.OrderBy(x => x.MetadataToken);
foreach (PropertyInfo p in properties)
{ etc etc }
In all cases, the property or field list returns an empty list, and as such, I can't iterate through the object to spit out a header row or data rows. Tracing through all the code and inspecting the variables indicates that everything is fine until I get to the GetProperties/GetFields line and the code fails.
What rookie mistake am I making? Should I be replacing <Object> with something else?
To pass an anonymous type, or a collection that contains anonymous
types, as an argument to a method, you can declare the parameter as
type object. However, doing this defeats the purpose of strong typing.
If you must store query results or pass them outside the method
boundary, consider using an ordinary named struct or class instead of
an anonymous type.
by Anonymous Types (C# Programming Guide)
Create your own class and change method declaration to be IQueryable<MyClass> instead of object
Did you consider doing something like: db.Users.Select(u => new UserDto() { Id = user.Id, Name = ..., where UserDto is dedicated class that has all the properties you'll need in the future? I think you lose information about properties when you cast from anonymous class to an Object. Although, I never tried to obtain member info from anonymous class

LINQ Abbreviation Tip

I've a LINQ query like below:
foreach (var property in from property in properties where property.Name != "Type" select property)
{
}
how would you go about making this statement more concise without using the actual extension method which looks unattractive (i.e. without using .Where like: foreach (var property in properties.Where(...)).
You cant really..
You could put the query into a separate line.
var selectedProperties = from property in properties
where property.Name != "Type"
select property;
foreach (var property in selectedProperties)
{
}
Or you could factor the query out into a separate method if it is really huge.
foreach ( var property in ComplexSelectionOfProperties () )
...
But really I would say the exention method in this case is much neater. Its only when the queries get more complex and involve joins that the query syntax becomes tidier. (IMHO)
Beauty is always in the eye of the beholder :)
However in such a case I would go create a method that filters non-Type properties and iterate over its results.
Something like this:
IEnumerable<IProperty> GetNonTypeProperties(IEnumerable<IProperty> properties)
{
return (from property in properties where property.Name != "Type" select property);
}
void foo()
{
foreach (var property in GetNonTypeProperties(properties))
{
}
}
The lack of conciseness comes precisely from the sql style syntax : using a "dot" notation you will sensibly shorten your expression :
foreach (var property in properties.Where(property => property.Name != "Type"))
{
}
If you want to shorten the longest part which is obviously the boolean test, you have to put it elsewhere.
Either in the foreach loop itself :
foreach (var property in properties)
{
if(property.Name != "Type")
{
...
}
}
Either if a separate function :
foreach (var property in properties.Where(IsNotType))
{
}
//and farther :
bool IsNotType(Property p)
{
return property.Name != "Type";
}
But anyway you want to perform a loop with a test on each element, so in a way or another you will have to code that and it will take a minimum amount of characters.
Just in case that you don't like the lambda expression, not the extension method itself, you can make your own extension method with query inside, like this:
public static IEnumerable<Property> PropertiesExceptType(this IEnumerable<Property> properties) {
return from property in properties
where property.Name != "Type"
select property;
}
and use it:
foreach(var property in properties.PropertiesExceptType()) {
// ...
}
The good thing about encapsulating your query in separate method is that you can debug the method with loop and change the code on the fly (VS won't let you do this if you have a linq query right inside this method).
I honestly don't see anything wrong with var propery in properties.Where(), it's much better than any query expression in this context IMO. But if you want to stick with your query, at least introduce a variable:
var filteredProperties = from property in properties
where property.Name != "Type"
select property;
foreach(var property in filteredProperties)
{
// ...
}
People who will read and debug it later will thank you. But I still think that extension method is the way to go here

Categories

Resources