Preserve Analyzer Include in Unity-genereated csproj?
van800 opened this issue ยท 16 comments
The readme.md states that it is possible to install UnityEngineAnalyzer as a nuget and everything will work. But next time Unity regenerates csproj files reference to Analyzer will be lost. Am I missing something?
Something like JetBrains/resharper-unity#577
might be useful to ensure that Analyzer Include is added to generated csproj.
I think we will not merge the PR mentioned above in Rider, but I may contribute a separate AssetPostProcessor strait to this repo or separate gist. What would be better?
While you wait for a reply from the maintainer, maybe you could publish your gist and paste the link here. It's a start!
It would be great to have an event directly in the Rider plugin where we could plug extra post-processors (to avoid having to read each project file, modify, and save them back. This gets expensive in projects with many projects due to having a lot of asmdef files).
I would love to see this in Rider, but I noticed it didn't get much attention. Maybe Unity developers in general aren't aware of Roslyn analyzers yet, since there's no official support.
Providing an event is an interesting idea, but it would require committing EditorPlugin to vcs, which we do not suggest.
I keep asking here and there about the custom roslyn analyzers setup because I want to avoid adding a new way of doing a thing, which probably is already supported. The only feedback I got from Unity team is that a way proposed in JetBrains/resharper-unity#577 is definitely not their way.
Updated 09.04.2019
using System;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using UnityEditor;
using UnityEngine;
namespace RoslynAnalyserSupport
{
public class CsprojAssetPostprocessor : AssetPostprocessor
{
public override int GetPostprocessOrder()
{
return 20;
}
private static string[] GetCsprojLinesInSln()
{
var projectDirectory = Directory.GetParent(Application.dataPath).FullName;
var projectName = Path.GetFileName(projectDirectory);
var slnFile = Path.GetFullPath(string.Format("{0}.sln" , projectName));
if (!File.Exists(slnFile))
return new string[0];
var slnAllText = File.ReadAllText(slnFile);
var lines = slnAllText.Split(new[] {Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries)
.Where(a => a.StartsWith("Project(")).ToArray();
return lines;
}
public static void OnGeneratedCSProjectFiles()
{
try
{
// get only csproj files, which are mentioned in sln
var lines = GetCsprojLinesInSln();
var currentDirectory = Directory.GetCurrentDirectory();
var projectFiles = Directory.GetFiles(currentDirectory, "*.csproj")
.Where(csprojFile => lines.Any(line => line.Contains("\"" + Path.GetFileName(csprojFile) + "\""))).ToArray();
foreach (var file in projectFiles)
{
UpgradeProjectFile(file);
}
}
catch (Exception e)
{
// unhandled exception kills editor
Debug.LogError(e);
}
}
private static void UpgradeProjectFile(string projectFile)
{
XDocument doc;
try
{
doc = XDocument.Load(projectFile);
}
catch (Exception)
{
Debug.LogError(string.Format("Failed to Load {0}", projectFile));
return;
}
var projectContentElement = doc.Root;
XNamespace xmlns = projectContentElement.Name.NamespaceName; // do not use var
SetRoslynAnalyzers(projectContentElement, xmlns);
doc.Save(projectFile);
}
// add everything from RoslyAnalyzers folder to csproj
//<ItemGroup><Analyzer Include="RoslynAnalyzers\UnityEngineAnalyzer.1.0.0.0\analyzers\dotnet\cs\UnityEngineAnalyzer.dll" /></ItemGroup>
//<CodeAnalysisRuleSet>..\path\to\myrules.ruleset</CodeAnalysisRuleSet>
private static void SetRoslynAnalyzers(XElement projectContentElement, XNamespace xmlns)
{
var currentDirectory = Directory.GetCurrentDirectory();
var roslynAnalysersBaseDir = new DirectoryInfo(Path.Combine(currentDirectory, "RoslynAnalyzers"));
if (!roslynAnalysersBaseDir.Exists)
return;
var relPaths = roslynAnalysersBaseDir.GetFiles("*", SearchOption.AllDirectories)
.Select(x => x.FullName.Substring(currentDirectory.Length+1));
var itemGroup = new XElement(xmlns + "ItemGroup");
foreach (var file in relPaths)
{
if (new FileInfo(file).Extension == ".dll")
{
var reference = new XElement(xmlns + "Analyzer");
reference.Add(new XAttribute("Include", file));
itemGroup.Add(reference);
}
if (new FileInfo(file).Extension == ".ruleset")
{
SetOrUpdateProperty(projectContentElement, xmlns, "CodeAnalysisRuleSet", existing => file);
}
}
projectContentElement.Add(itemGroup);
}
private static bool SetOrUpdateProperty(XElement root, XNamespace xmlns, string name, Func<string, string> updater)
{
var element = root.Elements(xmlns + "PropertyGroup").Elements(xmlns + name).FirstOrDefault();
if (element != null)
{
var result = updater(element.Value);
if (result != element.Value)
{
Debug.Log(string.Format("Overriding existing project property {0}. Old value: {1}, new value: {2}", name,
element.Value, result));
element.SetValue(result);
return true;
}
Debug.Log(string.Format("Property {0} already set. Old value: {1}, new value: {2}", name, element.Value, result));
}
else
{
AddProperty(root, xmlns, name, updater(string.Empty));
return true;
}
return false;
}
// Adds a property to the first property group without a condition
private static void AddProperty(XElement root, XNamespace xmlns, string name, string content)
{
Debug.Log(string.Format("Adding project property {0}. Value: {1}", name, content));
var propertyGroup = root.Elements(xmlns + "PropertyGroup")
.FirstOrDefault(e => !e.Attributes(xmlns + "Condition").Any());
if (propertyGroup == null)
{
propertyGroup = new XElement(xmlns + "PropertyGroup");
root.AddFirst(propertyGroup);
}
propertyGroup.Add(new XElement(xmlns + name, content));
}
}
}
Interesting. I'm watching the development of the Incremental Compiler, since it has an option related to analyzers, but it seems to be just a stub for now.
I wonder why they don't like the idea of supporting Roslyn analyzers the same way we're all used to.
Thanks for pasting the code here. I'm sure it will help others!
So is this something you guys think we should include as part of the Analyzer code? it needs to run in the Context of Unity Editor - right now there's no good mechanism for this.
Hey @vad710! Do you confirm that currently custom analyzers effectively can't be used in any IDE (Rider/VS), because Unity regenerates csproj thus removing the 'Analyzer Include="RoslynAnalyzers...' line?
I propose to put the CsprojAssetPostprocessor above in your repo somewhere and make a note in Readme.md, that if anyone wants your roslyn analysers results directly in IDE:
- Add script to Editor folder
- Add dlls with custom analysers to "RoslynAnalyzers" folder in the solution root.
Updated ruleset support
Updated 22.08.2018
using System; using System.IO; using System.Linq; using System.Xml.Linq; using UnityEditor; using UnityEngine; namespace Editor { public class CsprojAssetPostprocessor : AssetPostprocessor { public override int GetPostprocessOrder() { return 20; } private static string[] GetCsprojLinesInSln() { var projectDirectory = Directory.GetParent(Application.dataPath).FullName; var projectName = Path.GetFileName(projectDirectory); var slnFile = Path.GetFullPath(string.Format("{0}.sln" , projectName)); if (!File.Exists(slnFile)) return new string[0]; var slnAllText = File.ReadAllText(slnFile); var lines = slnAllText.Split(new[] {Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries) .Where(a => a.StartsWith("Project(")).ToArray(); return lines; } public static void OnGeneratedCSProjectFiles() { try { // get only csproj files, which are mentioned in sln var lines = GetCsprojLinesInSln(); var currentDirectory = Directory.GetCurrentDirectory(); var projectFiles = Directory.GetFiles(currentDirectory, "*.csproj") .Where(csprojFile => lines.Any(line => line.Contains("\"" + Path.GetFileName(csprojFile) + "\""))).ToArray(); foreach (var file in projectFiles) { UpgradeProjectFile(file); } } catch (Exception e) { // unhandled exception kills editor Debug.LogError(e); } } private static void UpgradeProjectFile(string projectFile) { XDocument doc; try { doc = XDocument.Load(projectFile); } catch (Exception) { Debug.LogError(string.Format("Failed to Load {0}", projectFile)); return; } var projectContentElement = doc.Root; XNamespace xmlns = projectContentElement.Name.NamespaceName; // do not use var SetRoslynAnalyzers(projectContentElement, xmlns); doc.Save(projectFile); } // add everything from RoslyAnalyzers folder to csproj //<ItemGroup><Analyzer Include="RoslynAnalyzers\UnityEngineAnalyzer.1.0.0.0\analyzers\dotnet\cs\UnityEngineAnalyzer.dll" /></ItemGroup> //<CodeAnalysisRuleSet>..\path\to\myrules.ruleset</CodeAnalysisRuleSet> private static void SetRoslynAnalyzers(XElement projectContentElement, XNamespace xmlns) { var currentDirectory = Directory.GetCurrentDirectory(); var roslynAnalysersBaseDir = new DirectoryInfo(Path.Combine(currentDirectory, "RoslynAnalyzers")); if (!roslynAnalysersBaseDir.Exists) return; var relPaths = roslynAnalysersBaseDir.GetFiles("*", SearchOption.AllDirectories) .Select(x => x.FullName.Substring(currentDirectory.Length+1)); var itemGroup = new XElement(xmlns + "ItemGroup"); foreach (var file in relPaths) { if (new FileInfo(file).Extension == ".dll") { var reference = new XElement(xmlns + "Analyzer"); reference.Add(new XAttribute("Include", file)); itemGroup.Add(reference); } if (new FileInfo(file).Extension == ".ruleset") { SetOrUpdateProperty(projectContentElement, xmlns, "CodeAnalysisRuleSet", existing => file); } } projectContentElement.Add(itemGroup); } private static bool SetOrUpdateProperty(XElement root, XNamespace xmlns, string name, Func<string, string> updater) { var element = root.Elements(xmlns + "PropertyGroup").Elements(xmlns + name).FirstOrDefault(); if (element != null) { var result = updater(element.Value); if (result != element.Value) { Debug.Log(string.Format("Overriding existing project property {0}. Old value: {1}, new value: {2}", name, element.Value, result)); element.SetValue(result); return true; } Debug.Log(string.Format("Property {0} already set. Old value: {1}, new value: {2}", name, element.Value, result)); } else { AddProperty(root, xmlns, name, updater(string.Empty)); return true; } return false; } // Adds a property to the first property group without a condition private static void AddProperty(XElement root, XNamespace xmlns, string name, string content) { Debug.Log(string.Format("Adding project property {0}. Value: {1}", name, content)); var propertyGroup = root.Elements(xmlns + "PropertyGroup") .FirstOrDefault(e => !e.Attributes(xmlns + "Condition").Any()); if (propertyGroup == null) { propertyGroup = new XElement(xmlns + "PropertyGroup"); root.AddFirst(propertyGroup); } propertyGroup.Add(new XElement(xmlns + name, content)); } } }
The use of the namespace 'Editor' can cause compilation errors because 'Editor' is a class in the 'UnityEditor' namespace.
using UnityEngine;
using UnityEditor;
namespace MyNamespace
{
public class MyCustomEditor : Editor // <-- Causes compilation error 'namespace is used like a type'
{
}
}
Update: Rider package 1.1.3+ brings support for this using csc.rsp arguments.
See more here JetBrains/resharper-unity#1337 (comment)
Late to the party but why can't you just add your analyzer package reference with Directory.Build.props like this https://rider-support.jetbrains.com/hc/en-us/community/posts/360002398539/comments/360001394920
@danielakl That may actually work. Have you tried? A reference to Directory.Build.props would not be preserved, when sln is regenerated by Unity. However, I guess, it is optional, right?
@van800 Yes, I am using it now and it seems like it works. I have only tried this with JetBrains Rider. The only issue so far is that changing the ruleset doesn't seem to apply until reloading the solution.
No need to reference Directory.Build.props in the .sln or .csproj so it won't be affected by Unity regenerating.
I have added your concern about change in Directory.Build.props is not instantly applied to this request: https://youtrack.jetbrains.com/issue/RIDER-24559#focus=streamItem-27-3948862.0-0
Please follow up there.
@danielakl Did you find a way to make it only showing up in Rider? I've added a .props
file and now all the errors are showing up in Unity, too. We don't want that because it delays compile time a lot.
@marcelwooga No, did not find a good way to do this.
Also my solution doesn't really work when you add certain dependencies. The analyser will start to find errors in external libraries that are added as projects.
@danielakl if you are using msbuild
you can create a .editorconfig
file to exclude folders from the analysis during build time. I use the generated_code
settings to first exclude all C# files and then only include my script folder. The only problem is that Rider doesn't seem to respect those files and still scans the whole solution when doing "live analysis".