vad710 / UnityEngineAnalyzer

Roslyn Analyzer for Unity3D
MIT License
273 stars 43 forks source link

Preserve Analyzer Include in Unity-genereated csproj? #27

Closed van800 closed 5 years ago

van800 commented 6 years ago

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 https://github.com/JetBrains/resharper-unity/pull/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 commented 6 years ago

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 commented 6 years ago

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 https://github.com/JetBrains/resharper-unity/pull/577 is definitely not their way.

van800 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

Updated ruleset support

sindrijo commented 5 years ago

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' 
    {
        }
}
van800 commented 5 years ago

Update: Rider package 1.1.3+ brings support for this using csc.rsp arguments. See more here https://github.com/JetBrains/resharper-unity/issues/1337#issuecomment-543572527

danielakl commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

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