Get dependencies between classes in Roslyn - c#

I am successfully getting dependencies between projects with Roslyn, and now I would like to get dependencies between classes, similar to the Code Map feature in Visual Studio Enterprise.
Here is my code, the "?????" part is where I imagine I could get something. I am very new to the Roslyn API, though, and I don't know how to proceed from there on.
Solution solution = MSBuildWorkspace.Create()
.OpenSolutionAsync(Path.Combine(repoRootFolder, "MySolution.sln"))
.Result;
ProjectDependencyGraph projdeps = solution.GetProjectDependencyGraph();
Digraph graph = new Digraph();
foreach (ProjectId projectId in projdeps.GetTopologicallySortedProjects())
{
string projName = solution.GetProject(projectId).Name;
var projDeps = projdeps.GetProjectsThatThisProjectDirectlyDependsOn(projectId);
foreach (ProjectId depId in projDeps)
{
Project dep = solution.GetProject(depId);
Compilation compilation = dep.GetCompilationAsync().Result;
foreach (var syntree in compilation.SyntaxTrees)
{
foreach (var classNode in syntree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>())
{
var classesThisClassNodeReferences = ?????????
}
}
string depName = dep.Name;
graph.Dependencies.Add(new Dependency
{
Source = projName,
Target = depName
});
}
}

I'm not sure about your requirements, but you can probably go for checking all descendant SyntaxNodes of the class and get the corresponding symbol, and it's type, and then collect these types. Something like the following:
var semantic = compilation.GetSemanticModel(syntree);
var typesForCurrentClass = classNode.DescendantNodes().Select(n =>
semantic.GetTypeInfo(n).Type);
Note that there can be multiple typesForCurrentClass for a given class symbol because of partial classes.

Related

Roslyn MsBuildWorkspace compilation emit does not contain my changes

I'm trying to do minor changes to documents with roslyn and then compile the project to a new dll. But when I compile it to a new dll all my changes are gone. Am I missing something here?
var workspace = MSBuildWorkspace.Create();
var project = workspace.OpenProjectAsync(#"path\to\.csproj").Result;
var documents = project.DocumentIds;
foreach (var documentId in documents)
{
var document = project.GetDocument(documentId);
var root = document.GetSyntaxRootAsync().Result
var rewrite = new MyRewrite();
root = rewrite.Visit(root);
var newDocument = document.WithSyntaxRoot(root);
var compilation = newDocument.Project.GetCompilationAsync().Result;
// When I look at the sementatic model here it contains my changes.
var sementaticModel =
compilation.GetSemanticModel(newDocument.GetSyntaxTreeAsync().Result);
// But when I inspect this dll with dotPeek it's still the old code without changes.
compilation.Emit("new/dll/path");
}
Somehow it worked by changing:
var newDocument = document.WithSyntaxRoot(root);
to
var newDocument = document.WithText(root.GetText());

Asp.Net Core: Reflecting an Asp.Net Assembly throw exception

I'm currently implementing an elementary solution to load the Services in an Asp.Net Core by reflection instead of having to pass every single type.
To have some wiggle-room, I created a static helper returning me the assembly-types using the new core-reflection types:
internal static class ReflectionTypeHelper
{
private static readonly Assembly _currentAssembly = typeof(ServiceContainerInitializer).GetTypeInfo().Assembly;
internal static IReadOnlyCollection<Type> ScanAssembliesForTypes(Func<Type, bool> predicate)
{
var result = new List<Type>();
var appAssemblies = GetApplicationAssemblies();
foreach (var ass in appAssemblies)
{
var typesFromAssembly = ass.GetTypes().Where(predicate);
result.AddRange(typesFromAssembly);
}
return result;
}
private static IEnumerable<Assembly> GetApplicationAssemblies()
{
var consideredFileExtensions = new[]
{
".dll",
".exe"
};
var result = new List<Assembly>();
var namespaceStartingPart = GetNamespaceStartingPart();
var assemblyPath = GetPath();
IEnumerable<string> assemblyFiles = Directory.GetFiles(assemblyPath);
var fileInfos = assemblyFiles.Select(f => new FileInfo(f));
fileInfos = fileInfos.Where(f => f.Name.StartsWith(namespaceStartingPart) && consideredFileExtensions.Contains(f.Extension.ToLower()));
// Net.Core can't load the Services for some reason, so we exclude it at the moment
//fileInfos = fileInfos.Where(f => f.Name.IndexOf("Services", StringComparison.OrdinalIgnoreCase) == -1);
foreach (var fi in fileInfos)
{
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(fi.FullName);
result.Add(assembly);
}
return result;
}
private static string GetNamespaceStartingPart()
{
var fullNamespace = _currentAssembly.FullName;
var splittedNamespace = fullNamespace.Split('.');
var result = string.Concat(splittedNamespace[0], ".", splittedNamespace[1]);
return result;
}
private static string GetPath()
{
var codeBase = _currentAssembly.CodeBase;
var uri = new UriBuilder(codeBase);
var result = Uri.UnescapeDataString(uri.Path);
result = Path.GetDirectoryName(result);
return result;
}
}
As you can probably see in the code-comment, I can't load the "Services"-Assembly, which I creted from the "ASP.NET Core Web Application (.Net Core)" - Project template.
Unfortunately, the exception is quite generic
Could not load file or assembly 'Argusnet.Pis.Services,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.
Also, the file is there as expected.
I did find some hints on GitHub-Issues regarding this topic, but they're all solved in the Release Candidates.
Interesting enough, all other assemblies work as you'd expect, so there has to be something specific regarding this assembly-types?
Edit: Screenshot of the exception:
One of the reason why it's failing to load might be the mismatch in target processor architecture selected during compiling. Argusnet.Pis.Services might be compiled with x86 configuration and the client application trying to load might be built using the x64 option during compiling Or the other way around. Make sure both projects have the same option(x86 or x64) set before building them. Otherwise try building them with Any CPU option.

How to add additional files to an ad hoc Roslyn workspace to expose them to analyzers

I am creating some Roslyn analyzers, that use the AdditionFiles feature to access a settings file. I am trying to test the analyzers use this correctly.
I have a method that sets up an ad hoc test workspace, and I've tried adding additional documents via two routes:
private static Project CreateProject(IEnumerable<string> sources)
{
var projectId = ProjectId.CreateNewId(TestProjectName);
var solution = new AdhocWorkspace()
.CurrentSolution
.AddProject(projectId, TestProjectName, TestProjectName, LanguageNames.CSharp)
.AddMetadataReference(projectId, CorlibReference)
.AddMetadataReference(projectId, SystemCoreReference)
.AddAdditionalDocument(DocumentInfo.Create(DocumentId.CreateNewId(projectId),
"arnolyzer.yaml",
filePath: #"..\..\arnolyzer.yaml"));
var count = 0;
foreach (var source in sources)
{
var newFileName = $"{DefaultFilePathPrefix}{count++}.{CSharpDefaultFileExt}";
var documentId = DocumentId.CreateNewId(projectId, newFileName);
solution = solution.AddDocument(documentId, newFileName, SourceText.From(source));
}
var settingsFileId = DocumentId.CreateNewId(projectId, "arnolyzer.yaml");
solution = solution.AddAdditionalDocument(settingsFileId, "arnolyzer.yaml", SourceText.From(#"..\..\arnolyzer.yaml"));
return solution.GetProject(projectId);
}
Examining the Project instance, I can see that both additional documents have been added.
However, when inspecting CompilationStartAnalysisContext.Options.AdditionalFiles within a AnalysisContext.RegisterCompilationStartAction action, AdditionalFiles is empty.
Does anyone know whether this approach should work and thus whetehr I've gone wrong somewhere? Or are the additional documents added to a Project unrelated to the AditionalFiles feature?
This will not directly answer your question, but you mention that you are trying to test analyzers if they use the additional file or not. So, here is how we solve that:
var compilationWithAnalyzer = compilation.WithAnalyzers(
diagnosticAnalyzers,
new AnalyzerOptions(ImmutableArray.Create<AdditionalText>(new AnalyzerAdditionalFile(configuration.Path))),
tokenSource.Token);
var diagnostics = await compilationWithAnalyzer.GetAnalyzerDiagnosticsAsync();
where AnalyzerAdditionalFile just extends AdditionalText:
public sealed class AnalyzerAdditionalFile : AdditionalText
{
private readonly string path;
public AnalyzerAdditionalFile(string path)
{
this.path = path;
}
public override string Path => path;
public override SourceText GetText(CancellationToken cancellationToken)
{
return SourceText.From(File.ReadAllText(path));
}
}

Use Roslyn MSBuildWorkspace Project AddAnalyzerReference doesn't load analyzers

I'm working on a code report project.
Currently, I'm able to compile the solution projects, get the diagnostics related to the compilation, etc..
The problem appears when I try to load my custom IDiagnosticAnalyzers, I've tried to use the AnalyzerFileReference and the AnalyzerImageReference without any result, Always I access the projects.Analizers are empty.
var inmutableArray = (new List<IDiagnosticAnalyzer>
{
new VariableEndedWithIdNamedCorrectlyDiagnosticAnalyzer()
}).ToImmutableArray();
var analyzerImageReference = new AnalyzerImageReference(inmutableArray);
foreach (Project project in solution.Projects)
{
project.AddAnalyzerReference(analyzerImageReference );
//No analizers loaded....
}
UPDATE (thanks for the feedback [Josh Varty])
I've tried this two ways:
var newProjects = new List<Project>();
foreach (Project project in solution.Projects)
{
var newSolutionn= solution.AddAnalyzerReference(project.Id, analyzerImageReference);
newProjects.Add(newSolutionn.Projects.FirstOrDefault(p=> p.Id == project.Id));
}
foreach (Project project in solution.Projects)
{
var newProject = project.AddAnalyzerReference( analyzerImageReference);
}
In both cases have the analyzers loaded but when I get the compilation and I get the diagnostics, I don't get the output related to this analyzers (I think they are not being called at the get compilation function).
var compilation = newProject.GetCompilationAsync().Result;
var diagnostics = compilation.GetDiagnostics();
Any suggestions?
As I commented, most Roslyn objects are immutable. This means methods like AddAnalyzerReference() don't mutate the project, but instead return a new one.
I don't have an analyzer to test this, but I believe you can use the following. Note that I'm using Solution.AddAnalyzerReference() instead of the one you were using.
var inmutableArray =(new List<IDiagnosticAnalyzer>
{
new VariableEndedWithIdNamedCorrectlyDiagnosticAnalyzer()
}).ToImmutableArray();
var analyzerImageReference = new AnalyzerImageReference(inmutableArray);
Solution newSolution = solution;
//We iterate over the original solution
foreach (Project project in solution.Projects)
{
//But we save our work in the newSolution
newSolution = newSolution.AddAnalyzerReference(project.Id, analyzerImageReference);
}
//Now newSolution should contain all your changes.
//Maybe you want to save this reference?
solution = newSolution;
I've found the way to do it:
public static Task<ImmutableArray<Diagnostic>> GetDiagnosticsAsync(this Compilation compilation, ImmutableArray<DiagnosticAnalyzer> analyzers, AnalyzerOptions options, CancellationToken cancellationToken = default(CancellationToken))
{
options = options ?? new AnalyzerOptions(ImmutableArray<AdditionalStream>.Empty, ImmutableDictionary<string, string>.Empty);
Compilation newCompilation = null;
var analyzerDriver = AnalyzerDriver.Create(compilation, analyzers, options, out newCompilation, cancellationToken);
newCompilation.GetDiagnostics(cancellationToken);
return analyzerDriver.GetDiagnosticsAsync();
}
I've published a version of the open source project that I've been working using Roslyn, you can see the code and other thing related to analyzers and codefix.
https://bitbucket.org/jrierapeiro/codeanalyzer
I had similar question which i answered over here.
You have to use compilation.WithAnalyzer(analyzer) and then getDiagnostics()

Analysing C# source with Irony

This is what my team and I chose to do for our school project. Well, actually we haven't decided on how to parse the C# source files yet.
What we are aiming to achieve is, perform a full analysis on a C# source file, and produce up a report.
In which the report is going to contain stuff that happening in the codes.
The report only has to contain:
string literals
method names
variable names
field names
etc
I'm in charge of looking into this Irony library. To be honest, I don't know the best way to sort the data out into a clean readable report. I am using the C# grammar class packed with the zip.
Is there any step where I can properly identify each node children? (eg: using directives, namespace declaration, class declaration etc, method body)
Any help or advice would be very much appreciated. Thanks.
EDIT: Sorry I forgot to say we need to analysis the method calls too.
Your main goal is to master the basics of formal languages. A good start-up might be found here. This article describes the way to use Irony on the sample of a grammar of a simple numeric calculator.
Suppose you want to parse a certain file containing C# code the path to which you know:
private void ParseForLongMethods(string path)
{
_parser = new Parser(new CSharpGrammar());
if (_parser == null || !_parser.Language.CanParse()) return;
_parseTree = null;
GC.Collect(); //to avoid disruption of perf times with occasional collections
_parser.Context.SetOption(ParseOptions.TraceParser, true);
try
{
string contents = File.ReadAllText(path);
_parser.Parse(contents);//, "<source>");
}
catch (Exception ex)
{
}
finally
{
_parseTree = _parser.Context.CurrentParseTree;
TraverseParseTree();
}
}
And here is the traversal method itself with counting some info in the nodes. Actually this code counts the number of statements in every method of the class. If you have any question you are always welcome to ask me
private void TraverseParseTree()
{
if (_parseTree == null) return;
ParseNodeRec(_parseTree.Root);
}
private void ParseNodeRec(ParseTreeNode node)
{
if (node == null) return;
string functionName = "";
if (node.ToString().CompareTo("class_declaration") == 0)
{
ParseTreeNode tmpNode = node.ChildNodes[2];
currentClass = tmpNode.AstNode.ToString();
}
if (node.ToString().CompareTo("method_declaration") == 0)
{
foreach (var child in node.ChildNodes)
{
if (child.ToString().CompareTo("qual_name_with_targs") == 0)
{
ParseTreeNode tmpNode = child.ChildNodes[0];
while (tmpNode.ChildNodes.Count != 0)
{ tmpNode = tmpNode.ChildNodes[0]; }
functionName = tmpNode.AstNode.ToString();
}
if (child.ToString().CompareTo("method_body") == 0) //method_declaration
{
int statementsCount = FindStatements(child);
//Register bad smell
if (statementsCount>(((LongMethodsOptions)this.Options).MaxMethodLength))
{
//function.StartPoint.Line
int functionLine = GetLine(functionName);
foundSmells.Add(new BadSmellRegistry(name, functionLine,currentFile,currentProject,currentSolution,false));
}
}
}
}
foreach (var child in node.ChildNodes)
{ ParseNodeRec(child); }
}
I'm not sure this is what you need but you could use the CodeDom and CodeDom.Compiler namespaces to compile the C# code, and than analyze the results using Reflection, something like:
// Create assamblly in Memory
CodeSnippetCompileUnit code = new CodeSnippetCompileUnit(classCode);
CSharpCodeProvider provider = new CSharpCodeProvider();
CompilerResults results = provider.CompileAssemblyFromDom(compileParams, code);
foreach(var type in results.CompiledAssembly)
{
// Your analysis go here
}
Update: In VS2015 you could use the new C# compiler (AKA Roslyn) to do the same, for example:
var root = (CompilationUnitSyntax)tree.GetRoot();
var compilation = CSharpCompilation.Create("HelloTDN")
.AddReferences(references: new[] { MetadataReference.CreateFromAssembly(typeof(object).Assembly) })
.AddSyntaxTrees(tree);
var model = compilation.GetSemanticModel(tree);
var nameInfo = model.GetSymbolInfo(root.Usings[0].Name);
var systemSymbol = (INamespaceSymbol)nameInfo.Symbol;
foreach (var ns in systemSymbol.GetNamespaceMembers())
{
Console.WriteLine(ns.Name);
}

Categories