Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preserve Analyzer Include in Unity-genereated csproj? #27

Closed
van800 opened this issue Jun 22, 2018 · 16 comments
Closed

Preserve Analyzer Include in Unity-genereated csproj? #27

van800 opened this issue Jun 22, 2018 · 16 comments

Comments

@van800
Copy link

van800 commented Jun 22, 2018

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?

@SugoiDev
Copy link

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.

@van800
Copy link
Author

van800 commented Jun 24, 2018

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.

@van800
Copy link
Author

van800 commented Jun 24, 2018

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));
    }
  }
}

@SugoiDev
Copy link

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!

@vad710
Copy link
Owner

vad710 commented Jun 27, 2018

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.

@van800
Copy link
Author

van800 commented Jun 27, 2018

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:

  1. Add script to Editor folder
  2. Add dlls with custom analysers to "RoslynAnalyzers" folder in the solution root.

40771589-0c106fb2-64be-11e8-9fbf-de78bb31b0d8

@van800
Copy link
Author

van800 commented Aug 21, 2018

Updated ruleset support

@sindrijo
Copy link

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' 
	{
        }
}

nxfs added a commit to thegamefactory/the-world-factory-unity that referenced this issue Jun 11, 2019
See: vad710/UnityEngineAnalyzer#27

This is to be able to configure the Microsoft VS project with
RoslynAnalyzers. In my config I use:
-StyleCop
-FxCop
@van800
Copy link
Author

van800 commented Oct 18, 2019

Update: Rider package 1.1.3+ brings support for this using csc.rsp arguments.
See more here JetBrains/resharper-unity#1337 (comment)

@van800 van800 closed this as completed Oct 18, 2019
@danielakl
Copy link

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

@van800
Copy link
Author

van800 commented Feb 17, 2020

@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?

@danielakl
Copy link

danielakl commented Feb 18, 2020

@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.

@van800
Copy link
Author

van800 commented Feb 18, 2020

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.

@marcelwooga
Copy link

@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.

@danielakl
Copy link

@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.

@marcelwooga
Copy link

@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".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants