@@ -47,6 +47,9 @@ public static AnalysisResult Analyze(
4747 // Analyze dead-end states
4848 AnalyzeDeadEndStates ( states , initialState , result ) ;
4949
50+ // Analyze unused triggers
51+ AnalyzeUnusedTriggers ( states , result ) ;
52+
5053 return result ;
5154 }
5255
@@ -277,6 +280,89 @@ private static void AnalyzeDeadEndStates(
277280 }
278281 }
279282
283+ /// <summary>
284+ /// Detect trigger types that are defined but never used in any transition.
285+ /// </summary>
286+ private static void AnalyzeUnusedTriggers (
287+ IReadOnlyDictionary < TState , StateMachine < TState , TTrigger , TData , TCommand > . StateDefinition > states ,
288+ AnalysisResult result )
289+ {
290+ // Collect all used trigger types
291+ var usedTriggers = new HashSet < object > ( ) ;
292+
293+ foreach ( var definition in states . Values )
294+ {
295+ foreach ( var triggerKey in definition . Transitions . Keys )
296+ {
297+ usedTriggers . Add ( triggerKey ) ;
298+ }
299+ }
300+
301+ // Get all possible trigger types from the TTrigger type
302+ var triggerType = typeof ( TTrigger ) ;
303+
304+ // If TTrigger is a sealed record hierarchy, find all derived types
305+ var allTriggerTypes = GetAllTriggerTypes ( triggerType ) ;
306+
307+ // Find unused trigger types
308+ foreach ( var possibleTrigger in allTriggerTypes )
309+ {
310+ // Check if this trigger type is used
311+ bool isUsed = usedTriggers . Any ( t =>
312+ {
313+ // Handle both type-based and value-based triggers
314+ if ( t is Type )
315+ return ( Type ) t == possibleTrigger ;
316+ return t . GetType ( ) == possibleTrigger ;
317+ } ) ;
318+
319+ if ( ! isUsed )
320+ {
321+ result . AddWarning (
322+ $ "Trigger type '{ possibleTrigger . Name } ' is defined but never used in any transition. " +
323+ "Consider removing it or adding transitions that use it." ) ;
324+ }
325+ }
326+ }
327+
328+ private static HashSet < Type > GetAllTriggerTypes ( Type triggerType )
329+ {
330+ var types = new HashSet < Type > ( ) ;
331+
332+ // Add the base trigger type if it's abstract/record
333+ if ( triggerType . IsAbstract || IsRecordType ( triggerType ) )
334+ {
335+ // Find all derived types in the same assembly
336+ var assembly = triggerType . Assembly ;
337+ foreach ( var type in assembly . GetTypes ( ) )
338+ {
339+ // Check if it's a record/sealed record deriving from the trigger type
340+ if ( type != triggerType &&
341+ triggerType . IsAssignableFrom ( type ) &&
342+ ! type . IsAbstract &&
343+ IsRecordType ( type ) )
344+ {
345+ types . Add ( type ) ;
346+ }
347+ }
348+ }
349+
350+ // Always add the trigger type itself as a fallback
351+ if ( types . Count == 0 )
352+ {
353+ types . Add ( triggerType ) ;
354+ }
355+
356+ return types ;
357+ }
358+
359+ private static bool IsRecordType ( Type type )
360+ {
361+ // Records are detected by checking for the generated 'EqualityContract' property
362+ return type . GetProperty ( "EqualityContract" ,
363+ System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Instance ) != null ;
364+ }
365+
280366 private static string GetTriggerTypeName ( object triggerKey )
281367 {
282368 // triggerKey is typically the trigger type or trigger value
0 commit comments