statable 3.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codegen/__init__.py +38 -0
- codegen/c_code_generator.py +1752 -0
- codegen/code_merger.py +371 -0
- codegen/code_templates.py +509 -0
- codegen/config.py +167 -0
- codegen/enum_generator.py +377 -0
- codegen/event_queue_generator.py +303 -0
- codegen/interrupt_generator.py +345 -0
- codegen/naming_convention.py +171 -0
- codegen/osal_generator.py +282 -0
- codegen/role_function_generator.py +1244 -0
- codegen/sample_data.py +269 -0
- codegen/state_actions_generator.py +395 -0
- codegen/struct_generator.py +721 -0
- codegen/timer_generator.py +299 -0
- codegen/transition_generator.py +907 -0
- codegen/type_mapper.py +69 -0
- codegen/validate/__init__.py +38 -0
- codegen/validate/change_actions.py +90 -0
- codegen/validate/change_applier.py +416 -0
- codegen/validate/clipboard_manager.py +47 -0
- codegen/validate/data/__init__.py +30 -0
- codegen/validate/data/action_definitions.py +200 -0
- codegen/validate/data/keywords.py +46 -0
- codegen/validate/data/prompt_templates.py +176 -0
- codegen/validate/data/validation_rules.py +205 -0
- codegen/validate/items/__init__.py +30 -0
- codegen/validate/items/base_validator.py +25 -0
- codegen/validate/items/cell_validator.py +275 -0
- codegen/validate/items/custom_type_validator.py +75 -0
- codegen/validate/items/event_validator.py +78 -0
- codegen/validate/items/flag_validator.py +74 -0
- codegen/validate/items/interrupt_validator.py +75 -0
- codegen/validate/items/queue_validator.py +73 -0
- codegen/validate/items/role_function_validator.py +175 -0
- codegen/validate/items/state_validator.py +88 -0
- codegen/validate/items/timer_validator.py +120 -0
- codegen/validate/items/transition_validator.py +113 -0
- codegen/validate/items/variable_validator.py +132 -0
- codegen/validate/logger.py +52 -0
- codegen/validate/models.py +177 -0
- codegen/validate/prompt_generator.py +724 -0
- codegen/validate/response_parser.py +154 -0
- codegen/validate/response_validator.py +275 -0
- codegen/validate/validation_dialog.py +322 -0
- codegen/validate/validator.py +94 -0
- codegen/variable_generator.py +419 -0
- statable/__init__.py +71 -0
- statable/__main__.py +49 -0
- statable/cli.py +251 -0
- statable/global_defs.py +256 -0
- statable/mermaid_gen.py +144 -0
- statable/model.py +426 -0
- statable/parser.py +64 -0
- statable/sample_data.py +435 -0
- statable/shared/__init__.py +24 -0
- statable/shared/condition_library.py +63 -0
- statable/shared/literal_library.py +66 -0
- statable/shared/role_function_library.py +150 -0
- statable/state_machine.py +121 -0
- statable/xml_io.py +1164 -0
- statable-3.1.0.dist-info/METADATA +151 -0
- statable-3.1.0.dist-info/RECORD +117 -0
- statable-3.1.0.dist-info/WHEEL +5 -0
- statable-3.1.0.dist-info/entry_points.txt +2 -0
- statable-3.1.0.dist-info/top_level.txt +3 -0
- statable_gui/__init__.py +23 -0
- statable_gui/action_edit_dialog.py +214 -0
- statable_gui/code_generation_dialog.py +486 -0
- statable_gui/code_generation_settings_dialog.py +522 -0
- statable_gui/common_widgets.py +537 -0
- statable_gui/condition_builder_dialog.py +500 -0
- statable_gui/condition_edit_dialog.py +123 -0
- statable_gui/condition_template_dialog.py +58 -0
- statable_gui/config.py +21 -0
- statable_gui/dialogs.py +439 -0
- statable_gui/event_definition_dialog.py +605 -0
- statable_gui/event_delivery_settings_dialog.py +191 -0
- statable_gui/event_queue_dialog.py +245 -0
- statable_gui/global_defs.py +167 -0
- statable_gui/global_defs_dialog.py +796 -0
- statable_gui/i18n/__init__.py +118 -0
- statable_gui/interrupt_handler_edit_dialog.py +810 -0
- statable_gui/layer_settings_dialog.py +180 -0
- statable_gui/libcntrl/__init__.py +23 -0
- statable_gui/libcntrl/condition_library.py +13 -0
- statable_gui/libcntrl/literal_library.py +13 -0
- statable_gui/libcntrl/literal_management_dialog.py +185 -0
- statable_gui/libcntrl/role_function_edit_dialog.py +145 -0
- statable_gui/libcntrl/role_function_library.py +13 -0
- statable_gui/literal_definition_dialog.py +62 -0
- statable_gui/logger.py +83 -0
- statable_gui/main_window.py +1380 -0
- statable_gui/matrix_table.py +413 -0
- statable_gui/preference_keys.py +18 -0
- statable_gui/preferences.py +74 -0
- statable_gui/role_function_dialog.py +244 -0
- statable_gui/sample_data.py +11 -0
- statable_gui/state_actions_dialog.py +398 -0
- statable_gui/symbol_picker.py +170 -0
- statable_gui/traceball.py +37 -0
- statable_gui/transition_editor_direct/__init__.py +15 -0
- statable_gui/transition_editor_direct/actions_tab.py +856 -0
- statable_gui/transition_editor_direct/code_widget.py +344 -0
- statable_gui/transition_editor_direct/coverage_analyzer.py +163 -0
- statable_gui/transition_editor_direct/dialog.py +335 -0
- statable_gui/transition_editor_direct/draft.py +270 -0
- statable_gui/transition_editor_direct/edit_dialogs.py +160 -0
- statable_gui/transition_editor_direct/overview_tab.py +173 -0
- statable_gui/transition_editor_direct/palette_widget.py +233 -0
- statable_gui/transition_editor_direct/relations_edit_dialog.py +156 -0
- statable_gui/transition_editor_direct/relations_tab.py +230 -0
- statable_gui/transition_editor_direct/system_global_dialog.py +52 -0
- statable_gui/transition_editor_direct/transition_actions_dialog.py +109 -0
- statable_gui/transition_editor_direct/transitions_tab.py +620 -0
- statable_gui/validation_dialog.py +285 -0
- statable_gui/widgets.py +1315 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# codegen/validate/items/queue_validator.py
|
|
2
|
+
"""\nQueue validation\n"""
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
import os
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
9
|
+
|
|
10
|
+
from ..logger import logger
|
|
11
|
+
from ..models import ValidationIssue, ValidationSeverity, ValidationContext
|
|
12
|
+
from ..data.validation_rules import VALIDATION_RULES
|
|
13
|
+
from .base_validator import BaseValidator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class QueueValidator(BaseValidator):
|
|
17
|
+
"""Queue validation class"""
|
|
18
|
+
|
|
19
|
+
category = "queue"
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
logger.debug("QueueValidator.__init__ started")
|
|
23
|
+
self.rules_data = VALIDATION_RULES.get(self.category, {})
|
|
24
|
+
self.rules = {
|
|
25
|
+
'QUEUE_INVALID_SIZE': self._check_invalid_size,
|
|
26
|
+
'QUEUE_UNDEFINED_EVENT': self._check_undefined_event,
|
|
27
|
+
}
|
|
28
|
+
logger.debug(f"QueueValidator.__init__ completed: {len(self.rules)} rules")
|
|
29
|
+
|
|
30
|
+
def validate(self, context: ValidationContext) -> List[ValidationIssue]:
|
|
31
|
+
logger.debug(f"QueueValidator.validate started")
|
|
32
|
+
issues = []
|
|
33
|
+
for code, rule_func in self.rules.items():
|
|
34
|
+
try:
|
|
35
|
+
result = rule_func(context)
|
|
36
|
+
if result:
|
|
37
|
+
issues.extend(result)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
logger.error(f"Rule '{code}' failed: {e}", exc_info=True)
|
|
40
|
+
return issues
|
|
41
|
+
|
|
42
|
+
def _create_issue(self, code: str, **kwargs) -> ValidationIssue:
|
|
43
|
+
rule = self.rules_data.get(code, {})
|
|
44
|
+
message = rule.get('message', '').format(**kwargs)
|
|
45
|
+
severity = ValidationSeverity.from_string(rule.get('severity', 'info'))
|
|
46
|
+
return ValidationIssue(
|
|
47
|
+
category=self.category,
|
|
48
|
+
code=code,
|
|
49
|
+
message=message,
|
|
50
|
+
severity=severity,
|
|
51
|
+
suggestion=rule.get('suggestion', ''),
|
|
52
|
+
target=kwargs.get('name', ''),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def _check_invalid_size(self, context):
|
|
56
|
+
issues = []
|
|
57
|
+
for queue in context.event_queues:
|
|
58
|
+
size = getattr(queue, 'size', 0)
|
|
59
|
+
name = getattr(queue, 'name', '')
|
|
60
|
+
if size <= 0:
|
|
61
|
+
issues.append(self._create_issue('QUEUE_INVALID_SIZE', name=name))
|
|
62
|
+
return issues
|
|
63
|
+
|
|
64
|
+
def _check_undefined_event(self, context):
|
|
65
|
+
issues = []
|
|
66
|
+
event_names = set(context.events.keys())
|
|
67
|
+
for queue in context.event_queues:
|
|
68
|
+
event_ids = getattr(queue, 'event_ids', [])
|
|
69
|
+
name = getattr(queue, 'name', '')
|
|
70
|
+
for event_id in event_ids:
|
|
71
|
+
if event_id not in event_names:
|
|
72
|
+
issues.append(self._create_issue('QUEUE_UNDEFINED_EVENT', name=name))
|
|
73
|
+
return issues
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# codegen/validate/items/role_function_validator.py
|
|
2
|
+
"""
|
|
3
|
+
Role function validation
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
from typing import List
|
|
9
|
+
|
|
10
|
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
11
|
+
|
|
12
|
+
from ..logger import logger
|
|
13
|
+
from ..models import ValidationIssue, ValidationSeverity, ValidationContext
|
|
14
|
+
from ..data.validation_rules import VALIDATION_RULES
|
|
15
|
+
from .base_validator import BaseValidator
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RoleFunctionValidator(BaseValidator):
|
|
19
|
+
"""Role function validator class"""
|
|
20
|
+
|
|
21
|
+
category = "role_function"
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
logger.debug("RoleFunctionValidator.__init__ started")
|
|
25
|
+
self.rules_data = VALIDATION_RULES.get(self.category, {})
|
|
26
|
+
self.rules = {
|
|
27
|
+
'ROLE_FUNC_NO_RETURN_TYPE': self._check_no_return_type,
|
|
28
|
+
'ROLE_FUNC_ARG_MISMATCH': self._check_arg_mismatch,
|
|
29
|
+
'ROLE_FUNC_UNUSED': self._check_unused_functions,
|
|
30
|
+
}
|
|
31
|
+
logger.debug(f"RoleFunctionValidator.__init__ completed: {len(self.rules)} rules")
|
|
32
|
+
|
|
33
|
+
def validate(self, context: ValidationContext) -> List[ValidationIssue]:
|
|
34
|
+
logger.debug(f"RoleFunctionValidator.validate started")
|
|
35
|
+
issues = []
|
|
36
|
+
for code, rule_func in self.rules.items():
|
|
37
|
+
try:
|
|
38
|
+
result = rule_func(context)
|
|
39
|
+
if result:
|
|
40
|
+
issues.extend(result)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
logger.error(f"Rule '{code}' failed: {e}", exc_info=True)
|
|
43
|
+
return issues
|
|
44
|
+
|
|
45
|
+
def _create_issue(self, code: str, **kwargs) -> ValidationIssue:
|
|
46
|
+
rule = self.rules_data.get(code, {})
|
|
47
|
+
message = rule.get('message', '').format(**kwargs)
|
|
48
|
+
severity = ValidationSeverity.from_string(rule.get('severity', 'info'))
|
|
49
|
+
return ValidationIssue(
|
|
50
|
+
category=self.category,
|
|
51
|
+
code=code,
|
|
52
|
+
message=message,
|
|
53
|
+
severity=severity,
|
|
54
|
+
suggestion=rule.get('suggestion', ''),
|
|
55
|
+
target=kwargs.get('name', ''),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def _check_no_return_type(self, context):
|
|
59
|
+
issues = []
|
|
60
|
+
for name, func in context.role_functions.items():
|
|
61
|
+
return_type = getattr(func, 'return_type', '')
|
|
62
|
+
if not return_type:
|
|
63
|
+
issues.append(self._create_issue('ROLE_FUNC_NO_RETURN_TYPE', name=name))
|
|
64
|
+
return issues
|
|
65
|
+
|
|
66
|
+
def _check_arg_mismatch(self, context):
|
|
67
|
+
issues = []
|
|
68
|
+
for name, func in context.role_functions.items():
|
|
69
|
+
arg1_type = getattr(func, 'arg1_type', '')
|
|
70
|
+
arg1_name = getattr(func, 'arg1_name', '')
|
|
71
|
+
arg2_type = getattr(func, 'arg2_type', '')
|
|
72
|
+
arg2_name = getattr(func, 'arg2_name', '')
|
|
73
|
+
if (arg1_type and not arg1_name) or (arg1_name and not arg1_type):
|
|
74
|
+
issues.append(self._create_issue('ROLE_FUNC_ARG_MISMATCH', name=name))
|
|
75
|
+
if (arg2_type and not arg2_name) or (arg2_name and not arg2_type):
|
|
76
|
+
issues.append(self._create_issue('ROLE_FUNC_ARG_MISMATCH', name=name))
|
|
77
|
+
return issues
|
|
78
|
+
|
|
79
|
+
def _collect_referenced_names(self, context):
|
|
80
|
+
"""[v2.7.2 fix] Collect every name that references a role function.
|
|
81
|
+
|
|
82
|
+
Sources:
|
|
83
|
+
- Transition.action / .pre_actions / .else_actions
|
|
84
|
+
- Transition.condition (parsed for RoleFunc_X / NS.Name)
|
|
85
|
+
- State.entry / exit / do_actions
|
|
86
|
+
- Cell actions (via state_machine.get_actions_for_cell)
|
|
87
|
+
- Event.trigger_detail.caller
|
|
88
|
+
"""
|
|
89
|
+
import re
|
|
90
|
+
used = set()
|
|
91
|
+
|
|
92
|
+
def add(ref):
|
|
93
|
+
if not ref:
|
|
94
|
+
return
|
|
95
|
+
ref = str(ref).strip()
|
|
96
|
+
if not ref:
|
|
97
|
+
return
|
|
98
|
+
used.add(ref)
|
|
99
|
+
if '.' in ref:
|
|
100
|
+
used.add(ref.split('.', 1)[1])
|
|
101
|
+
|
|
102
|
+
def add_text(text):
|
|
103
|
+
if not text:
|
|
104
|
+
return
|
|
105
|
+
text = str(text)
|
|
106
|
+
# RoleFunc_<NS>_<Name> pattern
|
|
107
|
+
for m in re.finditer(r'RoleFunc_(\w+)', text):
|
|
108
|
+
used.add(m.group(0))
|
|
109
|
+
used.add(m.group(1))
|
|
110
|
+
# NS.Name pattern
|
|
111
|
+
for m in re.finditer(r'([A-Z]\w*)\.([A-Za-z_]\w*)', text):
|
|
112
|
+
used.add(f"{m.group(1)}.{m.group(2)}")
|
|
113
|
+
used.add(m.group(2))
|
|
114
|
+
|
|
115
|
+
# 1. Transitions
|
|
116
|
+
for t in getattr(context, 'transitions', []) or []:
|
|
117
|
+
add(getattr(t, 'action', ''))
|
|
118
|
+
add_text(getattr(t, 'condition', ''))
|
|
119
|
+
for a in (getattr(t, 'pre_actions', []) or []):
|
|
120
|
+
add(a)
|
|
121
|
+
for a in (getattr(t, 'else_actions', []) or []):
|
|
122
|
+
add(a)
|
|
123
|
+
|
|
124
|
+
# 2. Via state_machine (defensive)
|
|
125
|
+
sm = (getattr(context, 'state_machine', None)
|
|
126
|
+
or getattr(context, 'sm', None))
|
|
127
|
+
if sm is not None:
|
|
128
|
+
for state in (getattr(sm, 'states', {}) or {}).values():
|
|
129
|
+
for attr in ('entry', 'exit', 'do_actions'):
|
|
130
|
+
for a in (getattr(state, attr, []) or []):
|
|
131
|
+
rf = getattr(a, 'role_function', None)
|
|
132
|
+
if rf:
|
|
133
|
+
add(rf)
|
|
134
|
+
elif isinstance(a, str):
|
|
135
|
+
add(a)
|
|
136
|
+
for ev in (getattr(sm, 'events', {}) or {}).values():
|
|
137
|
+
td = getattr(ev, 'trigger_detail', None)
|
|
138
|
+
if td is not None:
|
|
139
|
+
add(getattr(td, 'caller', ''))
|
|
140
|
+
try:
|
|
141
|
+
for key in sm.get_cell_keys():
|
|
142
|
+
for a in sm.get_actions_for_cell(*key):
|
|
143
|
+
rf = getattr(a, 'role_function', None)
|
|
144
|
+
if rf:
|
|
145
|
+
add(rf)
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
# 3. Fallback: context.states
|
|
150
|
+
for state in (getattr(context, 'states', {}) or {}).values():
|
|
151
|
+
for attr in ('entry', 'exit', 'do_actions'):
|
|
152
|
+
for a in (getattr(state, attr, []) or []):
|
|
153
|
+
rf = getattr(a, 'role_function', None)
|
|
154
|
+
if rf:
|
|
155
|
+
add(rf)
|
|
156
|
+
elif isinstance(a, str):
|
|
157
|
+
add(a)
|
|
158
|
+
|
|
159
|
+
return used
|
|
160
|
+
|
|
161
|
+
def _check_unused_functions(self, context):
|
|
162
|
+
"""[v2.7.2 fix] Match refs via bare / qualified / RoleFunc_ aliases."""
|
|
163
|
+
issues = []
|
|
164
|
+
used = self._collect_referenced_names(context)
|
|
165
|
+
for name, func in context.role_functions.items():
|
|
166
|
+
aliases = {name}
|
|
167
|
+
ns = getattr(func, 'namespace', '') or ''
|
|
168
|
+
if ns:
|
|
169
|
+
aliases.add(f"{ns}.{name}")
|
|
170
|
+
aliases.add(f"RoleFunc_{ns}_{name}")
|
|
171
|
+
aliases.add(f"RoleFunc_{name}")
|
|
172
|
+
if aliases & used:
|
|
173
|
+
continue
|
|
174
|
+
issues.append(self._create_issue('ROLE_FUNC_UNUSED', name=name))
|
|
175
|
+
return issues
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# codegen/validate/items/state_validator.py
|
|
2
|
+
"""\nState validation\n"""
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
import os
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
9
|
+
|
|
10
|
+
from ..logger import logger
|
|
11
|
+
from ..models import ValidationIssue, ValidationSeverity, ValidationContext
|
|
12
|
+
from ..data.validation_rules import VALIDATION_RULES
|
|
13
|
+
from .base_validator import BaseValidator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StateValidator(BaseValidator):
|
|
17
|
+
"""State validation class"""
|
|
18
|
+
|
|
19
|
+
category = "state"
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
logger.debug("StateValidator.__init__ started")
|
|
23
|
+
self.rules_data = VALIDATION_RULES.get(self.category, {})
|
|
24
|
+
self.rules = {
|
|
25
|
+
'STATE_NO_INITIAL': self._check_initial_state,
|
|
26
|
+
'STATE_UNREACHABLE': self._check_unreachable_states,
|
|
27
|
+
'STATE_NO_TRANSITION': self._check_no_transition_states,
|
|
28
|
+
'STATE_DUPLICATE': self._check_duplicate_states,
|
|
29
|
+
}
|
|
30
|
+
logger.debug(f"StateValidator.__init__ completed: {len(self.rules)} rules")
|
|
31
|
+
|
|
32
|
+
def validate(self, context: ValidationContext) -> List[ValidationIssue]:
|
|
33
|
+
logger.debug(f"StateValidator.validate started")
|
|
34
|
+
issues = []
|
|
35
|
+
for code, rule_func in self.rules.items():
|
|
36
|
+
try:
|
|
37
|
+
result = rule_func(context)
|
|
38
|
+
if result:
|
|
39
|
+
issues.extend(result)
|
|
40
|
+
except Exception as e:
|
|
41
|
+
logger.error(f"Rule '{code}' failed: {e}", exc_info=True)
|
|
42
|
+
return issues
|
|
43
|
+
|
|
44
|
+
def _create_issue(self, code: str, **kwargs) -> ValidationIssue:
|
|
45
|
+
rule = self.rules_data.get(code, {})
|
|
46
|
+
message = rule.get('message', '').format(**kwargs)
|
|
47
|
+
severity = ValidationSeverity.from_string(rule.get('severity', 'info'))
|
|
48
|
+
|
|
49
|
+
return ValidationIssue(
|
|
50
|
+
category=self.category,
|
|
51
|
+
code=code,
|
|
52
|
+
message=message,
|
|
53
|
+
severity=severity,
|
|
54
|
+
suggestion=rule.get('suggestion', ''),
|
|
55
|
+
target=kwargs.get('name', ''),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def _check_initial_state(self, context):
|
|
59
|
+
if context.initial_state is None:
|
|
60
|
+
return [self._create_issue('STATE_NO_INITIAL')]
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
def _check_unreachable_states(self, context):
|
|
64
|
+
issues = []
|
|
65
|
+
reachable = set(t.target for t in context.transitions if t.target)
|
|
66
|
+
for name in context.states.keys():
|
|
67
|
+
if name not in reachable and name != context.initial_state:
|
|
68
|
+
issues.append(self._create_issue('STATE_UNREACHABLE', name=name))
|
|
69
|
+
return issues
|
|
70
|
+
|
|
71
|
+
def _check_no_transition_states(self, context):
|
|
72
|
+
issues = []
|
|
73
|
+
sources = set(t.source for t in context.transitions)
|
|
74
|
+
for name in context.states.keys():
|
|
75
|
+
if name not in sources:
|
|
76
|
+
issues.append(self._create_issue('STATE_NO_TRANSITION', name=name))
|
|
77
|
+
return issues
|
|
78
|
+
|
|
79
|
+
def _check_duplicate_states(self, context):
|
|
80
|
+
issues = []
|
|
81
|
+
lower_names = {}
|
|
82
|
+
for name in context.states.keys():
|
|
83
|
+
lower = name.lower()
|
|
84
|
+
if lower in lower_names:
|
|
85
|
+
issues.append(self._create_issue('STATE_DUPLICATE', name=name, other=lower_names[lower]))
|
|
86
|
+
else:
|
|
87
|
+
lower_names[lower] = name
|
|
88
|
+
return issues
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# codegen/validate/items/timer_validator.py
|
|
2
|
+
"""
|
|
3
|
+
Timer validation
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
from typing import List
|
|
9
|
+
|
|
10
|
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
11
|
+
|
|
12
|
+
from ..logger import logger
|
|
13
|
+
from ..models import ValidationIssue, ValidationSeverity, ValidationContext
|
|
14
|
+
from ..data.validation_rules import VALIDATION_RULES
|
|
15
|
+
from .base_validator import BaseValidator
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TimerValidator(BaseValidator):
|
|
19
|
+
"""Timer validator class"""
|
|
20
|
+
|
|
21
|
+
category = "timer"
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
logger.debug("TimerValidator.__init__ started")
|
|
25
|
+
self.rules_data = VALIDATION_RULES.get(self.category, {})
|
|
26
|
+
self.rules = {
|
|
27
|
+
'TIMER_DUPLICATE_VARIABLE': self._check_duplicate_variables,
|
|
28
|
+
'TIMER_INVALID_MULTIPLIER': self._check_invalid_multiplier,
|
|
29
|
+
}
|
|
30
|
+
logger.debug(f"TimerValidator.__init__ completed: {len(self.rules)} rules")
|
|
31
|
+
|
|
32
|
+
def validate(self, context: ValidationContext) -> List[ValidationIssue]:
|
|
33
|
+
logger.debug(f"TimerValidator.validate started")
|
|
34
|
+
issues = []
|
|
35
|
+
for code, rule_func in self.rules.items():
|
|
36
|
+
try:
|
|
37
|
+
result = rule_func(context)
|
|
38
|
+
if result:
|
|
39
|
+
issues.extend(result)
|
|
40
|
+
except Exception as e:
|
|
41
|
+
logger.error(f"Rule '{code}' failed: {e}", exc_info=True)
|
|
42
|
+
return issues
|
|
43
|
+
|
|
44
|
+
def _create_issue(self, code: str, **kwargs) -> ValidationIssue:
|
|
45
|
+
rule = self.rules_data.get(code, {})
|
|
46
|
+
message = rule.get('message', '').format(**kwargs)
|
|
47
|
+
severity = ValidationSeverity.from_string(rule.get('severity', 'info'))
|
|
48
|
+
return ValidationIssue(
|
|
49
|
+
category=self.category,
|
|
50
|
+
code=code,
|
|
51
|
+
message=message,
|
|
52
|
+
severity=severity,
|
|
53
|
+
suggestion=rule.get('suggestion', ''),
|
|
54
|
+
target=kwargs.get('name', ''),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def _get_all_timer_variables(self, context):
|
|
58
|
+
timer_vars = []
|
|
59
|
+
timer_base = context.timer_base
|
|
60
|
+
if timer_base:
|
|
61
|
+
timer_vars.append(getattr(timer_base, 'variable_name', ''))
|
|
62
|
+
for derived in getattr(timer_base, 'derived', []):
|
|
63
|
+
timer_vars.append(getattr(derived, 'variable_name', ''))
|
|
64
|
+
for timer in context.extra_timers:
|
|
65
|
+
timer_vars.append(getattr(timer, 'variable_name', ''))
|
|
66
|
+
for derived in getattr(timer, 'derived', []):
|
|
67
|
+
timer_vars.append(getattr(derived, 'variable_name', ''))
|
|
68
|
+
return timer_vars
|
|
69
|
+
|
|
70
|
+
def _check_duplicate_variables(self, context):
|
|
71
|
+
"""[v2.7.2 fix] Skip when the conflicting SystemVariable is group='Timer'.
|
|
72
|
+
|
|
73
|
+
The TUTORIAL declares 'g_system_tick' in both:
|
|
74
|
+
- <SystemVariables> with group="Timer"
|
|
75
|
+
- <TimerBase variable_name="g_system_tick">
|
|
76
|
+
This is the intended pattern (timer base names its counter
|
|
77
|
+
variable, which is also a shared SystemVariable). Only flag:
|
|
78
|
+
- two timer bases using the same variable_name
|
|
79
|
+
- timer name conflicting with a non-Timer SystemVariable
|
|
80
|
+
"""
|
|
81
|
+
issues = []
|
|
82
|
+
var_groups = {}
|
|
83
|
+
for v in context.variables:
|
|
84
|
+
name = getattr(v, 'name', '')
|
|
85
|
+
group = getattr(v, 'group', '')
|
|
86
|
+
var_groups[name] = group
|
|
87
|
+
timer_vars = self._get_all_timer_variables(context)
|
|
88
|
+
seen = set()
|
|
89
|
+
for var_name in timer_vars:
|
|
90
|
+
if not var_name:
|
|
91
|
+
continue
|
|
92
|
+
if var_name in seen:
|
|
93
|
+
issues.append(self._create_issue(
|
|
94
|
+
'TIMER_DUPLICATE_VARIABLE', name=var_name))
|
|
95
|
+
continue
|
|
96
|
+
if var_name in var_groups:
|
|
97
|
+
if var_groups[var_name] == 'Timer':
|
|
98
|
+
pass # expected: shared SystemVariable + TimerBase
|
|
99
|
+
else:
|
|
100
|
+
issues.append(self._create_issue(
|
|
101
|
+
'TIMER_DUPLICATE_VARIABLE', name=var_name))
|
|
102
|
+
seen.add(var_name)
|
|
103
|
+
return issues
|
|
104
|
+
|
|
105
|
+
def _check_invalid_multiplier(self, context):
|
|
106
|
+
issues = []
|
|
107
|
+
timer_base = context.timer_base
|
|
108
|
+
if timer_base:
|
|
109
|
+
for derived in getattr(timer_base, 'derived', []):
|
|
110
|
+
multiplier = getattr(derived, 'multiplier', 0)
|
|
111
|
+
name = getattr(derived, 'variable_name', '')
|
|
112
|
+
if multiplier <= 0:
|
|
113
|
+
issues.append(self._create_issue('TIMER_INVALID_MULTIPLIER', name=name))
|
|
114
|
+
for timer in context.extra_timers:
|
|
115
|
+
for derived in getattr(timer, 'derived', []):
|
|
116
|
+
multiplier = getattr(derived, 'multiplier', 0)
|
|
117
|
+
name = getattr(derived, 'variable_name', '')
|
|
118
|
+
if multiplier <= 0:
|
|
119
|
+
issues.append(self._create_issue('TIMER_INVALID_MULTIPLIER', name=name))
|
|
120
|
+
return issues
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# codegen/validate/items/transition_validator.py
|
|
2
|
+
"""
|
|
3
|
+
Transition validation
|
|
4
|
+
|
|
5
|
+
[v1.8 section 11.2 #6]
|
|
6
|
+
- Unified logger output with StateValidator
|
|
7
|
+
- Added start / completion logs to __init__ / validate
|
|
8
|
+
- Wrapped rule execution in try/except; on failure, log via logger.error
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
import os
|
|
13
|
+
from typing import List
|
|
14
|
+
|
|
15
|
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
16
|
+
|
|
17
|
+
from ..logger import logger
|
|
18
|
+
from ..models import ValidationIssue, ValidationSeverity, ValidationContext
|
|
19
|
+
from ..data.validation_rules import VALIDATION_RULES
|
|
20
|
+
from .base_validator import BaseValidator
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TransitionValidator(BaseValidator):
|
|
24
|
+
"""Transition validation class"""
|
|
25
|
+
|
|
26
|
+
category = "transition"
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
logger.debug("TransitionValidator.__init__ started")
|
|
30
|
+
self.rules_data = VALIDATION_RULES.get(self.category, {})
|
|
31
|
+
self.rules = {
|
|
32
|
+
'TRANSITION_TARGET_UNDEFINED': self._check_target_undefined,
|
|
33
|
+
'TRANSITION_EVENT_UNDEFINED': self._check_event_undefined,
|
|
34
|
+
'TRANSITION_SOURCE_UNDEFINED': self._check_source_undefined,
|
|
35
|
+
'TRANSITION_DUPLICATE': self._check_duplicate_transitions,
|
|
36
|
+
'TRANSITION_SELF_LOOP': self._check_self_loops,
|
|
37
|
+
}
|
|
38
|
+
logger.debug(f"TransitionValidator.__init__ completed: {len(self.rules)} rules")
|
|
39
|
+
|
|
40
|
+
def validate(self, context: ValidationContext) -> List[ValidationIssue]:
|
|
41
|
+
logger.debug("TransitionValidator.validate started")
|
|
42
|
+
issues = []
|
|
43
|
+
for code, rule_func in self.rules.items():
|
|
44
|
+
try:
|
|
45
|
+
result = rule_func(context)
|
|
46
|
+
if result:
|
|
47
|
+
issues.extend(result)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
logger.error(f"Rule '{code}' failed: {e}", exc_info=True)
|
|
50
|
+
logger.debug(f"TransitionValidator.validate completed: {len(issues)} issues")
|
|
51
|
+
return issues
|
|
52
|
+
|
|
53
|
+
def _create_issue(self, code: str, **kwargs) -> ValidationIssue:
|
|
54
|
+
rule = self.rules_data.get(code, {})
|
|
55
|
+
message = rule.get('message', '').format(**kwargs)
|
|
56
|
+
severity = ValidationSeverity.from_string(rule.get('severity', 'info'))
|
|
57
|
+
|
|
58
|
+
target = kwargs.get('target', '') or kwargs.get('source', '') or kwargs.get('event', '')
|
|
59
|
+
|
|
60
|
+
return ValidationIssue(
|
|
61
|
+
category=self.category,
|
|
62
|
+
code=code,
|
|
63
|
+
message=message,
|
|
64
|
+
severity=severity,
|
|
65
|
+
suggestion=rule.get('suggestion', ''),
|
|
66
|
+
target=target,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def _check_target_undefined(self, context):
|
|
70
|
+
issues = []
|
|
71
|
+
state_names = set(context.states.keys())
|
|
72
|
+
for t in context.transitions:
|
|
73
|
+
if t.target and t.target not in state_names:
|
|
74
|
+
issues.append(self._create_issue('TRANSITION_TARGET_UNDEFINED', target=t.target))
|
|
75
|
+
return issues
|
|
76
|
+
|
|
77
|
+
def _check_event_undefined(self, context):
|
|
78
|
+
issues = []
|
|
79
|
+
event_names = set(context.events.keys())
|
|
80
|
+
for t in context.transitions:
|
|
81
|
+
if t.event and t.event not in event_names:
|
|
82
|
+
issues.append(self._create_issue('TRANSITION_EVENT_UNDEFINED', event=t.event))
|
|
83
|
+
return issues
|
|
84
|
+
|
|
85
|
+
def _check_source_undefined(self, context):
|
|
86
|
+
issues = []
|
|
87
|
+
state_names = set(context.states.keys())
|
|
88
|
+
for t in context.transitions:
|
|
89
|
+
if t.source not in state_names:
|
|
90
|
+
issues.append(self._create_issue('TRANSITION_SOURCE_UNDEFINED', source=t.source))
|
|
91
|
+
return issues
|
|
92
|
+
|
|
93
|
+
def _check_duplicate_transitions(self, context):
|
|
94
|
+
issues = []
|
|
95
|
+
seen = set()
|
|
96
|
+
for t in context.transitions:
|
|
97
|
+
key = (t.source, t.event, t.target)
|
|
98
|
+
if key in seen:
|
|
99
|
+
issues.append(self._create_issue(
|
|
100
|
+
'TRANSITION_DUPLICATE', source=t.source, event=t.event, target=t.target
|
|
101
|
+
))
|
|
102
|
+
else:
|
|
103
|
+
seen.add(key)
|
|
104
|
+
return issues
|
|
105
|
+
|
|
106
|
+
def _check_self_loops(self, context):
|
|
107
|
+
issues = []
|
|
108
|
+
for t in context.transitions:
|
|
109
|
+
if t.source == t.target:
|
|
110
|
+
issues.append(self._create_issue(
|
|
111
|
+
'TRANSITION_SELF_LOOP', source=t.source, event=t.event
|
|
112
|
+
))
|
|
113
|
+
return issues
|