├── .DS_Store ├── .gitignore ├── LICENSE.md ├── LICENSE.md.meta ├── README.md ├── README.md.meta ├── Runtime.meta ├── Runtime ├── NonNullAttribute.cs ├── NonNullAttribute.cs.meta ├── co.northplay.nonnull.asmdef └── co.northplay.nonnull.asmdef.meta ├── package.json └── package.json.meta /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulrikdamm/UnityNonNull/c04d716466493765785fe907b88512b26c5ecbb2/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulrikdamm/UnityNonNull/c04d716466493765785fe907b88512b26c5ecbb2/.gitignore -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ulrik Flænø Damm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 381cc387c593f4974ac9d48f21195adf 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UnityNonNull 2 | 3 | Small package to add a `[NonNull]` attribute. Put this on a field to make it highlight in the editor when the field haven't been assigned, or on a class to automatically make it apply to all fields in that class. When you put in on a class, you can also put `[AllowNull]` on specific fields to allow them to be unassigned. 4 | 5 | ![](http://ufd.dk/NonNullEditor.png) 6 | 7 | The script will also produce an error when you play the game in the editor or when you make a build if you have any unassigned references in your scenes. 8 | 9 | ![](http://ufd.dk/NonNullError.png) 10 | 11 | This is handy to make sure something doesn't get unassigned by accident. You can also check wether an object is still in use or not by removing it, and then making a build, and see if you get any errors for unassigned references. 12 | 13 | Fields with unassigned references many times only have one reference you could possibly assign to it; a `GameManager` field probably wants to refer to the `GameManager` MonoBehaviour in the scene, and a `LocalizationsHandler` field probably wants to refer to the single `LocalizationsHandler` ScriptableObject in your assets. In these cases, the editor will show a "fill" button next to the field, which will automatically fill out the reference. If it could possibly refer to multiple things, like a Rigidbody reference, there won't be a fill button, since it can't be automatically filled. 14 | 15 | For value types, the package comes with a `[NonEmpty]` attribute, which can be used to check that lists and strings aren't empty, colors aren't unassigned, enums aren't the default case, numbers aren't zero, and so on. 16 | 17 | ## Installation 18 | 19 | You can drop the NonNullAttribute.cs into your Assets folder (but it does *not* go in the Editor folder), or you can install it via the Unity package manager (recommended!) by adding this line to your Packages/manifest.json file: 20 | 21 | `"co.northplay.nonnull": "https://github.com/ulrikdamm/UnityNonNull.git"` 22 | 23 | And it will appear in the package manager and automatically install into your project. 24 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 62bee17b3d34b448584206ea73844835 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e106c95c9a89e47378ce3f8b5a06fddf 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/NonNullAttribute.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | #if UNITY_EDITOR 4 | using UnityEditor; 5 | using UnityEditor.SceneManagement; 6 | using System.Reflection; 7 | using UnityEditor.Callbacks; 8 | using System.Collections.Generic; 9 | #endif 10 | 11 | [System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Class)] 12 | public class NonNullAttribute : PropertyAttribute {} 13 | 14 | [System.AttributeUsage(System.AttributeTargets.Field)] 15 | public class AllowNullAttribute : PropertyAttribute {} 16 | 17 | [System.AttributeUsage(System.AttributeTargets.Field)] 18 | public class NonEmptyAttribute : PropertyAttribute {} 19 | 20 | #if UNITY_EDITOR 21 | static class NullFieldGUI { 22 | public static void nullCheckedField(Rect position, SerializedProperty property, GUIContent label, bool showWarning) { 23 | if (!showWarning) { 24 | EditorGUI.PropertyField(position, property, label); 25 | return; 26 | } 27 | 28 | GUI.backgroundColor = Color.red; 29 | 30 | string fillButtonText; 31 | var fillCandidate = FindNonNull.findObjectToFill(property, out fillButtonText); 32 | if (fillCandidate == null) { 33 | EditorGUI.PropertyField(position, property, label); 34 | GUI.backgroundColor = Color.white; 35 | return; 36 | } 37 | 38 | var propertyRect = new Rect { x = position.x, y = position.y, width = position.width - 45, height = position.height }; 39 | var buttonRect = new Rect { x = position.x + propertyRect.width + 8, y = position.y, width = 45 - 8, height = position.height }; 40 | 41 | EditorGUI.PropertyField(propertyRect, property, label); 42 | 43 | GUI.backgroundColor = Color.white; 44 | if (GUI.Button(buttonRect, fillButtonText)) { property.objectReferenceValue = fillCandidate; } 45 | } 46 | } 47 | 48 | [CustomPropertyDrawer(typeof(Object), useForChildren: true)] 49 | public class DefaultObjectDrawer : PropertyDrawer { 50 | public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { 51 | EditorGUI.BeginProperty(position, label, property); 52 | 53 | var showWarning = ( 54 | property.propertyType == SerializedPropertyType.ObjectReference 55 | && property.objectReferenceValue == null 56 | && FindNonNull.classHasAttributeOfType(property.serializedObject.targetObject.GetType(), typeof(NonNullAttribute)) 57 | ); 58 | 59 | NullFieldGUI.nullCheckedField(position, property, label, showWarning); 60 | EditorGUI.EndProperty(); 61 | } 62 | } 63 | 64 | [CustomPropertyDrawer(typeof(AllowNullAttribute))] 65 | public class AllowNullAttributeDrawer : PropertyDrawer { 66 | public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { 67 | EditorGUI.BeginProperty(position, label, property); 68 | EditorGUI.PropertyField(position, property, label); 69 | EditorGUI.EndProperty(); 70 | } 71 | } 72 | 73 | [CustomPropertyDrawer(typeof(NonNullAttribute))] 74 | public class NonNullAttributeDrawer : PropertyDrawer { 75 | public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { 76 | EditorGUI.BeginProperty(position, label, property); 77 | var showWarning = (property.propertyType == SerializedPropertyType.ObjectReference && property.objectReferenceValue == null); 78 | NullFieldGUI.nullCheckedField(position, property, label, showWarning); 79 | EditorGUI.EndProperty(); 80 | } 81 | } 82 | 83 | [CustomPropertyDrawer(typeof(NonEmptyAttribute))] 84 | public class NonEmptyAttributeDrawer : PropertyDrawer { 85 | public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { 86 | EditorGUI.BeginProperty(position, label, property); 87 | 88 | bool showWarning; 89 | 90 | switch (property.propertyType) { 91 | case SerializedPropertyType.String: showWarning = string.IsNullOrEmpty(property.stringValue); break; 92 | case SerializedPropertyType.AnimationCurve: showWarning = (property.animationCurveValue == null || property.animationCurveValue.length == 0); break; 93 | case SerializedPropertyType.LayerMask: showWarning = (property.intValue == 0); break; 94 | case SerializedPropertyType.ArraySize: showWarning = (property.arraySize == 0); break; 95 | case SerializedPropertyType.Color: showWarning = (property.colorValue == null || property.colorValue == new Color(0, 0, 0, 0)); break; 96 | case SerializedPropertyType.Enum: showWarning = (property.enumValueIndex == 0); break; 97 | case SerializedPropertyType.Integer: showWarning = (property.intValue == 0); break; 98 | case SerializedPropertyType.Float: showWarning = (Mathf.Approximately((float)property.doubleValue, 0)); break; 99 | case SerializedPropertyType.Vector2: showWarning = property.vector2Value == Vector2.zero; break; 100 | case SerializedPropertyType.Vector3: showWarning = property.vector3Value == Vector3.zero; break; 101 | // case SerializedPropertyType.Vector4: showWarning = property.vector4Value == Vector4.zero; break; 102 | case SerializedPropertyType.Vector2Int: showWarning = property.vector2IntValue == Vector2Int.zero; break; 103 | case SerializedPropertyType.Vector3Int: showWarning = property.vector3IntValue == Vector3Int.zero; break; 104 | default: showWarning = false; break; 105 | } 106 | 107 | NullFieldGUI.nullCheckedField(position, property, label, showWarning); 108 | EditorGUI.EndProperty(); 109 | } 110 | } 111 | 112 | class FindNonNull { 113 | [PostProcessScene] 114 | public static void scenePostProcess() { 115 | if (findAllNonNulls()) { 116 | if (Application.isPlaying) { EditorApplication.isPaused = true; } 117 | } 118 | } 119 | 120 | [MenuItem("Assets/NonNull/Check for unassigned references in current scene")] 121 | public static bool findAllNonNulls() { 122 | var anyNulls = false; 123 | 124 | enumerateAllComponentsInScene((GameObject obj, Component component) => { 125 | nullCheckComponent(obj, component, ref anyNulls); 126 | }); 127 | 128 | return anyNulls; 129 | } 130 | 131 | [MenuItem("Assets/NonNull/Check for unassigned references in all scenes in build settings")] 132 | public static void findAllNonNullsInAllScenes() { 133 | var scenes = EditorBuildSettings.scenes; 134 | 135 | if (scenes.Length == 0) { 136 | Debug.Log("No scenes in build settings, so no scenes checked."); 137 | return; 138 | } 139 | 140 | for (int i = 0; i < scenes.Length; i++) { 141 | var scene = scenes[i]; 142 | 143 | if (EditorUtility.DisplayCancelableProgressBar("Checking all scenes", scene.path, i / (float)scenes.Length)) { 144 | EditorUtility.ClearProgressBar(); 145 | return; 146 | } 147 | 148 | EditorSceneManager.OpenScene(scene.path, OpenSceneMode.Single); 149 | findAllNonNulls(); 150 | } 151 | 152 | EditorUtility.ClearProgressBar(); 153 | } 154 | 155 | static void nullCheckComponent(GameObject obj, Component component, ref bool anyNulls) { 156 | if (component == null) { 157 | logError("Missing script for component", obj); 158 | anyNulls = true; 159 | return; 160 | } 161 | 162 | var componentHasNonNull = classHasAttributeOfType(component.GetType(), typeof(NonNullAttribute)); 163 | var fields = component.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); 164 | 165 | for (var i = 0; i < fields.Length; i++) { 166 | var field = fields[i]; 167 | 168 | var isSerialized = fieldHasAttributeOfType(field, typeof(SerializeField)); 169 | var isPublic = fieldAccessIs(field, FieldAttributes.Public); 170 | if (!isSerialized && !isPublic) { continue; } 171 | 172 | nullCheckField(obj, component, field, componentHasNonNull, ref anyNulls); 173 | emptyCheckField(obj, component, field, ref anyNulls); 174 | } 175 | } 176 | 177 | static void emptyCheckField(GameObject obj, Component component, FieldInfo field, ref bool anyNulls) { 178 | if (!fieldHasAttributeOfType(field, typeof(NonEmptyAttribute))) { return; } 179 | 180 | var fieldValue = field.GetValue(component); 181 | 182 | if (fieldValue is string && string.IsNullOrEmpty((string)fieldValue)) { 183 | logError("Empty string", obj, component, field); 184 | } else if (fieldValue is AnimationCurve && (fieldValue == null || ((AnimationCurve)fieldValue).length == 0)) { 185 | logError("Empty animation curve", obj, component, field); 186 | } else if (fieldValue is LayerMask && (fieldValue == null || ((LayerMask)fieldValue) == 0)) { 187 | logError("Unspecified layer mask", obj, component, field); 188 | } else if (fieldValue is System.Array && (fieldValue == null || ((System.Array)fieldValue).Length == 0)) { 189 | logError("Empty array", obj, component, field); 190 | } else if (fieldValue is System.Collections.IList && (fieldValue == null || ((System.Collections.IList)fieldValue).Count == 0)) { 191 | logError("Empty list", obj, component, field); 192 | } else if (fieldValue is Color && (fieldValue == null || ((Color)fieldValue) == new Color(0, 0, 0, 0))) { 193 | logError("No color", obj, component, field); 194 | } else if (fieldValue != null && fieldValue.GetType().IsEnum && ((int)fieldValue) == 0) { 195 | logError("Empty enum value", obj, component, field); 196 | } else if (fieldValue is int && ((int)fieldValue) == 0) { 197 | logError("Zero integer value", obj, component, field); 198 | } else if (fieldValue is float && Mathf.Approximately((float)fieldValue, 0)) { 199 | logError("Zero float value", obj, component, field); 200 | } else if (fieldValue is double && (double)fieldValue == 0) { 201 | logError("Zero double value", obj, component, field); 202 | } else if (isEmptyVector(fieldValue)) { 203 | logError("Empty vector value", obj, component, field); 204 | } else { 205 | return; 206 | } 207 | 208 | anyNulls = true; 209 | } 210 | 211 | static bool isEmptyVector(object value) { 212 | if (value is Vector2 && (Vector2)value == Vector2.zero) { return true; } 213 | if (value is Vector2Int && (Vector2Int)value == Vector2Int.zero) { return true; } 214 | if (value is Vector3 && (Vector3)value == Vector3.zero) { return true; } 215 | if (value is Vector3Int && (Vector3Int)value == Vector3Int.zero) { return true; } 216 | if (value is Vector4 && (Vector4)value == Vector4.zero) { return true; } 217 | return false; 218 | } 219 | 220 | static void nullCheckField(GameObject obj, Component component, FieldInfo field, bool componentHasNonNull, ref bool anyNulls) { 221 | if (!shouldNullCheckField(field, componentHasNonNull)) { return; } 222 | 223 | var fieldValue = field.GetValue(component); 224 | 225 | if (fieldValue is UnityEngine.Object) { 226 | if (((Object)fieldValue) != null) { return; } 227 | } else { 228 | if (!object.ReferenceEquals(fieldValue, null)) { return; } 229 | } 230 | 231 | logError("Missing reference", obj, component, field); 232 | anyNulls = true; 233 | } 234 | 235 | static bool shouldNullCheckField(FieldInfo field, bool componentHasNonNull) { 236 | if (!field.FieldType.IsClass) { return false; } 237 | 238 | if (!componentHasNonNull) { 239 | if (!fieldHasAttributeOfType(field, typeof(NonNullAttribute))) { return false; } 240 | } else { 241 | if (fieldHasAttributeOfType(field, typeof(AllowNullAttribute))) { return false; } 242 | } 243 | 244 | return true; 245 | } 246 | 247 | public static bool classHasAttributeOfType(System.Type classType, System.Type ofType) { 248 | return (classType.GetCustomAttributes(ofType, false).Length > 0); 249 | } 250 | 251 | static bool fieldAccessIs(FieldInfo field, FieldAttributes attribute) { 252 | return ((field.Attributes & FieldAttributes.FieldAccessMask) == attribute); 253 | } 254 | 255 | static bool fieldHasAttributeOfType(FieldInfo field, System.Type type) { 256 | return (field.GetCustomAttributes(type, false).Length > 0); 257 | } 258 | 259 | static void enumerateAllComponentsInScene(System.Action callback) { 260 | enumerateAllGameObjectsInScene(obj => { 261 | var components = obj.GetComponents(); 262 | for (var i = 0; i < components.Length; i++) { 263 | callback(obj, components[i]); 264 | } 265 | }); 266 | } 267 | 268 | static void enumerateAllGameObjectsInScene(System.Action callback) { 269 | var rootObjects = EditorSceneManager.GetActiveScene().GetRootGameObjects(); 270 | 271 | foreach (var rootObject in rootObjects) { 272 | enumerateChildrenOf(rootObject, callback); 273 | } 274 | } 275 | 276 | static void enumerateChildrenOf(GameObject obj, System.Action callback) { 277 | callback(obj); 278 | 279 | for (var i = 0; i < obj.transform.childCount; i++) { 280 | enumerateChildrenOf(obj.transform.GetChild(i).gameObject, callback); 281 | } 282 | } 283 | 284 | static FieldInfo getField(string propertyPath, System.Type fromType) { 285 | var field = fromType.GetField(propertyPath, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance); 286 | if (field != null) { return field; } 287 | 288 | var baseType = fromType.BaseType; 289 | if (baseType != null && baseType != typeof(object)) { return getField(propertyPath, baseType); } 290 | 291 | return null; 292 | } 293 | 294 | public static Object findObjectToFill(SerializedProperty property, out string actionName) { 295 | actionName = null; 296 | if (property.propertyType != SerializedPropertyType.ObjectReference) { return null; } 297 | if (property.propertyPath.Contains(".Array")) { return null; } 298 | 299 | var objectType = property.serializedObject.targetObject.GetType(); 300 | var field = getField(property.propertyPath, fromType: objectType); 301 | if (field == null) { return null; } 302 | 303 | var fieldType = field.FieldType; 304 | 305 | if (fieldType.IsSubclassOf(typeof(Component))) { 306 | var component = property.serializedObject.targetObject as Component; 307 | if (component != null) { 308 | var components = component.GetComponents(fieldType); 309 | if (components.Length == 1) { 310 | actionName = "This"; 311 | return components[0]; 312 | } 313 | } 314 | 315 | actionName = "Fill"; 316 | return findSceneObjectToFill(fieldType); 317 | } 318 | 319 | if (fieldType.IsSubclassOf(typeof(ScriptableObject))) { 320 | actionName = "Fill"; 321 | return findAssetObjectToFill(fieldType); 322 | } 323 | 324 | return null; 325 | } 326 | 327 | static Object findSceneObjectToFill(System.Type fieldType) { 328 | Object objectInScene = null; 329 | 330 | var rootObjects = EditorSceneManager.GetActiveScene().GetRootGameObjects(); 331 | for (var i = 0; i < rootObjects.Length; i++) { 332 | var candidates = rootObjects[i].GetComponentsInChildren(fieldType, includeInactive: true); 333 | if (candidates.Length > 1) { return null; } 334 | if (candidates.Length == 1 && objectInScene != null) { return null; } 335 | if (candidates.Length == 1) { objectInScene = candidates[0]; } 336 | } 337 | 338 | return objectInScene; 339 | } 340 | 341 | static Object findAssetObjectToFill(System.Type fieldType) { 342 | var objectsInAssets = AssetDatabase.FindAssets("t:" + fieldType.Name); 343 | if (objectsInAssets.Length != 1) { return null; } 344 | 345 | var assetId = objectsInAssets[0]; 346 | var path = AssetDatabase.GUIDToAssetPath(assetId); 347 | return AssetDatabase.LoadAssetAtPath(path, fieldType); 348 | } 349 | 350 | static void logError(string error, GameObject obj, Component component, FieldInfo field) { 351 | Debug.LogError(error + " for " + field.Name + " in " + component.GetType().Name + " on " + obj.name + " in scene " + EditorSceneManager.GetActiveScene().name, component); 352 | } 353 | 354 | static void logError(string error, GameObject obj) { 355 | Debug.LogError(error + " on " + obj.name + " in scene " + EditorSceneManager.GetActiveScene().name, obj); 356 | } 357 | } 358 | #endif 359 | -------------------------------------------------------------------------------- /Runtime/NonNullAttribute.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0d3cf1bee27ad4c65bdbe7ea5dd3c17c 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/co.northplay.nonnull.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NonNull" 3 | } 4 | -------------------------------------------------------------------------------- /Runtime/co.northplay.nonnull.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: dd9767582f71d4c0494d985bdef9d4c5 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "co.northplay.nonnull", 3 | "displayName": "NonNull attribute", 4 | "version": "1.0.0", 5 | "unity": "2018.1", 6 | "description": "Includes the [NonNull] attribute for fields or classes, which allows visual indication in the editor that a reference is unassigned, and errors when starting the game or building the player. Also includes [AllowsNull] for fields in [NonNull] classes that can be unassigned.", 7 | "keywords": ["attribute", "null", "nonnull", "editor"], 8 | "category": "editor", 9 | "dependencies": {} 10 | } -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b3e05b736b6b74e8f9bf430b9da45298 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------