UnitAnalyser.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using UnityEngine;
  5. namespace Unity.VisualScripting
  6. {
  7. [Analyser(typeof(IUnit))]
  8. public class UnitAnalyser<TUnit> : Analyser<TUnit, UnitAnalysis>
  9. where TUnit : class, IUnit
  10. {
  11. public UnitAnalyser(GraphReference reference, TUnit target) : base(reference, target) { }
  12. public TUnit unit => target;
  13. [Assigns]
  14. protected bool IsEntered()
  15. {
  16. using (var recursion = Recursion.New(1))
  17. {
  18. return IsEntered(unit, recursion);
  19. }
  20. }
  21. private static bool IsEntered(IUnit unit, Recursion recursion)
  22. {
  23. if (unit.isControlRoot)
  24. {
  25. return true;
  26. }
  27. foreach (var controlInput in unit.controlInputs)
  28. {
  29. if (!controlInput.isPredictable || controlInput.couldBeEntered)
  30. {
  31. return true;
  32. }
  33. }
  34. foreach (var valueOutput in unit.valueOutputs)
  35. {
  36. if (!recursion?.TryEnter(valueOutput) ?? false)
  37. {
  38. continue;
  39. }
  40. var valueOutputEntered = valueOutput.validConnections.Any(c => IsEntered(c.destination.unit, recursion));
  41. recursion?.Exit(valueOutput);
  42. if (valueOutputEntered)
  43. {
  44. return true;
  45. }
  46. }
  47. return false;
  48. }
  49. private string PortLabel(IUnitPort port)
  50. {
  51. return port.Description<UnitPortDescription>().label;
  52. }
  53. [Assigns]
  54. protected virtual IEnumerable<Warning> Warnings()
  55. {
  56. var isEntered = IsEntered();
  57. if (!unit.isDefined)
  58. {
  59. if (unit.definitionException != null)
  60. {
  61. yield return Warning.Exception(unit.definitionException);
  62. }
  63. else if (!unit.canDefine)
  64. {
  65. yield return Warning.Caution("Node is not properly configured.");
  66. }
  67. }
  68. if (!isEntered)
  69. {
  70. yield return Warning.Info("Node is never entered.");
  71. }
  72. // Obsolete attribute is not inherited, so traverse the chain manually
  73. var obsoleteAttribute = unit.GetType().AndHierarchy().FirstOrDefault(t => t.HasAttribute<ObsoleteAttribute>())?.GetAttribute<ObsoleteAttribute>();
  74. if (obsoleteAttribute != null)
  75. {
  76. var unitName = BoltFlowNameUtility.UnitTitle(unit.GetType(), true, false);
  77. if (obsoleteAttribute.Message != null)
  78. {
  79. Debug.LogWarning($"\"{unitName}\" node is deprecated: {obsoleteAttribute.Message}");
  80. yield return Warning.Caution($"Deprecated: {obsoleteAttribute.Message}");
  81. }
  82. else
  83. {
  84. Debug.LogWarning($"\"{unitName}\" node is deprecated.");
  85. yield return Warning.Caution("This node is deprecated.");
  86. }
  87. }
  88. if (unit.isDefined)
  89. {
  90. foreach (var invalidInput in unit.invalidInputs)
  91. {
  92. yield return Warning.Caution($"{PortLabel(invalidInput)} is not used by this unit.");
  93. }
  94. foreach (var invalidOutput in unit.invalidOutputs)
  95. {
  96. yield return Warning.Caution($"{PortLabel(invalidOutput)} is not provided by this unit.");
  97. }
  98. foreach (var validPort in unit.validPorts)
  99. {
  100. if (validPort.hasInvalidConnection)
  101. {
  102. yield return Warning.Caution($"{PortLabel(validPort)} has an invalid connection.");
  103. }
  104. }
  105. #if UNITY_IOS || UNITY_ANDROID || UNITY_TVOS
  106. if (unit is IMouseEventUnit)
  107. {
  108. var graphName = string.IsNullOrEmpty(unit.graph.title) ? "A ScriptGraph" : $"The ScriptGraph {unit.graph.title}";
  109. var unitName = BoltFlowNameUtility.UnitTitle(unit.GetType(), true, false);
  110. Debug.LogWarning($"{graphName} contains a {unitName} node. Presence of MouseEvent nodes might impact performance on handheld devices.");
  111. yield return Warning.Caution("Presence of MouseEvent nodes might impact performance on handheld devices.");
  112. }
  113. #endif
  114. }
  115. foreach (var controlInput in unit.controlInputs)
  116. {
  117. if (!controlInput.hasValidConnection)
  118. {
  119. continue;
  120. }
  121. foreach (var relation in controlInput.relations)
  122. {
  123. if (relation.source is ValueInput)
  124. {
  125. var valueInput = (ValueInput)relation.source;
  126. foreach (var warning in ValueInputWarnings(valueInput))
  127. {
  128. yield return warning;
  129. }
  130. }
  131. }
  132. }
  133. foreach (var controlOutput in unit.controlOutputs)
  134. {
  135. if (!controlOutput.hasValidConnection)
  136. {
  137. continue;
  138. }
  139. var controlInputs = controlOutput.relations.Select(r => r.source).OfType<ControlInput>();
  140. var isTriggered = !controlInputs.Any() || controlInputs.Any(ci => !ci.isPredictable || ci.couldBeEntered);
  141. foreach (var relation in controlOutput.relations)
  142. {
  143. if (relation.source is ValueInput)
  144. {
  145. var valueInput = (ValueInput)relation.source;
  146. foreach (var warning in ValueInputWarnings(valueInput))
  147. {
  148. yield return warning;
  149. }
  150. }
  151. }
  152. if (isEntered && !isTriggered)
  153. {
  154. yield return Warning.Caution($"{PortLabel(controlOutput)} is connected, but it is never triggered.");
  155. }
  156. }
  157. foreach (var valueOutput in unit.valueOutputs)
  158. {
  159. if (!valueOutput.hasValidConnection)
  160. {
  161. continue;
  162. }
  163. foreach (var relation in valueOutput.relations)
  164. {
  165. if (relation.source is ControlInput)
  166. {
  167. var controlInput = (ControlInput)relation.source;
  168. if (isEntered && controlInput.isPredictable && !controlInput.couldBeEntered)
  169. {
  170. yield return Warning.Severe($"{PortLabel(controlInput)} is required, but it is never entered.");
  171. }
  172. }
  173. else if (relation.source is ValueInput)
  174. {
  175. var valueInput = (ValueInput)relation.source;
  176. foreach (var warning in ValueInputWarnings(valueInput))
  177. {
  178. yield return warning;
  179. }
  180. }
  181. }
  182. }
  183. }
  184. private IEnumerable<Warning> ValueInputWarnings(ValueInput valueInput)
  185. {
  186. // We can disable null reference check if no self is available
  187. // and the port requires an owner, for example in macros.
  188. var trustFutureOwner = valueInput.nullMeansSelf && reference.self == null;
  189. var checkForNullReference = BoltFlow.Configuration.predictPotentialNullReferences && !valueInput.allowsNull && !trustFutureOwner;
  190. var checkForMissingComponent = BoltFlow.Configuration.predictPotentialMissingComponents && typeof(Component).IsAssignableFrom(valueInput.type);
  191. // Note that we cannot directly check the input's predicted value, because it
  192. // will return false for safeguard specifically because it might be missing requirements.
  193. // Therefore, we first check the connected value, then the default value.
  194. // If the port is connected to a predictable output, use the connected value to perform checks.
  195. if (valueInput.hasValidConnection)
  196. {
  197. var valueOutput = valueInput.validConnectedPorts.Single();
  198. if (Flow.CanPredict(valueOutput, reference))
  199. {
  200. if (checkForNullReference)
  201. {
  202. if (Flow.Predict(valueOutput, reference) == null)
  203. {
  204. yield return Warning.Severe($"{PortLabel(valueInput)} cannot be null.");
  205. }
  206. }
  207. if (checkForMissingComponent)
  208. {
  209. var connectedPredictedValue = Flow.Predict(valueOutput, reference);
  210. // This check is necessary, because the predicted value could be
  211. // incompatible as connections with non-guaranteed conversions are allowed.
  212. if (ConversionUtility.CanConvert(connectedPredictedValue, typeof(GameObject), true))
  213. {
  214. var gameObject = ConversionUtility.Convert<GameObject>(connectedPredictedValue);
  215. if (gameObject != null)
  216. {
  217. var component = (Component)ConversionUtility.Convert(gameObject, valueInput.type);
  218. if (component == null)
  219. {
  220. yield return Warning.Caution($"{PortLabel(valueInput)} is missing a {valueInput.type.DisplayName()} component.");
  221. }
  222. }
  223. }
  224. }
  225. }
  226. }
  227. // If the port isn't connected but has a default value, use the default value to perform checks.
  228. else if (valueInput.hasDefaultValue)
  229. {
  230. if (checkForNullReference)
  231. {
  232. if (Flow.Predict(valueInput, reference) == null)
  233. {
  234. yield return Warning.Severe($"{PortLabel(valueInput)} cannot be null.");
  235. }
  236. }
  237. if (checkForMissingComponent)
  238. {
  239. var unconnectedPredictedValue = Flow.Predict(valueInput, reference);
  240. if (ConversionUtility.CanConvert(unconnectedPredictedValue, typeof(GameObject), true))
  241. {
  242. var gameObject = ConversionUtility.Convert<GameObject>(unconnectedPredictedValue);
  243. if (gameObject != null)
  244. {
  245. var component = (Component)ConversionUtility.Convert(gameObject, valueInput.type);
  246. if (component == null)
  247. {
  248. yield return Warning.Caution($"{PortLabel(valueInput)} is missing a {valueInput.type.DisplayName()} component.");
  249. }
  250. }
  251. }
  252. }
  253. }
  254. // The value isn't connected and has no default value,
  255. // therefore it is certain to be missing at runtime.
  256. else
  257. {
  258. yield return Warning.Severe($"{PortLabel(valueInput)} is missing.");
  259. }
  260. }
  261. }
  262. }