95 lines
3.2 KiB
Python
95 lines
3.2 KiB
Python
from __future__ import annotations
|
|
|
|
import ast
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Set
|
|
|
|
|
|
@dataclass
|
|
class HilInitIssue:
|
|
file: str
|
|
key: str
|
|
message: str
|
|
|
|
|
|
def _get_str_const(node: ast.AST) -> Optional[str]:
|
|
return node.value if isinstance(node, ast.Constant) and isinstance(node.value, str) else None
|
|
|
|
|
|
class _PhysicalValuesInitCollector(ast.NodeVisitor):
|
|
"""
|
|
Colleziona le chiavi inizializzate in vari modi:
|
|
- physical_values["x"] = ...
|
|
- physical_values["x"] += ...
|
|
- physical_values.setdefault("x", ...)
|
|
- physical_values.update({"x": ..., "y": ...})
|
|
"""
|
|
def __init__(self) -> None:
|
|
self.inits: Set[str] = set()
|
|
|
|
def visit_Assign(self, node: ast.Assign) -> None:
|
|
for tgt in node.targets:
|
|
k = self._key_from_physical_values_subscript(tgt)
|
|
if k:
|
|
self.inits.add(k)
|
|
self.generic_visit(node)
|
|
|
|
def visit_AugAssign(self, node: ast.AugAssign) -> None:
|
|
k = self._key_from_physical_values_subscript(node.target)
|
|
if k:
|
|
self.inits.add(k)
|
|
self.generic_visit(node)
|
|
|
|
def visit_Call(self, node: ast.Call) -> None:
|
|
# physical_values.setdefault("x", ...)
|
|
if isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name):
|
|
if node.func.value.id == "physical_values":
|
|
if node.func.attr == "setdefault" and node.args:
|
|
k = _get_str_const(node.args[0])
|
|
if k:
|
|
self.inits.add(k)
|
|
|
|
# physical_values.update({...})
|
|
if node.func.attr == "update" and node.args:
|
|
arg0 = node.args[0]
|
|
if isinstance(arg0, ast.Dict):
|
|
for key_node in arg0.keys:
|
|
k = _get_str_const(key_node)
|
|
if k:
|
|
self.inits.add(k)
|
|
|
|
self.generic_visit(node)
|
|
|
|
@staticmethod
|
|
def _key_from_physical_values_subscript(node: ast.AST) -> Optional[str]:
|
|
# physical_values["x"] -> Subscript(Name("physical_values"), Constant("x"))
|
|
if not isinstance(node, ast.Subscript):
|
|
return None
|
|
if not (isinstance(node.value, ast.Name) and node.value.id == "physical_values"):
|
|
return None
|
|
return _get_str_const(node.slice)
|
|
|
|
|
|
def validate_hil_initialization(hil_logic_file: str, required_keys: Set[str]) -> List[HilInitIssue]:
|
|
"""
|
|
Verifica che nel file HIL ci sia almeno un'inizializzazione per ciascuna key richiesta.
|
|
Best-effort: guarda tutte le assegnazioni nel file (non solo dentro logic()).
|
|
"""
|
|
path = Path(hil_logic_file)
|
|
text = path.read_text(encoding="utf-8", errors="replace")
|
|
tree = ast.parse(text)
|
|
|
|
collector = _PhysicalValuesInitCollector()
|
|
collector.visit(tree)
|
|
|
|
missing = sorted(required_keys - collector.inits)
|
|
return [
|
|
HilInitIssue(
|
|
file=str(path),
|
|
key=k,
|
|
message=f"physical_values['{k}'] non sembra inizializzato nel file HIL (manca un assegnamento/setdefault/update).",
|
|
)
|
|
for k in missing
|
|
]
|