Source code for pyfcstm.diagnostics.analyzers.design_health

"""Design-health diagnostics derived from inspect-surface data."""

import re
from dataclasses import replace
from typing import TYPE_CHECKING, Iterable, List, Optional

from ..suggested_fix import refs_with_suggested_fix
from ...utils.validate import ModelDiagnostic
from .const_fold import collect_const_fold_warnings
from .data_flow import collect_data_flow_warnings
from .naming import collect_naming_warnings
from .redundancy import collect_redundancy_warnings
from .structural import collect_structural_warnings
from .thresholds import collect_threshold_warnings
from .transition_info import collect_transition_infos
from .type_shape import collect_type_warnings

if TYPE_CHECKING:  # pragma: no cover - import-time type hints only
    from ..inspect import (
        ActionInfo,
        EventInfo,
        ForcedTransitionInfo,
        ModelMetrics,
        StateInfo,
        TransitionInfo,
        VariableInfo,
    )
    from ...model.model import StateMachine


[docs] def collect_design_health_warnings( states: Iterable['StateInfo'], transitions: Iterable['TransitionInfo'], variables: Iterable['VariableInfo'], events: Iterable['EventInfo'], actions: Iterable['ActionInfo'], forced_transitions: Iterable['ForcedTransitionInfo'], metrics: 'ModelMetrics', reachability_graph, root_state_path: Optional[str] = None, deep_hierarchy_threshold: int = 6, large_composite_threshold: int = 12, var_to_leaf_ratio_threshold: float = 2.0, machine: Optional['StateMachine'] = None, ) -> List[ModelDiagnostic]: """Collect design-health warning diagnostics from inspect payloads.""" diagnostics: List[ModelDiagnostic] = [] states = list(states) transitions = list(transitions) variables = list(variables) events = list(events) actions = list(actions) forced_transitions = list(forced_transitions) resolved_root_state_path = _resolve_root_state_path(states, root_state_path) diagnostics.extend(_unreachable_state_diagnostics( states, reachability_graph, resolved_root_state_path, )) diagnostics.extend( collect_const_fold_warnings(machine) if machine is not None else _guard_const_false_diagnostics(transitions) ) diagnostics.extend(_unused_event_diagnostics(events)) diagnostics.extend(collect_structural_warnings( states, transitions, actions, forced_transitions, reachability_graph, root_state_path=resolved_root_state_path, )) diagnostics.extend(collect_threshold_warnings( states, metrics, deep_hierarchy_threshold=deep_hierarchy_threshold, large_composite_threshold=large_composite_threshold, var_to_leaf_ratio_threshold=var_to_leaf_ratio_threshold, )) diagnostics.extend(collect_naming_warnings(actions)) diagnostics.extend(collect_type_warnings(variables)) diagnostics.extend(collect_data_flow_warnings(variables, machine)) diagnostics.extend(collect_redundancy_warnings(transitions, events, states)) diagnostics.extend(collect_transition_infos(states, transitions)) return _with_suggested_fixes(diagnostics)
def _with_suggested_fixes(diagnostics: List[ModelDiagnostic]) -> List[ModelDiagnostic]: return [ replace(diag, refs=refs_with_suggested_fix(diag.code, diag.refs)) for diag in diagnostics ] def _resolve_root_state_path(states, root_state_path): if root_state_path is not None: return root_state_path if not states: return None return states[0].path def _unreachable_state_diagnostics(states, reachability_graph, root_state_path) -> List[ModelDiagnostic]: if not states or root_state_path is None: return [] reachable = set(reachability_graph.get(root_state_path, ())) reachable.add(root_state_path) diagnostics: List[ModelDiagnostic] = [] for state in states: if not state.is_leaf or state.is_pseudo or state.path in reachable: continue diagnostics.append( ModelDiagnostic( code='W_UNREACHABLE_STATE', severity='warning', message=f'State {state.path!r} is unreachable from the root entry path.', span=state.span, refs={'state_path': state.path}, ) ) return diagnostics def _guard_const_false_diagnostics(transitions) -> List[ModelDiagnostic]: diagnostics: List[ModelDiagnostic] = [] for transition in transitions: if _is_minimal_const_false_guard(transition.guard): diagnostics.append( ModelDiagnostic( code='W_GUARD_CONST_FALSE', severity='warning', message=( f'Transition {transition.from_path!r} -> {transition.to_path!r} ' 'has a guard that is statically false.' ), span=transition.span, refs={ 'transition_span': transition.span, 'folded_value': False, 'from_path': transition.from_path, 'to_path': transition.to_path, 'guard_text': transition.guard, 'transition_index': transition.transition_index, }, ) ) return diagnostics def _is_minimal_const_false_guard(guard_text) -> bool: if guard_text is None: return False normalized = guard_text.strip().lower() if normalized in {'false', '0'}: return True match = re.match(r'^\s*([+-]?\d+)\s*==\s*([+-]?\d+)\s*$', guard_text) if match is not None: return int(match.group(1)) != int(match.group(2)) return False def _unused_event_diagnostics(events) -> List[ModelDiagnostic]: diagnostics: List[ModelDiagnostic] = [] for event in events: if not event.is_declared or event.is_used: continue diagnostics.append( ModelDiagnostic( code='W_UNUSED_EVENT', severity='warning', message=f'Event {event.qualified_name!r} is declared but never used.', span=event.span, refs={ 'event_qualified_name': event.qualified_name, 'scope': event.scope, }, ) ) return diagnostics