diff --git a/UnityEngineAnalyzer.CLI/AnalyzerReport.cs b/UnityEngineAnalyzer.CLI/AnalyzerReport.cs index 8dd2add..4444837 100644 --- a/UnityEngineAnalyzer.CLI/AnalyzerReport.cs +++ b/UnityEngineAnalyzer.CLI/AnalyzerReport.cs @@ -26,16 +26,28 @@ public void AppendDiagnostics(IEnumerable diagnosticResults) foreach (var diagnostic in diagnosticResults) { - var locationSpan = diagnostic.Location.SourceSpan; - var lineSpan = diagnostic.Location.SourceTree.GetLineSpan(locationSpan); + var location = diagnostic.Location; + var lineNumber = 0; + var characterPosition = 0; + var fileName = string.Empty; + + if (location != Location.None) + { + var locationSpan = location.SourceSpan; + var lineSpan = location.SourceTree.GetLineSpan(locationSpan); + lineNumber = lineSpan.StartLinePosition.Line; + characterPosition = lineSpan.StartLinePosition.Character; + fileName = location.SourceTree?.FilePath; + } var diagnosticInfo = new DiagnosticInfo { Id = diagnostic.Id, Message = diagnostic.GetMessage(), - FileName = diagnostic.Location.SourceTree.FilePath, - LineNumber = lineSpan.StartLinePosition.Line, - Severity = (DiagnosticInfoSeverity)diagnostic.Severity + FileName = fileName, + LineNumber = lineNumber, + CharacterPosition = characterPosition, + Severity = (DiagnosticInfo.DiagnosticInfoSeverity)diagnostic.Severity }; @@ -50,7 +62,7 @@ public void FinalizeReport(TimeSpan duration) { foreach (var exporter in _exporters) { - exporter.Finish(duration); + exporter.FinalizeExporter(duration); } } @@ -61,5 +73,13 @@ public void InitializeReport(FileInfo projectFile) exporter.InitializeExporter(projectFile); } } + + public void NotifyException(Exception exception) + { + foreach (var exporter in _exporters) + { + exporter.NotifyException(exception); + } + } } } diff --git a/UnityEngineAnalyzer.CLI/Program.cs b/UnityEngineAnalyzer.CLI/Program.cs index 8c04e7a..0fe0c69 100644 --- a/UnityEngineAnalyzer.CLI/Program.cs +++ b/UnityEngineAnalyzer.CLI/Program.cs @@ -3,16 +3,29 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Syntax; using UnityEngineAnalyzer.CLI.Reporting; namespace UnityEngineAnalyzer.CLI { public class Program { + private static readonly Dictionary AvailableExporters = new Dictionary(); + + static Program() + { + AvailableExporters.Add(nameof(JsonAnalyzerExporter), typeof(JsonAnalyzerExporter)); + AvailableExporters.Add(nameof(StandardOutputAnalyzerReporter), typeof(StandardOutputAnalyzerReporter)); + AvailableExporters.Add(nameof(ConsoleAnalyzerExporter), typeof(ConsoleAnalyzerExporter)); + } + + public static void Main(string[] args) { try { + //TODO: Use a proper parser for the commands + if (args.Length <= 0) { return; @@ -25,8 +38,19 @@ public static void Main(string[] args) //NOTE: This could be configurable via the CLI at some point var report = new AnalyzerReport(); - report.AddExporter(new ConsoleAnalyzerExporter()); - report.AddExporter(new JsonAnalyzerExporter()); + + if (args.Length > 1 && AvailableExporters.ContainsKey(args[1])) + { + var exporterInstance = Activator.CreateInstance(AvailableExporters[args[1]]); + report.AddExporter(exporterInstance as IAnalyzerExporter); + } + else + { + //It's generally a good idea to make sure that the Console Exporter is last since it is interactive + report.AddExporter(new JsonAnalyzerExporter()); + report.AddExporter(new ConsoleAnalyzerExporter()); + } + report.InitializeReport(fileInfo); @@ -46,8 +70,6 @@ public static void Main(string[] args) report.FinalizeReport(duration); - Console.WriteLine("Press any key to exit..."); - Console.ReadKey(); } catch (Exception generalException) { @@ -55,9 +77,6 @@ public static void Main(string[] args) Console.WriteLine("There was an exception running the analysis"); Console.WriteLine(generalException.ToString()); } - - - } diff --git a/UnityEngineAnalyzer.CLI/Reporting/ConsoleAnalyzerExporter.cs b/UnityEngineAnalyzer.CLI/Reporting/ConsoleAnalyzerExporter.cs index bb4809a..5a7127f 100644 --- a/UnityEngineAnalyzer.CLI/Reporting/ConsoleAnalyzerExporter.cs +++ b/UnityEngineAnalyzer.CLI/Reporting/ConsoleAnalyzerExporter.cs @@ -3,57 +3,20 @@ namespace UnityEngineAnalyzer.CLI.Reporting { - public class ConsoleAnalyzerExporter : IAnalyzerExporter + public class ConsoleAnalyzerExporter : StandardOutputAnalyzerReporter { - private const string ConsoleSeparator = "\t"; - private const DiagnosticInfoSeverity MinimalSeverity = DiagnosticInfoSeverity.Warning; - public void AppendDiagnostic(DiagnosticInfo diagnosticInfo) - { - if (diagnosticInfo.Severity < MinimalSeverity) - { - return; - } - - Console.Write(diagnosticInfo.Id); - Console.Write(ConsoleSeparator); - - Console.ForegroundColor = ConsoleColorFromSeverity(diagnosticInfo.Severity); - Console.Write(diagnosticInfo.Severity.ToString()); - Console.Write(ConsoleSeparator); - - Console.ForegroundColor = ConsoleColor.Cyan; - Console.Write(diagnosticInfo.Message); - Console.ResetColor(); - Console.Write(ConsoleSeparator); - Console.WriteLine(@"{0}({1})",diagnosticInfo.FileName,diagnosticInfo.LineNumber); - } - - private ConsoleColor ConsoleColorFromSeverity(DiagnosticInfoSeverity severity) - { - switch (severity) - { - case DiagnosticInfoSeverity.Hidden: - return ConsoleColor.Gray; - case DiagnosticInfoSeverity.Info: - return ConsoleColor.Green; - case DiagnosticInfoSeverity.Warning: - return ConsoleColor.Yellow; - case DiagnosticInfoSeverity.Error: - return ConsoleColor.Red; - default: - return ConsoleColor.White; - } - } - - public void Finish(TimeSpan duration) + public override void FinalizeExporter(TimeSpan duration) { Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("Console Export Finished ({0})", duration); Console.ResetColor(); + + Console.WriteLine("Press any key to exit"); + Console.ReadKey(); } - public void InitializeExporter(FileInfo projectFile) + public override void InitializeExporter(FileInfo projectFile) { Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("Unity Syntax Analyzer"); diff --git a/UnityEngineAnalyzer.CLI/Reporting/DiagnosticInfo.cs b/UnityEngineAnalyzer.CLI/Reporting/DiagnosticInfo.cs index e092be6..c2fd93e 100644 --- a/UnityEngineAnalyzer.CLI/Reporting/DiagnosticInfo.cs +++ b/UnityEngineAnalyzer.CLI/Reporting/DiagnosticInfo.cs @@ -1,19 +1,24 @@ namespace UnityEngineAnalyzer.CLI.Reporting { - public enum DiagnosticInfoSeverity - { - Hidden = 0, - Info = 1, - Warning = 2, - Error = 3 - } + public class DiagnosticInfo { + //TODO: Rename this to something like AnalysisResult + public string Id { get; set; } public string Message { get; set; } public string FileName { get; set; } public int LineNumber { get; set; } + public int CharacterPosition { get; set; } public DiagnosticInfoSeverity Severity { get; set; } + + public enum DiagnosticInfoSeverity + { + Hidden = 0, + Info = 1, + Warning = 2, + Error = 3 + } } } \ No newline at end of file diff --git a/UnityEngineAnalyzer.CLI/Reporting/IAnalyzerExporter.cs b/UnityEngineAnalyzer.CLI/Reporting/IAnalyzerExporter.cs index 62b65ad..3aee835 100644 --- a/UnityEngineAnalyzer.CLI/Reporting/IAnalyzerExporter.cs +++ b/UnityEngineAnalyzer.CLI/Reporting/IAnalyzerExporter.cs @@ -6,7 +6,8 @@ namespace UnityEngineAnalyzer.CLI.Reporting public interface IAnalyzerExporter { void AppendDiagnostic(DiagnosticInfo diagnosticInfo); - void Finish(TimeSpan duration); + void FinalizeExporter(TimeSpan duration); void InitializeExporter(FileInfo projectFile); + void NotifyException(Exception exception); } } \ No newline at end of file diff --git a/UnityEngineAnalyzer.CLI/Reporting/JsonAnalyzerExporter.cs b/UnityEngineAnalyzer.CLI/Reporting/JsonAnalyzerExporter.cs index a0e7b6c..4f3c8c3 100644 --- a/UnityEngineAnalyzer.CLI/Reporting/JsonAnalyzerExporter.cs +++ b/UnityEngineAnalyzer.CLI/Reporting/JsonAnalyzerExporter.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using Newtonsoft.Json; @@ -9,11 +10,12 @@ public class JsonAnalyzerExporter : IAnalyzerExporter { private const string JsonReportFileName = "report.json"; private const string HtmlReportFileName = "UnityReport.html"; - private const DiagnosticInfoSeverity MinimalSeverity = DiagnosticInfoSeverity.Warning; + private const DiagnosticInfo.DiagnosticInfoSeverity MinimalSeverity = DiagnosticInfo.DiagnosticInfoSeverity.Warning; private JsonTextWriter _jsonWriter; private readonly JsonSerializer _jsonSerializer = new JsonSerializer(); + private readonly List _exceptions = new List(); private string _destinationReportFile; @@ -25,13 +27,23 @@ public void AppendDiagnostic(DiagnosticInfo diagnosticInfo) } } - public void Finish(TimeSpan duration) + public void FinalizeExporter(TimeSpan duration) { _jsonWriter.WriteEndArray(); + + _jsonWriter.WritePropertyName("Exceptions"); + _jsonWriter.WriteStartArray(); + + foreach (var exception in _exceptions) + { + _jsonSerializer.Serialize(_jsonWriter, exception); + } + _jsonWriter.WriteEndArray(); + _jsonWriter.WriteEndObject(); _jsonWriter.Close(); - + //Console.WriteLine(Process.GetCurrentProcess().StartInfo.WorkingDirectory); File.Copy(HtmlReportFileName, _destinationReportFile, true); //NOTE: This code might be temporary as it assumes that the CLI is being executed interactively @@ -68,5 +80,10 @@ public void InitializeExporter(FileInfo projectFile) _jsonSerializer.Formatting = Formatting.Indented; } + + public void NotifyException(Exception exception) + { + _exceptions.Add(exception); + } } } \ No newline at end of file diff --git a/UnityEngineAnalyzer.CLI/Reporting/StandardOutputAnalyzerReporter.cs b/UnityEngineAnalyzer.CLI/Reporting/StandardOutputAnalyzerReporter.cs new file mode 100644 index 0000000..3be7a79 --- /dev/null +++ b/UnityEngineAnalyzer.CLI/Reporting/StandardOutputAnalyzerReporter.cs @@ -0,0 +1,76 @@ +using System; +using System.IO; + +namespace UnityEngineAnalyzer.CLI.Reporting +{ + public class StandardOutputAnalyzerReporter : IAnalyzerExporter + { + protected const string ConsoleSeparator = "\t"; + protected const DiagnosticInfo.DiagnosticInfoSeverity MinimalSeverity = DiagnosticInfo.DiagnosticInfoSeverity.Warning; + + protected const string FailurePrefix = "# "; + + public void AppendDiagnostic(DiagnosticInfo diagnosticInfo) + { + if (diagnosticInfo.Severity < MinimalSeverity) + { + return; + } + + Console.Write(diagnosticInfo.Id); + Console.Write(ConsoleSeparator); + + Console.ForegroundColor = ConsoleColorFromSeverity(diagnosticInfo.Severity); + Console.Write(diagnosticInfo.Severity.ToString()); + Console.Write(ConsoleSeparator); + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.Write(diagnosticInfo.Message); + Console.ResetColor(); + Console.WriteLine(@"{0}{1}{0}{2},{3}", ConsoleSeparator,diagnosticInfo.FileName, diagnosticInfo.LineNumber, diagnosticInfo.CharacterPosition); + + } + + private ConsoleColor ConsoleColorFromSeverity(DiagnosticInfo.DiagnosticInfoSeverity severity) + { + switch (severity) + { + case DiagnosticInfo.DiagnosticInfoSeverity.Hidden: + return ConsoleColor.Gray; + case DiagnosticInfo.DiagnosticInfoSeverity.Info: + return ConsoleColor.Green; + case DiagnosticInfo.DiagnosticInfoSeverity.Warning: + return ConsoleColor.Yellow; + case DiagnosticInfo.DiagnosticInfoSeverity.Error: + return ConsoleColor.Red; + default: + return ConsoleColor.White; + } + } + + public virtual void FinalizeExporter(TimeSpan duration) + { + } + + public virtual void InitializeExporter(FileInfo projectFile) + { + } + + public virtual void NotifyException(Exception exception) + { + Console.ForegroundColor = ConsoleColor.Red; + + var delimeters = new[] {"\r", "\n", Environment.NewLine }; + var exceptionLines = exception.ToString().Split(delimeters, StringSplitOptions.RemoveEmptyEntries); + + foreach (var exceptionLine in exceptionLines) + { + Console.WriteLine(FailurePrefix + exceptionLine); + } + + Console.WriteLine(FailurePrefix); + + Console.ResetColor(); + } + } +} diff --git a/UnityEngineAnalyzer.CLI/SolutionAnalyzer.cs b/UnityEngineAnalyzer.CLI/SolutionAnalyzer.cs index 724ee98..0e13ea8 100644 --- a/UnityEngineAnalyzer.CLI/SolutionAnalyzer.cs +++ b/UnityEngineAnalyzer.CLI/SolutionAnalyzer.cs @@ -44,13 +44,22 @@ private ImmutableArray GetAnalyzers() return analyzers; } - private async Task AnalyzeProject(Project project, ImmutableArray analyzers, AnalyzerReport report) + private async Task AnalyzeProject(Project project, ImmutableArray analyzers, + AnalyzerReport report) { - var compilation = await project.GetCompilationAsync(); + try + { + var compilation = await project.GetCompilationAsync(); + + var diagnosticResults = await compilation.WithAnalyzers(analyzers).GetAnalyzerDiagnosticsAsync(); - var diagnosticResults = await compilation.WithAnalyzers(analyzers).GetAnalyzerDiagnosticsAsync(); + report.AppendDiagnostics(diagnosticResults); + } + catch (Exception exception) + { + report.NotifyException(exception); + } - report.AppendDiagnostics(diagnosticResults); } } diff --git a/UnityEngineAnalyzer.CLI/UnityEngineAnalyzer.CLI.csproj b/UnityEngineAnalyzer.CLI/UnityEngineAnalyzer.CLI.csproj index 1e6c031..37697e1 100644 --- a/UnityEngineAnalyzer.CLI/UnityEngineAnalyzer.CLI.csproj +++ b/UnityEngineAnalyzer.CLI/UnityEngineAnalyzer.CLI.csproj @@ -110,6 +110,7 @@ + diff --git a/UnityEngineAnalyzer/UnityEngineAnalyzer/DiagnosticDescriptors.cs b/UnityEngineAnalyzer/UnityEngineAnalyzer/DiagnosticDescriptors.cs index 5e40115..6d21546 100644 --- a/UnityEngineAnalyzer/UnityEngineAnalyzer/DiagnosticDescriptors.cs +++ b/UnityEngineAnalyzer/UnityEngineAnalyzer/DiagnosticDescriptors.cs @@ -5,6 +5,7 @@ using UnityEngineAnalyzer.EmptyMonoBehaviourMethods; using UnityEngineAnalyzer.FindMethodsInUpdate; using UnityEngineAnalyzer.ForEachInUpdate; +using UnityEngineAnalyzer.IL2CPP; using UnityEngineAnalyzer.OnGUI; using UnityEngineAnalyzer.StringMethods; @@ -13,6 +14,7 @@ namespace UnityEngineAnalyzer static class DiagnosticDescriptors { //NOTES: Naming of Descriptors are a bit inconsistant + //NOTES: The Resource Reading code seems repetative public static readonly DiagnosticDescriptor DoNotUseOnGUI = new DiagnosticDescriptor( id: DiagnosticIDs.DoNotUseOnGUI, @@ -114,5 +116,25 @@ static class DiagnosticDescriptors isEnabledByDefault: true, description: new LocalizableResourceString(nameof(DoNotUseForEachInUpdateResources.Description), DoNotUseForEachInUpdateResources.ResourceManager, typeof(DoNotUseForEachInUpdateResources)) ); + + public static readonly DiagnosticDescriptor UnsealedDerivedClass = new DiagnosticDescriptor( + id: DiagnosticIDs.UnsealedDerivedClass, + title: new LocalizableResourceString(nameof(UnsealedDerivedClassResources.Title), UnsealedDerivedClassResources.ResourceManager, typeof(UnsealedDerivedClassResources)), + messageFormat: new LocalizableResourceString(nameof(UnsealedDerivedClassResources.MessageFormat), UnsealedDerivedClassResources.ResourceManager, typeof(UnsealedDerivedClassResources)), + category: DiagnosticCategories.Performance, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: new LocalizableResourceString(nameof(UnsealedDerivedClassResources.Description), UnsealedDerivedClassResources.ResourceManager, typeof(UnsealedDerivedClassResources)) + ); + + public static readonly DiagnosticDescriptor InvokeFunctionMissing = new DiagnosticDescriptor( + id: DiagnosticIDs.InvokeFunctionMissing, + title: new LocalizableResourceString(nameof(InvokeFunctionMissingResources.Title), InvokeFunctionMissingResources.ResourceManager, typeof(InvokeFunctionMissingResources)), + messageFormat: new LocalizableResourceString(nameof(InvokeFunctionMissingResources.MessageFormat), InvokeFunctionMissingResources.ResourceManager, typeof(InvokeFunctionMissingResources)), + category: DiagnosticCategories.Performance, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: new LocalizableResourceString(nameof(InvokeFunctionMissingResources.Description), InvokeFunctionMissingResources.ResourceManager, typeof(InvokeFunctionMissingResources)) + ); } } diff --git a/UnityEngineAnalyzer/UnityEngineAnalyzer/DiagnosticIDs.cs b/UnityEngineAnalyzer/UnityEngineAnalyzer/DiagnosticIDs.cs index aac1077..7c154b4 100644 --- a/UnityEngineAnalyzer/UnityEngineAnalyzer/DiagnosticIDs.cs +++ b/UnityEngineAnalyzer/UnityEngineAnalyzer/DiagnosticIDs.cs @@ -10,11 +10,14 @@ public static class DiagnosticIDs public const string DoNotUseFindMethodsInUpdate = "UEA0005"; public const string DoNotUseCoroutines = "UEA0006"; public const string DoNotUseForEachInUpdate = "UEA0007"; + public const string UnsealedDerivedClass = "UEA0008"; + public const string InvokeFunctionMissing = "UEA0009"; //NOTES: These should probably be on their own analyzer - as they are not specific to Unity public const string DoNotUseRemoting = "AOT0001"; public const string DoNotUseReflectionEmit = "AOT0002"; public const string TypeGetType = "AOT0003"; + } } diff --git a/UnityEngineAnalyzer/UnityEngineAnalyzer/IL2CPP/UnsealedDerivedClassAnalyzer.cs b/UnityEngineAnalyzer/UnityEngineAnalyzer/IL2CPP/UnsealedDerivedClassAnalyzer.cs new file mode 100644 index 0000000..0d5501f --- /dev/null +++ b/UnityEngineAnalyzer/UnityEngineAnalyzer/IL2CPP/UnsealedDerivedClassAnalyzer.cs @@ -0,0 +1,42 @@ +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace UnityEngineAnalyzer.IL2CPP +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class UnsealedDerivedClassAnalyzer : DiagnosticAnalyzer + { + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(AnalyzeClassSyntax, SyntaxKind.ClassDeclaration); + } + + private void AnalyzeClassSyntax(SyntaxNodeAnalysisContext context) + { + var classDeclaration = (ClassDeclarationSyntax)context.Node; + + if (classDeclaration.IsDerived() && !classDeclaration.IsSealed()) + { + var methods = classDeclaration.Members.OfType(); + + foreach (var method in methods) + { + + if (method.IsOverriden() && !method.IsSealed()) + { + var diagnostic = Diagnostic.Create(SupportedDiagnostics.First(), method.GetLocation(), + method.Identifier.ToString(), classDeclaration.Identifier.ToString()); + + context.ReportDiagnostic(diagnostic); + } + } + } + } + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptors.UnsealedDerivedClass); + } +} diff --git a/UnityEngineAnalyzer/UnityEngineAnalyzer/IL2CPP/UnsealedDerivedClassResources.Designer.cs b/UnityEngineAnalyzer/UnityEngineAnalyzer/IL2CPP/UnsealedDerivedClassResources.Designer.cs new file mode 100644 index 0000000..752994c --- /dev/null +++ b/UnityEngineAnalyzer/UnityEngineAnalyzer/IL2CPP/UnsealedDerivedClassResources.Designer.cs @@ -0,0 +1,91 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace UnityEngineAnalyzer.IL2CPP { + using System; + using System.Reflection; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class UnsealedDerivedClassResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal UnsealedDerivedClassResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("UnityEngineAnalyzer.IL2CPP.UnsealedDerivedClassResources", typeof(UnsealedDerivedClassResources).GetTypeInfo().Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Unsealed Methods in Derived classes can impact performance. + /// + internal static string Description { + get { + return ResourceManager.GetString("Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Method {0} in Derived Class {1} is not sealed. Sealing the method or class improves performance.. + /// + internal static string MessageFormat { + get { + return ResourceManager.GetString("MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unsealed Derived Class. + /// + internal static string Title { + get { + return ResourceManager.GetString("Title", resourceCulture); + } + } + } +} diff --git a/UnityEngineAnalyzer/UnityEngineAnalyzer/IL2CPP/UnsealedDerivedClassResources.resx b/UnityEngineAnalyzer/UnityEngineAnalyzer/IL2CPP/UnsealedDerivedClassResources.resx new file mode 100644 index 0000000..cfee10d --- /dev/null +++ b/UnityEngineAnalyzer/UnityEngineAnalyzer/IL2CPP/UnsealedDerivedClassResources.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Unsealed Methods in Derived classes can impact performance + An optional longer localizable description of the diagnostic. + + + Method {0} in Derived Class {1} is not sealed. Sealing the method or class improves performance. + The format-able message the diagnostic displays. + + + Unsealed Derived Class + The title of the diagnostic. + + \ No newline at end of file diff --git a/UnityEngineAnalyzer/UnityEngineAnalyzer/Properties/AssemblyInfo.cs b/UnityEngineAnalyzer/UnityEngineAnalyzer/Properties/AssemblyInfo.cs index 6bfee81..1260997 100644 --- a/UnityEngineAnalyzer/UnityEngineAnalyzer/Properties/AssemblyInfo.cs +++ b/UnityEngineAnalyzer/UnityEngineAnalyzer/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("UnityEngineAnalyzer")] -[assembly: AssemblyCopyright("Copyright © Meng Hui Koh 2016")] +[assembly: AssemblyCopyright("Copyright © Meng Hui Koh, Vinny DaSilva 2016-2017")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/UnityEngineAnalyzer/UnityEngineAnalyzer/RolsynExtensions.cs b/UnityEngineAnalyzer/UnityEngineAnalyzer/RolsynExtensions.cs index d2c84ee..03de4b8 100644 --- a/UnityEngineAnalyzer/UnityEngineAnalyzer/RolsynExtensions.cs +++ b/UnityEngineAnalyzer/UnityEngineAnalyzer/RolsynExtensions.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -65,5 +62,41 @@ public static bool IsOverriden(this MethodDeclarationSyntax methodDeclaration) { return methodDeclaration.Modifiers.Any(m => m.Kind() == SyntaxKind.OverrideKeyword); } + + public static string MethodName(this InvocationExpressionSyntax invocation) + { + + string name; + + if (invocation.Expression is MemberAccessExpressionSyntax) + { + name = ((MemberAccessExpressionSyntax)invocation.Expression).Name.Identifier.ToString(); + } + else if (invocation.Expression is IdentifierNameSyntax) + { + name = ((IdentifierNameSyntax)invocation.Expression).ToString(); + } + else if (invocation.Expression is GenericNameSyntax) + { + name = ((GenericNameSyntax)invocation.Expression).Identifier.ToString(); + } + else + { + throw new ArgumentException("Unable to determine name of method. Invocation is of type: " + invocation.Expression.GetType()); + } + return name; + } + + public static T GetArgumentValue(this ArgumentSyntax argument) + { + //NOTE: Possibly add support for constant parameters in the future + + if (argument.Expression is LiteralExpressionSyntax) + { + var argumentValue = ((LiteralExpressionSyntax)argument.Expression).Token.Value; + return (T)argumentValue; + } + return default(T); + } } } diff --git a/UnityEngineAnalyzer/UnityEngineAnalyzer/StringMethods/DoNotUseStringMethodsAnalyzer.cs b/UnityEngineAnalyzer/UnityEngineAnalyzer/StringMethods/DoNotUseStringMethodsAnalyzer.cs index 3725ee2..d129df2 100644 --- a/UnityEngineAnalyzer/UnityEngineAnalyzer/StringMethods/DoNotUseStringMethodsAnalyzer.cs +++ b/UnityEngineAnalyzer/UnityEngineAnalyzer/StringMethods/DoNotUseStringMethodsAnalyzer.cs @@ -10,7 +10,7 @@ namespace UnityEngineAnalyzer.StringMethods [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class DoNotUseStringMethodsAnalyzer : DiagnosticAnalyzer { - private static readonly ImmutableHashSet StringMethods = ImmutableHashSet.Create("SendMessage", "SendMessageUpwards", "BroadcastMessage", "Invoke", "InvokeRepeating"); + private static readonly ImmutableHashSet StringMethods = ImmutableHashSet.Create("SendMessage", "SendMessageUpwards", "BroadcastMessage"); private static readonly ImmutableHashSet Namespaces = ImmutableHashSet.Create("UnityEngine.Component", "UnityEngine.GameObject", "UnityEngine.MonoBehaviour"); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptors.DoNotUseStringMethods); @@ -28,20 +28,7 @@ private static void AnalyzeNode(SyntaxNodeAnalysisContext context) return; } - - string name = null; - if (invocation.Expression is MemberAccessExpressionSyntax) - { - name = ((MemberAccessExpressionSyntax)invocation.Expression).Name.Identifier.ToString(); - } - else if (invocation.Expression is IdentifierNameSyntax) - { - name = ((IdentifierNameSyntax)invocation.Expression).ToString(); - } - else if (invocation.Expression is GenericNameSyntax) - { - name = ((GenericNameSyntax)invocation.Expression).Identifier.ToString(); - } + var name = invocation.MethodName(); // check if any of the "string" methods are used diff --git a/UnityEngineAnalyzer/UnityEngineAnalyzer/StringMethods/InvokeFunctionMissingAnalyzer.cs b/UnityEngineAnalyzer/UnityEngineAnalyzer/StringMethods/InvokeFunctionMissingAnalyzer.cs new file mode 100644 index 0000000..870d020 --- /dev/null +++ b/UnityEngineAnalyzer/UnityEngineAnalyzer/StringMethods/InvokeFunctionMissingAnalyzer.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace UnityEngineAnalyzer.StringMethods +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class InvokeFunctionMissingAnalyzer : DiagnosticAnalyzer + { + private static readonly ImmutableHashSet InvokeMethods = ImmutableHashSet.Create("Invoke", "InvokeRepeating"); + private static readonly string InvokeMethodTypeName = "UnityEngine.MonoBehaviour"; + + + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); + } + + private void AnalyzeInvocation(SyntaxNodeAnalysisContext context) + { + var invocation = context.Node as InvocationExpressionSyntax; + if (invocation == null) + { + return; + } + + var methodName = invocation.MethodName(); + + if (InvokeMethods.Contains(methodName)) + { + // check if the method is the one from UnityEngine + var symbolInfo = context.SemanticModel.GetSymbolInfo(invocation); + var methodSymbol = symbolInfo.Symbol as IMethodSymbol; + + var fullTypeName = methodSymbol?.ContainingType.ToString(); + + if (fullTypeName == InvokeMethodTypeName && invocation.ArgumentList.Arguments.Count > 0) + { + var firstArgumentExpression = invocation.ArgumentList.Arguments[0]; + + var invokedMethodName = firstArgumentExpression.GetArgumentValue(); + + var containingClassDeclaration = invocation.Ancestors().FirstOrDefault(a => a is ClassDeclarationSyntax) as ClassDeclarationSyntax; + + var allMethods = containingClassDeclaration?.Members.OfType(); + + var invokeEndPoint = allMethods.FirstOrDefault(m => m.Identifier.ValueText == invokedMethodName); + + if (invokeEndPoint == null) + { + var diagnostic = Diagnostic.Create(SupportedDiagnostics.First(), firstArgumentExpression.GetLocation(), + methodName, invokedMethodName); + + context.ReportDiagnostic(diagnostic); + } + } + + + + } + + } + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptors.InvokeFunctionMissing); + } +} \ No newline at end of file diff --git a/UnityEngineAnalyzer/UnityEngineAnalyzer/StringMethods/InvokeFunctionMissingResources.Designer.cs b/UnityEngineAnalyzer/UnityEngineAnalyzer/StringMethods/InvokeFunctionMissingResources.Designer.cs new file mode 100644 index 0000000..25dcb41 --- /dev/null +++ b/UnityEngineAnalyzer/UnityEngineAnalyzer/StringMethods/InvokeFunctionMissingResources.Designer.cs @@ -0,0 +1,91 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace UnityEngineAnalyzer.StringMethods { + using System; + using System.Reflection; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class InvokeFunctionMissingResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal InvokeFunctionMissingResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("UnityEngineAnalyzer.StringMethods.InvokeFunctionMissingResources", typeof(InvokeFunctionMissingResources).GetTypeInfo().Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The function being invoked does not exist. + /// + internal static string Description { + get { + return ResourceManager.GetString("Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The function "{0}" is invoking the method "{1}" that does not exist. + /// + internal static string MessageFormat { + get { + return ResourceManager.GetString("MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invoke Function is Missing. + /// + internal static string Title { + get { + return ResourceManager.GetString("Title", resourceCulture); + } + } + } +} diff --git a/UnityEngineAnalyzer/UnityEngineAnalyzer/StringMethods/InvokeFunctionMissingResources.resx b/UnityEngineAnalyzer/UnityEngineAnalyzer/StringMethods/InvokeFunctionMissingResources.resx new file mode 100644 index 0000000..a33bf05 --- /dev/null +++ b/UnityEngineAnalyzer/UnityEngineAnalyzer/StringMethods/InvokeFunctionMissingResources.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The function being invoked does not exist + + + The function "{0}" is invoking the method "{1}" that does not exist + + + Invoke Function is Missing + + \ No newline at end of file diff --git a/UnityEngineAnalyzer/UnityEngineAnalyzer/UnityEngineAnalyzer.csproj b/UnityEngineAnalyzer/UnityEngineAnalyzer/UnityEngineAnalyzer.csproj index fc59fdc..5ee6832 100644 --- a/UnityEngineAnalyzer/UnityEngineAnalyzer/UnityEngineAnalyzer.csproj +++ b/UnityEngineAnalyzer/UnityEngineAnalyzer/UnityEngineAnalyzer.csproj @@ -83,6 +83,12 @@ DoNotUseForEachInUpdateResources.resx + + + True + True + UnsealedDerivedClassResources.resx + @@ -98,6 +104,12 @@ True DoNotUseStringMethodsResources.resx + + + True + True + InvokeFunctionMissingResources.resx + @@ -130,15 +142,23 @@ ResXFileCodeGenerator - DoNotUseForEachInUpdateResources.Designer.cs + DoNotUseCoroutinesResources.Designer.cs + + + ResXFileCodeGenerator + UnsealedDerivedClassResources.Designer.cs ResXFileCodeGenerator - DoNotUseForEachInUpdateResources.Designer.cs + DoNotUseOnGUIResources.Designer.cs ResXFileCodeGenerator - DoNotUseForEachInUpdateResources.Designer.cs + DoNotUseStringMethodsResources.Designer.cs + + + ResXFileCodeGenerator + InvokeFunctionMissingResources.Designer.cs