Roslyn Add a document to a project - c#

I'm running roslyn ctp2
I am attempting to add a new html file to a project
IWorkspace workspace = Workspace.LoadSolution("MySolution.sln");
var originalSolution = workspace.CurrentSolution;
ISolution newSolution = originalSolution;
newSolution.GetProject(newSolution.ProjectIds.First())
.AddDocument("index.html", "<html></html>");
workspace.ApplyChanges(originalSolution, newSolution);
This results in no changes being written. I am trying to get the new html file to appear in VS

There are two issues here:
Roslyn ISolution, IProject, and IDocument objects are immutable, so in order to see changes you would need to create a new ISolution with the changes, then call Workspace.ApplyChanges().
In Roslyn, IDocument objects are only created for files that are passed to the compiler. Another way of saying this is things that are part of the Compile ItemGroup in the project file. For other files (including html files), you should use the normal Visual Studio interfaces like IVsSolution.

Workspaces are immutable. That means that any method that sounds like it's going to modify the workspace will instead be returning a new instance with the changes applied.
So you want something like:
IWorkspace workspace = Workspace.LoadSolution("MySolution.sln");
var originalSolution = workspace.CurrentSolution;
var project = originalSolution.GetProject(originalSolution.ProjectIds.First());
IDocument doc = project.AddDocument("index.html", "<html></html>");
workspace.ApplyChanges(originalSolution, doc.Project.Solution);
However, I'm not near a machine with Roslyn installed at the moment, so I can't guarantee this 100%.

Related

Add non-C# files in source generators

I'm creating a source generator that creates Typescript utilities based on user C# code, right now the only efficient way to create a file is AddSource() method, which can only create *.cs files.
I need to create *.ts files (or *.js), using File.Write* is also a pain, because the path and referencing project are unknown (Environment.CurrentDirectory will return the generator path which is not even close to user project) to generator, currently the only way to find the path is:
var baseFilePath = context.Compilation.SyntaxTrees.First(x => x.HasCompilationUnitRoot).FilePath;
var myDir = Path.Combine(Path.GetDirectoryName(baseFilePath)!, "tsFiles");
from here.
which as you can see is not really nice and safe and it would be a performance killer since it cannot be used in Initialize method, it has to be in Execute method which will execute forever and you have to either put an if statement to check File.Exists() or it will create that file for ever.
Considering all these, what is the most efficient way to create non-C# files (both in startup and execution time)
For either a ISourceGenerator or IIncrementalGenerator, you should be able to access the target project's "build_property.projectdir" value from AnalyzerConfigOptions.
The following is a gist that I use for the same thing in an IIncrementalGenerator.
IncrementalValueProvider<string?> projectDirProvider = context.AnalyzerConfigOptionsProvider
.Select(static (provider, _) => {
provider.GlobalOptions.TryGetValue("build_property.projectdir", out string? projectDirectory);
return projectDirectory;
});
context.RegisterSourceOutput(
projectDirProvider,
static (context, source) => {
string? projectDirectory = source;
});
This will give you the base/root directory of your target project and from there you should be able to use System.IO for what you need.

How do I retrieve text from the Visual Studio editor for use with Roslyn SyntaxTree?

I am attempting to write a Visual Studio extension that will analyze the C# code displayed in the editor and possibly update the code based on what I find. This would be on demand (via a menu item), and not using an analyzer and code fix.
There are a number of examples and samples on the Internet, but they all start either with the source code hard-coded in the samples, or create a new document, or look at each file in the VS solution that is open. How do I access the source code from the active editor window?
In a comment to my original question, #SJP gave a link to #Frank Bakker's answer to the question at Calling Roslyn from VSIX Command. This does work as outlined.
#JoshVarty provided a hint of the direction to go in his answer above. I combined that with code provided by #user1912383 for how to get an IWpfTextView answering the question Find an IVsTextView or IWpfTextView for a given ProjectItem, in 2010 RC extension. Here is the code I came up with:
var componentModel = (IComponentModel)Package.GetGlobalService(typeof(SComponentModel));
var textManager = (IVsTextManager)Package.GetGlobalService(typeof(SVsTextManager));
IVsTextView activeView = null;
ErrorHandler.ThrowOnFailure(textManager.GetActiveView(1, null, out activeView));
var editorAdapter = componentModel.GetService<IVsEditorAdaptersFactoryService>();
var textView = editorAdapter.GetWpfTextView(activeView);
var document = (textView.TextBuffer.ContentType.TypeName.Equals("CSharp"))
? textView : null;
In a comment after #user1912383's code mentioned above, #kman mentioned that this does not work for document types such as .sql files. It does, however, work for .cs files which is what I will be using it with.
First, you need to install the Microsoft.CodeAnalysis.EditorFeatures.Text package.
Then you need to add the appropriate using statement:
using Microsoft.CodeAnalysis.Text;
Now you can map between Visual Studio concepts (ITextSnapshot, ITextBuffer etc.) and Roslyn concepts (Document, SourceText etc.) with the extension methods found here: https://github.com/dotnet/roslyn/blob/master/src/EditorFeatures/Text/Extensions.cs
For example:
ITextSnapshot snapshot = ... //Get this from Visual Studio
var documents = snapshot.GetRelatedDocuments(); //There may be more than one

How to output a file from a roslyn code analyzer?

I'm using the roslyn API to write a DiagnosticAnalyzer and CodeFix.
After I have collected all strings and string-interpolations, I want to write all of them to a file but I am not sure how to do this the best way.
Of course I can always simply do a File.WriteAllText(...) but I'd like to expose more control to the user.
I'm also not sure about how to best trigger the generation of this file, so my questions are:
I do not want to hard-code the filename, what would be the best way to expose this setting to the user of the code-analyzer? A config file? If so, how would I access that? ie: How do I know the directory?
If one string is missing from the file, I'd like to to suggest a code fix like "Project contains changed or new strings, regenerate string file". Is this the best way to do this? Or is it possible to add a button or something to visual studio?
I'm calling the devenv.com executable from the commandline to trigger builds, is there a way to force my code-fix to run either while building, or before/after? Or would I have to "manually" load the solution with roslyn and execute my codefix?
I've just completed a project on this. There are a few things that you will need to do / know.
You will probably need to switch you're portable class library to a class library. otherwise you will have trouble calling the File.WriteAllText()
You can see how to Convert a portable class library to a regular here
This will potentially not appropriately work for when trying to apply all changes to document/project/solution. When Calling from a document/project/solution, the changes are precalcuated and applied in a preview window. If you cancel, an undo action is triggered to undo all changes, if you write to a file during this time, and do not register an undo action you will not undo the changes to the file.
I've opened a bug with roslyn but you can handle instances by override the preview you can see how to do so here
And one more final thing you may need to know is how to access the Solution from the analyzer which, Currently there is a hack I've written to do so here
As Tamas said you can use additional files you can see how to do so here
You can use additional files, but I know on the version I'm using resource files, are not marked as additional files by default they are embeddedResources.
So, for my users to not have to manually mark the resource as additonalFiles I wrote a function to get out the Designer.cs files associated with resource files from the csproj file using xDoc you can use it as an example if you choose to parse the csproj file:
protected List<string> GetEmbeddedResourceResxDocumentPaths(Project project)
{
XDocument xmldoc = XDocument.Load(project.FilePath);
XNamespace msbuild = "http://schemas.microsoft.com/developer/msbuild/2003";
var resxFiles = new List<string>();
foreach (var resource in xmldoc.Descendants(msbuild + "EmbeddedResource"))
{
string includePath = resource.Attribute("Include").Value;
var includeExtension = Path.GetExtension(includePath);
if (0 == string.Compare(includeExtension, RESX_FILE_EXTENSION, StringComparison.OrdinalIgnoreCase))
{
var outputTag = resource.Elements(msbuild + LAST_GENERATED_TAG).FirstOrDefault();
if (null != outputTag)
{
resxFiles.Add(outputTag.Value);
}
}
}
return resxFiles;
}
For config files you can use the AdditionalFiles msbuild property, which is passed to the analyzers through the context. See here.

Dynamically compiled project losing resources

I need to compile source code of big project dynamically and output type can be Windows Application or Class Library.
Code is nicely executed and its possible to make .dll or .exe files, but problem is that, when I'm trying to make .exe file - it's losing resources like project icon. Result file doesn't include assembly information to.
Any way to solve this? (Expected result should be the same, that manual Build function on project file in Visual Studio 2015).
Thank you!
var workspace = MSBuildWorkspace.Create();
//Locating project file that is WindowsApplication
var project = workspace.OpenProjectAsync(#"C:\RoslynTestProjectExe\RoslynTestProjectExe.csproj").Result;
var metadataReferences = project.MetadataReferences;
// removing all references
foreach (var reference in metadataReferences)
{
project = project.RemoveMetadataReference(reference);
}
//getting new path of dlls location and adding them to project
var param = CreateParamString(); //my own function that returns list of references
foreach (var par in param)
{
project = project.AddMetadataReference(MetadataReference.CreateFromFile(par));
}
//compiling
var projectCompilation = project.GetCompilationAsync().Result;
using (var stream = new MemoryStream())
{
var result = projectCompilation.Emit(stream);
if (result.Success)
{
/// Getting result
//writing exe file
using (var file = File.Create(Path.Combine(_buildPath, fileName)))
{
stream.Seek(0, SeekOrigin.Begin);
stream.CopyTo(file);
}
}
}
We never really designed the workspace API to include all the information you need to emit like this; in particular when you're calling Emit there's an EmitOptions you can pass that includes, amongst other things, resource information. But we don't expose that information since this scenario wasn't hugely considered. We've done some of the work in the past to enable this but ultimately never merged it. You might wish to consider filing a bug so we officially have the request somewhere.
So what can you do? I think there's a few options. You might consider not using Roslyn at all but rather modifying the project file and building that with the MSBuild APIs. Unfortunately I don't know what you're ultimately trying to achieve here (it would help if you mentioned it), but there's a lot more than just the compiler invocation that is involved in building a project. Changing references potentially changes other things too.
It'd also be possible, of course, to update MSBuildWorkspace yourself to pass this through. If you were to modify the Roslyn code, you'll see we implement a series of interfaces named "ICscHostObject#" (where # is a number) and we get passed the information from MSBuild to that. It looks like we already stash that in the command line arguments, so you might be able to pass that to our command line parser and get the data back you need that way.

programmatically extract interface using roslyn

I am looking for a way to extract an interface from a document (c# class declaration) using Roslyn.
going from the reformatter example.
MSBuildWorkspace workspace = MSBuildWorkspace.Create();
// Open the solution within the workspace.
Solution originalSolution = workspace.OpenSolutionAsync(project).Result;
// Declare a variable to store the intermediate solution snapshot at each step.
MSBuildWorkspace workspace = MSBuildWorkspace.Create();
Solution originalSolution = workspace.OpenSolutionAsync(project).Result;
Solution newSolution = originalSolution;
foreach (ProjectId projectId in originalSolution.ProjectIds)
{
// Look up the snapshot for the original project in the latest forked solution.
Project proj = newSolution.GetProject(projectId);
var comp = proj.GetCompilationAsync().Result;
///var bind = comp.
if (proj.Name.EndsWith("Core.DataLayer"))
{
foreach (DocumentId documentId in proj.DocumentIds)
{
Document document = newSolution.GetDocument(documentId);
if (IsRepositoryDocument(document))
{
//How to implement this?
var newinterface = GetInterfaceFromRespository(document);
}
}
}
}
I started out using the sample "reformat solution" that the Roslyn team provided. However I am unable to find a public API to extract an interface from a given class file.
When trying to find this functionality in the Roslyn source code I can only find internal classes. I found the relevant classes in
"src\Features\Core\Portable\ExtractInterface" of the roslyn source code, i could copy these into my project and get it working, but i would rather not.
TLDR; is there a public API that I can use from C# to extract an interface from a class programatically?
Note that this is done in a "regular" C# project and not in a visual studio extension or analyzer.
You can get all the interfaces from a C# file using the below code statements.
string code = new StreamReader(filePath).ReadToEnd();
var syntaxTree = CSharpSyntaxTree.ParseText(code);
var syntaxRoot = syntaxTree.GetRoot();
IEnumerable<InterfaceDeclarationSyntax> interfaceDeclarations = syntaxRoot.DescendantNodes().OfType<InterfaceDeclarationSyntax>();
Then you can iterate the available interfaces in the file.

Categories