From adebec4dd4d3bf4b45f23377a6ab1525fa2d78ef Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard <github@jplamondonw.com>
Date: Sat, 3 Mar 2018 17:49:24 -0500
Subject: automatically detect broken code (#453)

---
 src/SMAPI/Framework/ModLoading/AssemblyLoader.cs   |   5 +-
 .../Finders/ReferenceToMissingMemberFinder.cs      |  87 ++++++++++++++
 .../ReferenceToMemberWithUnexpectedTypeFinder.cs   | 131 +++++++++++++++++++++
 3 files changed, 222 insertions(+), 1 deletion(-)
 create mode 100644 src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs
 create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/ReferenceToMemberWithUnexpectedTypeFinder.cs

(limited to 'src/SMAPI/Framework/ModLoading')

diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
index 7dcf94bf..f0e186a1 100644
--- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
+++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
@@ -238,10 +238,13 @@ namespace StardewModdingAPI.Framework.ModLoading
 
                 // check CIL instructions
                 ILProcessor cil = method.Body.GetILProcessor();
-                foreach (Instruction instruction in cil.Body.Instructions.ToArray())
+                var instructions = cil.Body.Instructions;
+                // ReSharper disable once ForCanBeConvertedToForeach -- deliberate access by index so each handler sees replacements from previous handlers
+                for (int offset = 0; offset < instructions.Count; offset++)
                 {
                     foreach (IInstructionHandler handler in handlers)
                     {
+                        Instruction instruction = instructions[offset];
                         InstructionHandleResult result = handler.Handle(module, cil, instruction, this.AssemblyMap, platformChanged);
                         this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename);
                         if (result == InstructionHandleResult.Rewritten)
diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs
new file mode 100644
index 00000000..60373c9d
--- /dev/null
+++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs
@@ -0,0 +1,87 @@
+using System.Linq;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+
+namespace StardewModdingAPI.Framework.ModLoading.Finders
+{
+    /// <summary>Finds references to a field, property, or method which no longer exists.</summary>
+    /// <remarks>This implementation is purely heuristic. It should never return a false positive, but won't detect all cases.</remarks>
+    internal class ReferenceToMissingMemberFinder : IInstructionHandler
+    {
+        /*********
+        ** Accessors
+        *********/
+        /// <summary>A brief noun phrase indicating what the instruction finder matches.</summary>
+        public string NounPhrase { get; private set; } = "";
+
+
+        /*********
+        ** Public methods
+        *********/
+        /// <summary>Perform the predefined logic for a method if applicable.</summary>
+        /// <param name="module">The assembly module containing the instruction.</param>
+        /// <param name="method">The method definition containing the instruction.</param>
+        /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
+        /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
+        public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged)
+        {
+            return InstructionHandleResult.None;
+        }
+
+        /// <summary>Perform the predefined logic for an instruction if applicable.</summary>
+        /// <param name="module">The assembly module containing the instruction.</param>
+        /// <param name="cil">The CIL processor.</param>
+        /// <param name="instruction">The instruction to handle.</param>
+        /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
+        /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
+        public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged)
+        {
+            // field reference
+            FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
+            if (fieldRef != null)
+            {
+                FieldDefinition target = fieldRef.DeclaringType.Resolve()?.Fields.FirstOrDefault(p => p.Name == fieldRef.Name);
+                if (target == null)
+                {
+                    this.NounPhrase = $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (no such field)";
+                    return InstructionHandleResult.NotCompatible;
+                }
+            }
+
+            // method reference
+            MethodReference methodRef = RewriteHelper.AsMethodReference(instruction);
+            if (methodRef != null && !this.IsUnsupported(methodRef))
+            {
+                MethodDefinition target = methodRef.DeclaringType.Resolve()?.Methods.FirstOrDefault(p => p.Name == methodRef.Name);
+                if (target == null)
+                {
+                    this.NounPhrase = this.IsProperty(methodRef)
+                        ? $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name.Substring(4)} (no such property)"
+                        : $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no such method)";
+                    return InstructionHandleResult.NotCompatible;
+                }
+            }
+
+            return InstructionHandleResult.None;
+        }
+
+
+        /*********
+        ** Private methods
+        *********/
+        /// <summary>Get whether a method reference is a special case that's not currently supported (e.g. array methods).</summary>
+        /// <param name="method">The method reference.</param>
+        private bool IsUnsupported(MethodReference method)
+        {
+            return
+                method.DeclaringType.Name.Contains("["); // array methods
+        }
+
+        /// <summary>Get whether a method reference is a property getter or setter.</summary>
+        /// <param name="method">The method reference.</param>
+        private bool IsProperty(MethodReference method)
+        {
+            return method.Name.StartsWith("get_") || method.Name.StartsWith("set_");
+        }
+    }
+}
diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Rewriters/ReferenceToMemberWithUnexpectedTypeFinder.cs
new file mode 100644
index 00000000..f2080e40
--- /dev/null
+++ b/src/SMAPI/Framework/ModLoading/Rewriters/ReferenceToMemberWithUnexpectedTypeFinder.cs
@@ -0,0 +1,131 @@
+using System.Linq;
+using System.Text.RegularExpressions;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+
+namespace StardewModdingAPI.Framework.ModLoading.Rewriters
+{
+    /// <summary>Finds references to a field, property, or method which returns a different type than the code expects.</summary>
+    /// <remarks>This implementation is purely heuristic. It should never return a false positive, but won't detect all cases.</remarks>
+    internal class ReferenceToMemberWithUnexpectedTypeFinder : IInstructionHandler
+    {
+        /*********
+        ** Properties
+        *********/
+        /// <summary>A pattern matching type name substrings to strip for display.</summary>
+        private readonly Regex StripTypeNamePattern = new Regex(@"`\d+(?=<)", RegexOptions.Compiled);
+
+
+        /*********
+        ** Accessors
+        *********/
+        /// <summary>A brief noun phrase indicating what the instruction finder matches.</summary>
+        public string NounPhrase { get; private set; } = "";
+
+
+        /*********
+        ** Public methods
+        *********/
+        /// <summary>Perform the predefined logic for a method if applicable.</summary>
+        /// <param name="module">The assembly module containing the instruction.</param>
+        /// <param name="method">The method definition containing the instruction.</param>
+        /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
+        /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
+        public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged)
+        {
+            return InstructionHandleResult.None;
+        }
+
+        /// <summary>Perform the predefined logic for an instruction if applicable.</summary>
+        /// <param name="module">The assembly module containing the instruction.</param>
+        /// <param name="cil">The CIL processor.</param>
+        /// <param name="instruction">The instruction to handle.</param>
+        /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
+        /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
+        public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged)
+        {
+            // field reference
+            FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
+            if (fieldRef != null)
+            {
+                // get target field
+                FieldDefinition targetField = fieldRef.DeclaringType.Resolve()?.Fields.FirstOrDefault(p => p.Name == fieldRef.Name);
+                if (targetField == null)
+                    return InstructionHandleResult.None;
+
+                // validate return type
+                string actualReturnTypeID = this.GetComparableTypeID(targetField.FieldType);
+                string expectedReturnTypeID = this.GetComparableTypeID(fieldRef.FieldType);
+                if (actualReturnTypeID != expectedReturnTypeID)
+                {
+                    this.NounPhrase = $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (field returns {this.GetFriendlyTypeName(targetField.FieldType, actualReturnTypeID)}, not {this.GetFriendlyTypeName(fieldRef.FieldType, expectedReturnTypeID)})";
+                    return InstructionHandleResult.NotCompatible;
+                }
+            }
+
+            // method reference
+            MethodReference methodReference = RewriteHelper.AsMethodReference(instruction);
+            if (methodReference != null)
+            {
+                // get potential targets
+                MethodDefinition[] candidateMethods = methodReference.DeclaringType.Resolve()?.Methods.Where(found => found.Name == methodReference.Name).ToArray();
+                if (candidateMethods == null || !candidateMethods.Any())
+                    return InstructionHandleResult.None;
+
+                // compare return types
+                MethodDefinition methodDef = methodReference.Resolve();
+                if (methodDef == null)
+                {
+                    this.NounPhrase = $"reference to {methodReference.DeclaringType.FullName}.{methodReference.Name} (no such method)";
+                    return InstructionHandleResult.NotCompatible;
+                }
+
+                string expectedReturnType = this.GetComparableTypeID(methodDef.ReturnType);
+                if (candidateMethods.All(method => this.GetComparableTypeID(method.ReturnType) != expectedReturnType))
+                {
+                    this.NounPhrase = $"reference to {methodDef.DeclaringType.FullName}.{methodDef.Name} (no such method returns {this.GetFriendlyTypeName(methodDef.ReturnType, expectedReturnType)})";
+                    return InstructionHandleResult.NotCompatible;
+                }
+            }
+
+            return InstructionHandleResult.None;
+        }
+
+
+        /*********
+        ** Private methods
+        *********/
+        /// <summary>Get a unique string representation of a type.</summary>
+        /// <param name="type">The type reference.</param>
+        private string GetComparableTypeID(TypeReference type)
+        {
+            return this.StripTypeNamePattern.Replace(type.FullName, "");
+        }
+
+        /// <summary>Get a shorter type name for display.</summary>
+        /// <param name="type">The type reference.</param>
+        /// <param name="typeID">The comparable type ID from <see cref="GetComparableTypeID"/>.</param>
+        private string GetFriendlyTypeName(TypeReference type, string typeID)
+        {
+            // most common built-in types
+            switch (type.FullName)
+            {
+                case "System.Boolean":
+                    return "bool";
+                case "System.Int32":
+                    return "int";
+                case "System.String":
+                    return "string";
+            }
+
+            // most common unambiguous namespaces
+            foreach (string @namespace in new[] { "Microsoft.Xna.Framework", "Netcode", "System", "System.Collections.Generic" })
+            {
+                if (type.Namespace == @namespace)
+                    return typeID.Substring(@namespace.Length + 1);
+            }
+
+            return typeID;
+        }
+    }
+}
-- 
cgit