openpuppet-language 2.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.
- openpuppet_language-2.0.data/data/share/puppet/conformance/README.md +274 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/binding.json +238 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/capabilities.json +155 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/grammar.json +248 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/interaction.json +220 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/listen.json +133 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/platform.json +274 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/probes.json +117 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/render.json +180 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/tabs.json +39 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/templates.json +143 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_bad_result.py +11 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_basic.py +24 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_deps_mismatch.py +11 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_no_doc.py +10 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/runner.py +721 -0
- openpuppet_language-2.0.data/data/share/puppet/spec/01-grammar.md +283 -0
- openpuppet_language-2.0.data/data/share/puppet/spec/02-ir.md +77 -0
- openpuppet_language-2.0.data/data/share/puppet/spec/03-semantics.md +167 -0
- openpuppet_language-2.0.data/data/share/puppet/spec/04-vocabulary.md +227 -0
- openpuppet_language-2.0.data/data/share/puppet/spec/05-render-contract.md +126 -0
- openpuppet_language-2.0.data/data/share/puppet/spec/06-diagnostics.md +151 -0
- openpuppet_language-2.0.data/data/share/puppet/spec/README.md +73 -0
- openpuppet_language-2.0.dist-info/METADATA +50 -0
- openpuppet_language-2.0.dist-info/RECORD +40 -0
- openpuppet_language-2.0.dist-info/WHEEL +5 -0
- openpuppet_language-2.0.dist-info/entry_points.txt +2 -0
- openpuppet_language-2.0.dist-info/top_level.txt +1 -0
- puppet/__init__.py +19 -0
- puppet/adapter.py +112 -0
- puppet/capabilities.py +172 -0
- puppet/cli.py +132 -0
- puppet/diag.py +42 -0
- puppet/engine.py +1038 -0
- puppet/eval.py +243 -0
- puppet/ir.py +538 -0
- puppet/lang.py +755 -0
- puppet/raster_adapter.py +348 -0
- puppet/tk_adapter.py +374 -0
- puppet/vocab.py +147 -0
puppet/ir.py
ADDED
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
"""程序 IR 与校验(`spec/02-ir.md`)。
|
|
2
|
+
|
|
3
|
+
程序 IR 只由命令批次改变;运行期状态另存(见 `engine.py`)。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from . import vocab
|
|
12
|
+
from .diag import Diagnostic, ERROR, INFO, WARNING
|
|
13
|
+
from .lang import (ActionStmt, Add, CallStmt, Data, Del, DictLit, Expr, ListLit,
|
|
14
|
+
Listen, Lit, Move, On, Probe, Ref, SetStmt, Stmt, collect_refs,
|
|
15
|
+
is_static)
|
|
16
|
+
|
|
17
|
+
ROOT = "root"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Node:
|
|
22
|
+
id: str
|
|
23
|
+
type: str
|
|
24
|
+
parent: Optional[str] = None
|
|
25
|
+
children: List[str] = field(default_factory=list)
|
|
26
|
+
attrs: Dict[str, Expr] = field(default_factory=dict)
|
|
27
|
+
as_name: str = ""
|
|
28
|
+
line: int = 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class DataSource:
|
|
33
|
+
id: str
|
|
34
|
+
persist: bool = False
|
|
35
|
+
schema: Dict[str, tuple] = field(default_factory=dict)
|
|
36
|
+
initial: Optional[Expr] = None
|
|
37
|
+
line: int = 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class Handler:
|
|
42
|
+
target: str
|
|
43
|
+
event: str
|
|
44
|
+
bind: str = ""
|
|
45
|
+
when: Optional[Expr] = None
|
|
46
|
+
actions: List = field(default_factory=list)
|
|
47
|
+
line: int = 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class ListenDecl:
|
|
52
|
+
target: str
|
|
53
|
+
event: str
|
|
54
|
+
line: int = 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class Program:
|
|
59
|
+
nodes: Dict[str, Node] = field(default_factory=dict)
|
|
60
|
+
data: Dict[str, DataSource] = field(default_factory=dict)
|
|
61
|
+
handlers: List[Handler] = field(default_factory=list)
|
|
62
|
+
listens: List[ListenDecl] = field(default_factory=list)
|
|
63
|
+
calls: List[CallStmt] = field(default_factory=list)
|
|
64
|
+
probes: List[Probe] = field(default_factory=list)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def new_program() -> Program:
|
|
68
|
+
program = Program()
|
|
69
|
+
program.nodes[ROOT] = Node(id=ROOT, type="root")
|
|
70
|
+
return program
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ------------------------------------------------------------------ 结构应用
|
|
74
|
+
|
|
75
|
+
def apply_stmt(program: Program, stmt: Stmt, diags: List[Diagnostic]) -> List[str]:
|
|
76
|
+
"""把一条语句应用到程序 IR。返回被改动的数据源地址(供级联使用)。"""
|
|
77
|
+
changed: List[str] = []
|
|
78
|
+
line = getattr(stmt, "line", 0)
|
|
79
|
+
|
|
80
|
+
if isinstance(stmt, Add):
|
|
81
|
+
return _apply_add(program, stmt, diags)
|
|
82
|
+
|
|
83
|
+
if isinstance(stmt, SetStmt):
|
|
84
|
+
node = program.nodes.get(stmt.target)
|
|
85
|
+
if node is None:
|
|
86
|
+
src = program.data.get(stmt.target)
|
|
87
|
+
if src is None:
|
|
88
|
+
diags.append(Diagnostic("TARGET_MISSING", ERROR,
|
|
89
|
+
"目标 %s 不存在" % _hash(stmt.target), line=line))
|
|
90
|
+
return changed
|
|
91
|
+
return changed
|
|
92
|
+
for key, expr in stmt.attrs.items():
|
|
93
|
+
old = node.attrs.get(key)
|
|
94
|
+
if old is not None and not is_static(old):
|
|
95
|
+
diags.append(Diagnostic("BIND_OVERRIDDEN", INFO,
|
|
96
|
+
"属性 %s 的绑定已被覆盖" % key, line=line))
|
|
97
|
+
node.attrs[key] = expr
|
|
98
|
+
return changed
|
|
99
|
+
|
|
100
|
+
if isinstance(stmt, Del):
|
|
101
|
+
node = program.nodes.get(stmt.target)
|
|
102
|
+
if node is None:
|
|
103
|
+
diags.append(Diagnostic("DEL_MISSING", WARNING,
|
|
104
|
+
"要删除的 %s 不存在" % _hash(stmt.target), line=line))
|
|
105
|
+
return changed
|
|
106
|
+
_detach(program, node)
|
|
107
|
+
return changed
|
|
108
|
+
|
|
109
|
+
if isinstance(stmt, Move):
|
|
110
|
+
node = program.nodes.get(stmt.target)
|
|
111
|
+
new_parent = program.nodes.get(stmt.new_parent)
|
|
112
|
+
if node is None or new_parent is None:
|
|
113
|
+
diags.append(Diagnostic("TARGET_MISSING", ERROR, "移动的目标或新父不存在", line=line))
|
|
114
|
+
return changed
|
|
115
|
+
if stmt.anchor is not None:
|
|
116
|
+
anchor = program.nodes.get(stmt.anchor[1])
|
|
117
|
+
if anchor is None or anchor.parent != new_parent.id:
|
|
118
|
+
diags.append(Diagnostic("ANCHOR_MISSING", ERROR,
|
|
119
|
+
"锚点 %s 不存在于新父之下" % _hash(stmt.anchor[1]), line=line))
|
|
120
|
+
return changed
|
|
121
|
+
if new_parent.id == node.id or node.id in _ancestors(program, new_parent.id):
|
|
122
|
+
diags.append(Diagnostic("MOVE_CYCLE", ERROR, "移动会造成成环", line=line))
|
|
123
|
+
return changed
|
|
124
|
+
_detach(program, node)
|
|
125
|
+
node.parent = new_parent.id
|
|
126
|
+
_insert(program, new_parent, node.id, stmt.anchor)
|
|
127
|
+
return changed
|
|
128
|
+
|
|
129
|
+
if isinstance(stmt, Data):
|
|
130
|
+
if stmt.source in program.data or stmt.source in program.nodes:
|
|
131
|
+
diags.append(Diagnostic("ID_DUP", ERROR,
|
|
132
|
+
"地址 %s 已被占用" % _hash(stmt.source), line=line))
|
|
133
|
+
return changed
|
|
134
|
+
program.data[stmt.source] = DataSource(
|
|
135
|
+
id=stmt.source, persist=stmt.persist, schema=stmt.schema,
|
|
136
|
+
initial=stmt.initial, line=line)
|
|
137
|
+
# 声明不是"变化":装载期不得触发 `on … change` 级联。
|
|
138
|
+
return changed
|
|
139
|
+
|
|
140
|
+
if isinstance(stmt, On):
|
|
141
|
+
program.handlers.append(Handler(
|
|
142
|
+
target=stmt.target, event=stmt.event, bind=stmt.bind,
|
|
143
|
+
when=stmt.when, actions=stmt.actions, line=line))
|
|
144
|
+
return changed
|
|
145
|
+
|
|
146
|
+
if isinstance(stmt, Listen):
|
|
147
|
+
if stmt.target not in program.nodes and stmt.target not in program.data:
|
|
148
|
+
diags.append(Diagnostic("TARGET_MISSING", ERROR,
|
|
149
|
+
"订阅的目标 %s 不存在" % _hash(stmt.target), line=line))
|
|
150
|
+
return changed
|
|
151
|
+
program.listens.append(ListenDecl(target=stmt.target, event=stmt.event, line=line))
|
|
152
|
+
return changed
|
|
153
|
+
|
|
154
|
+
if isinstance(stmt, CallStmt):
|
|
155
|
+
program.calls.append(stmt)
|
|
156
|
+
return changed
|
|
157
|
+
|
|
158
|
+
if isinstance(stmt, Probe):
|
|
159
|
+
program.probes.append(stmt)
|
|
160
|
+
return changed
|
|
161
|
+
|
|
162
|
+
# 顶层动作(集合原语)由引擎执行:它们改的是状态,不是程序。
|
|
163
|
+
if isinstance(stmt, ActionStmt):
|
|
164
|
+
return changed
|
|
165
|
+
|
|
166
|
+
return changed
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _hash(name: str) -> str:
|
|
170
|
+
return "#" + name
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _apply_add(program: Program, stmt: Add, diags: List[Diagnostic]) -> List[str]:
|
|
174
|
+
line = stmt.line
|
|
175
|
+
parent = program.nodes.get(stmt.parent)
|
|
176
|
+
if parent is None:
|
|
177
|
+
diags.append(Diagnostic("PARENT_MISSING", ERROR,
|
|
178
|
+
"父 %s 不存在" % _hash(stmt.parent), line=line))
|
|
179
|
+
return []
|
|
180
|
+
if stmt.self_id in program.nodes or stmt.self_id in program.data:
|
|
181
|
+
diags.append(Diagnostic("ID_DUP", ERROR,
|
|
182
|
+
"地址 %s 已被占用" % _hash(stmt.self_id), line=line))
|
|
183
|
+
return []
|
|
184
|
+
if stmt.anchor is not None:
|
|
185
|
+
anchor = program.nodes.get(stmt.anchor[1])
|
|
186
|
+
if anchor is None or anchor.parent != parent.id:
|
|
187
|
+
diags.append(Diagnostic("ANCHOR_MISSING", ERROR,
|
|
188
|
+
"锚点 %s 不存在" % _hash(stmt.anchor[1]), line=line))
|
|
189
|
+
return []
|
|
190
|
+
node = program.nodes.get(stmt.self_id)
|
|
191
|
+
if node is None:
|
|
192
|
+
node = Node(id=stmt.self_id, type=stmt.type, line=line)
|
|
193
|
+
node.type = stmt.type
|
|
194
|
+
node.parent = parent.id
|
|
195
|
+
node.as_name = stmt.as_name
|
|
196
|
+
node.attrs.update(stmt.attrs)
|
|
197
|
+
program.nodes[stmt.self_id] = node
|
|
198
|
+
if stmt.self_id not in parent.children:
|
|
199
|
+
_insert(program, parent, stmt.self_id, stmt.anchor)
|
|
200
|
+
return []
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _insert(program: Program, parent: Node, node_id: str, anchor):
|
|
204
|
+
if anchor is None:
|
|
205
|
+
parent.children.append(node_id)
|
|
206
|
+
return
|
|
207
|
+
kind, anchor_id = anchor
|
|
208
|
+
if anchor_id in parent.children:
|
|
209
|
+
idx = parent.children.index(anchor_id)
|
|
210
|
+
parent.children.insert(idx if kind == "before" else idx + 1, node_id)
|
|
211
|
+
else:
|
|
212
|
+
parent.children.append(node_id)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _detach(program: Program, node: Node) -> None:
|
|
216
|
+
parent = program.nodes.get(node.parent) if node.parent else None
|
|
217
|
+
if parent and node.id in parent.children:
|
|
218
|
+
parent.children.remove(node.id)
|
|
219
|
+
for child in list(node.children):
|
|
220
|
+
_detach(program, program.nodes[child]) if child in program.nodes else None
|
|
221
|
+
program.nodes.pop(node.id, None)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _ancestors(program: Program, node_id: str) -> List[str]:
|
|
225
|
+
out, cur = [], program.nodes.get(node_id)
|
|
226
|
+
seen = set()
|
|
227
|
+
while cur is not None and cur.parent is not None:
|
|
228
|
+
if cur.parent in seen:
|
|
229
|
+
break
|
|
230
|
+
seen.add(cur.parent)
|
|
231
|
+
out.append(cur.parent)
|
|
232
|
+
cur = program.nodes.get(cur.parent)
|
|
233
|
+
return out
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def ancestors_of(program: Program, node_id: str) -> List[str]:
|
|
237
|
+
return _ancestors(program, node_id)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def in_template(program: Program, node_id: str) -> bool:
|
|
241
|
+
for aid in _ancestors(program, node_id):
|
|
242
|
+
node = program.nodes.get(aid)
|
|
243
|
+
if node is not None and node.type == "template":
|
|
244
|
+
return True
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _unknown_attr(name: str, line: int) -> Diagnostic:
|
|
249
|
+
guess = vocab.suggest_from(name, vocab.ALL_ATTRS)
|
|
250
|
+
return Diagnostic("UNKNOWN_ATTR", WARNING,
|
|
251
|
+
"未知属性 %s" % name, line=line,
|
|
252
|
+
suggest=("是不是想写 %s?" % guess) if guess else "")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# ------------------------------------------------------------------ 校验
|
|
256
|
+
|
|
257
|
+
def validate(program: Program) -> List[Diagnostic]:
|
|
258
|
+
diags: List[Diagnostic] = []
|
|
259
|
+
used_templates = set()
|
|
260
|
+
|
|
261
|
+
for node in program.nodes.values():
|
|
262
|
+
if node.id == ROOT:
|
|
263
|
+
continue
|
|
264
|
+
if node.type not in vocab.NODE_TYPES:
|
|
265
|
+
guess = vocab.suggest_from(node.type, vocab.NODE_TYPES)
|
|
266
|
+
diags.append(Diagnostic("UNKNOWN_TYPE", WARNING,
|
|
267
|
+
"未知节点类型 %s" % node.type, line=node.line,
|
|
268
|
+
suggest=("是不是想写 %s?" % guess) if guess else ""))
|
|
269
|
+
for name, expr in node.attrs.items():
|
|
270
|
+
if name not in vocab.ALL_ATTRS:
|
|
271
|
+
diags.append(_unknown_attr(name, node.line))
|
|
272
|
+
continue
|
|
273
|
+
only = vocab.TYPE_ONLY_ATTRS.get(name)
|
|
274
|
+
if only and node.type not in only:
|
|
275
|
+
diags.append(Diagnostic("ATTR_ON_TYPE", WARNING,
|
|
276
|
+
"属性 %s 不适用于 %s" % (name, node.type),
|
|
277
|
+
line=node.line))
|
|
278
|
+
if name in vocab.REF_ATTRS:
|
|
279
|
+
if not isinstance(expr, (Ref, Lit)):
|
|
280
|
+
diags.append(Diagnostic("REF_ATTR_EXPR", ERROR,
|
|
281
|
+
"引用类属性 %s 不能写表达式" % name,
|
|
282
|
+
line=node.line))
|
|
283
|
+
elif isinstance(expr, Ref) and expr.field is None:
|
|
284
|
+
used_templates.add(expr.addr)
|
|
285
|
+
if name == "icon" and isinstance(expr, Lit) and isinstance(expr.value, str):
|
|
286
|
+
if expr.value not in vocab.CORE_ICONS:
|
|
287
|
+
guess = vocab.suggest_from(expr.value, vocab.CORE_ICONS)
|
|
288
|
+
diags.append(Diagnostic("UNKNOWN_ICON", WARNING,
|
|
289
|
+
"未知图标 %s" % expr.value, line=node.line,
|
|
290
|
+
suggest=("是不是想写 %s?" % guess) if guess else ""))
|
|
291
|
+
if name == "states":
|
|
292
|
+
diags += _check_states(expr, node)
|
|
293
|
+
if name == "animate":
|
|
294
|
+
diags += _check_animate(expr, node)
|
|
295
|
+
|
|
296
|
+
for node in program.nodes.values():
|
|
297
|
+
if node.type == "template" and node.id not in used_templates:
|
|
298
|
+
diags.append(Diagnostic("TEMPLATE_UNUSED", WARNING,
|
|
299
|
+
"模板 %s 未被任何列表使用" % _hash(node.id), line=node.line))
|
|
300
|
+
|
|
301
|
+
diags += _check_handlers(program)
|
|
302
|
+
diags += _check_interaction_coverage(program)
|
|
303
|
+
diags += _check_controlled_writeback(program)
|
|
304
|
+
diags += _check_bindings(program)
|
|
305
|
+
return diags
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
MUTATING_ACTIONS = ("append", "remove", "remove_where", "update_where", "clear", "sort")
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _referenced_pairs(program: Program) -> set:
|
|
312
|
+
"""全程序里被引用过的 (地址, 属性) 对。"""
|
|
313
|
+
pairs: set = set()
|
|
314
|
+
|
|
315
|
+
def take(expr) -> None:
|
|
316
|
+
if expr is None:
|
|
317
|
+
return
|
|
318
|
+
for ref in collect_refs(expr):
|
|
319
|
+
pairs.add((ref.addr, ref.field))
|
|
320
|
+
|
|
321
|
+
for node in program.nodes.values():
|
|
322
|
+
for expr in node.attrs.values():
|
|
323
|
+
take(expr)
|
|
324
|
+
for handler in program.handlers:
|
|
325
|
+
take(handler.when)
|
|
326
|
+
for action in handler.actions:
|
|
327
|
+
take(action.item)
|
|
328
|
+
take(action.where)
|
|
329
|
+
for expr in action.attrs.values():
|
|
330
|
+
take(expr)
|
|
331
|
+
for expr in action.params.values():
|
|
332
|
+
take(expr)
|
|
333
|
+
for call in program.calls:
|
|
334
|
+
for expr in call.params.values():
|
|
335
|
+
take(expr)
|
|
336
|
+
return pairs
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _template_data_sources(program: Program, template: Node) -> set:
|
|
340
|
+
"""模板的行数据来自哪个数据源(由使用它的列表决定)。"""
|
|
341
|
+
out = set()
|
|
342
|
+
for node in program.nodes.values():
|
|
343
|
+
ref = node.attrs.get("template")
|
|
344
|
+
src = node.attrs.get("source")
|
|
345
|
+
if isinstance(ref, Ref) and ref.addr == template.id \
|
|
346
|
+
and isinstance(src, Ref) and src.addr in program.data:
|
|
347
|
+
out.add(src.addr)
|
|
348
|
+
return out
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _check_controlled_writeback(program: Program) -> List[Diagnostic]:
|
|
352
|
+
"""受控控件的值绑定了数据源,但它的 change 处理器没有回写那个数据源 → 警告。
|
|
353
|
+
|
|
354
|
+
绑定值是**派生值**:不回写,用户改动就会被重算覆盖(界面弹回)。
|
|
355
|
+
三条件同时成立才报,避免噪音:
|
|
356
|
+
① 值类属性是绑定、且能追溯到某个数据源;
|
|
357
|
+
② 该控件有 change 处理器;
|
|
358
|
+
③ 这些处理器一个都没写该数据源,且没有 call(能力可能写,无法静态判定)。
|
|
359
|
+
"""
|
|
360
|
+
diags: List[Diagnostic] = []
|
|
361
|
+
for node in program.nodes.values():
|
|
362
|
+
for field in ("value", "selected"):
|
|
363
|
+
expr = node.attrs.get(field)
|
|
364
|
+
if expr is None or is_static(expr):
|
|
365
|
+
continue
|
|
366
|
+
sources = {r.addr for r in collect_refs(expr) if r.addr in program.data}
|
|
367
|
+
template = _owning_template(program, node.id)
|
|
368
|
+
if template is not None:
|
|
369
|
+
sources |= _template_data_sources(program, template)
|
|
370
|
+
if not sources:
|
|
371
|
+
continue
|
|
372
|
+
handlers = [h for h in program.handlers
|
|
373
|
+
if h.target == node.id and h.event == "change"]
|
|
374
|
+
if not handlers:
|
|
375
|
+
continue
|
|
376
|
+
written, uncertain = set(), False
|
|
377
|
+
for handler in handlers:
|
|
378
|
+
for action in handler.actions:
|
|
379
|
+
if action.kind == "call":
|
|
380
|
+
uncertain = True
|
|
381
|
+
elif action.kind in MUTATING_ACTIONS:
|
|
382
|
+
written.add(action.target)
|
|
383
|
+
if uncertain or (sources & written):
|
|
384
|
+
continue
|
|
385
|
+
diags.append(Diagnostic(
|
|
386
|
+
"BOUND_VALUE_NOT_WRITTEN", WARNING,
|
|
387
|
+
"#%s 的 %s 绑定了 %s,但它的 change 处理器没有写回该数据源:"
|
|
388
|
+
"用户的改动会被绑定重算覆盖(界面弹回)"
|
|
389
|
+
% (node.id, field, "、".join("#" + s for s in sorted(sources))),
|
|
390
|
+
line=node.line))
|
|
391
|
+
return diags
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _owning_template(program: Program, node_id: str):
|
|
395
|
+
for aid in [node_id] + _ancestors(program, node_id):
|
|
396
|
+
node = program.nodes.get(aid)
|
|
397
|
+
if node is not None and node.type == "template":
|
|
398
|
+
return node
|
|
399
|
+
return None
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _check_interaction_coverage(program: Program) -> List[Diagnostic]:
|
|
403
|
+
"""主交互事件没有任何处理器 → 警告。
|
|
404
|
+
|
|
405
|
+
用户的动作不会有任何反应,而"没反应"必须被说出来,不能靠猜。
|
|
406
|
+
"""
|
|
407
|
+
diags: List[Diagnostic] = []
|
|
408
|
+
covered = {(h.target, h.event) for h in program.handlers}
|
|
409
|
+
watched = {(l.target, l.event) for l in program.listens}
|
|
410
|
+
read_pairs = _referenced_pairs(program)
|
|
411
|
+
for node in program.nodes.values():
|
|
412
|
+
event = vocab.PRIMARY_EVENT.get(node.type)
|
|
413
|
+
if event is None:
|
|
414
|
+
continue
|
|
415
|
+
# 被订阅也算"有人管":外部驱动者(LLM / 调度者)会响应,不属静默失败
|
|
416
|
+
if (node.id, event) in covered or (node.id, event) in watched:
|
|
417
|
+
continue
|
|
418
|
+
# 值类控件的值若在别处被读到,交互是有去处的
|
|
419
|
+
# ("提交时才读控件的值"是极常见且完全正当的写法)
|
|
420
|
+
if event == "change" and ((node.id, "value") in read_pairs
|
|
421
|
+
or (node.id, "selected") in read_pairs):
|
|
422
|
+
continue
|
|
423
|
+
diags.append(Diagnostic(
|
|
424
|
+
"UNCOVERED_INTERACTION", WARNING,
|
|
425
|
+
"%s #%s 的 %s 既没有处理器也没有被订阅:用户操作它不会有任何反应"
|
|
426
|
+
% (node.type, node.id, event), line=node.line))
|
|
427
|
+
return diags
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _check_states(expr: Expr, node: Node) -> List[Diagnostic]:
|
|
431
|
+
diags: List[Diagnostic] = []
|
|
432
|
+
if not isinstance(expr, DictLit):
|
|
433
|
+
return diags
|
|
434
|
+
for state, value in expr.fields.items():
|
|
435
|
+
if not isinstance(value, DictLit):
|
|
436
|
+
continue
|
|
437
|
+
for key in value.fields:
|
|
438
|
+
if key in vocab.STATE_APPEARANCE_LAYOUT:
|
|
439
|
+
diags.append(Diagnostic("STATE_LAYOUT_ATTR", WARNING,
|
|
440
|
+
"状态 %s 中不允许布局属性 %s" % (state, key),
|
|
441
|
+
line=node.line))
|
|
442
|
+
elif key not in vocab.STATE_APPEARANCE:
|
|
443
|
+
diags.append(Diagnostic("STATE_UNKNOWN_ATTR", WARNING,
|
|
444
|
+
"状态 %s 中的 %s 不是外观属性" % (state, key),
|
|
445
|
+
line=node.line))
|
|
446
|
+
return diags
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _check_animate(expr: Expr, node: Node) -> List[Diagnostic]:
|
|
450
|
+
names: List[str] = []
|
|
451
|
+
if isinstance(expr, Lit) and isinstance(expr.value, str):
|
|
452
|
+
names = [expr.value]
|
|
453
|
+
elif isinstance(expr, ListLit):
|
|
454
|
+
names = [i.value for i in expr.items if isinstance(i, Lit) and isinstance(i.value, str)]
|
|
455
|
+
return [Diagnostic("ANIMATE_UNKNOWN", WARNING,
|
|
456
|
+
"属性 %s 不可动画" % name, line=node.line)
|
|
457
|
+
for name in names if name not in vocab.ANIMATABLE]
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _check_handlers(program: Program) -> List[Diagnostic]:
|
|
461
|
+
diags: List[Diagnostic] = []
|
|
462
|
+
for h in program.handlers:
|
|
463
|
+
if h.event not in vocab.EVENTS:
|
|
464
|
+
diags.append(Diagnostic("UNKNOWN_EVENT", WARNING,
|
|
465
|
+
"未知事件 %s" % h.event, line=h.line))
|
|
466
|
+
node = program.nodes.get(h.target)
|
|
467
|
+
if node is not None and h.event in vocab.INTERACTION_EVENTS:
|
|
468
|
+
disabled = node.attrs.get("disabled")
|
|
469
|
+
if isinstance(disabled, Lit) and disabled.value is True:
|
|
470
|
+
diags.append(Diagnostic("DISABLED_HANDLER", WARNING,
|
|
471
|
+
"静态禁用的节点 #%s 绑定了 %s" % (h.target, h.event),
|
|
472
|
+
line=h.line))
|
|
473
|
+
if h.when is not None and isinstance(h.when, Lit) and not isinstance(h.when.value, bool):
|
|
474
|
+
diags.append(Diagnostic("WHEN_NOT_BOOL", ERROR,
|
|
475
|
+
"when 守卫必须是布尔", line=h.line))
|
|
476
|
+
for listen in program.listens:
|
|
477
|
+
if listen.event not in vocab.EVENTS:
|
|
478
|
+
diags.append(Diagnostic("UNKNOWN_EVENT", WARNING,
|
|
479
|
+
"未知事件 %s" % listen.event, line=listen.line))
|
|
480
|
+
return diags
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _check_bindings(program: Program) -> List[Diagnostic]:
|
|
484
|
+
diags: List[Diagnostic] = []
|
|
485
|
+
graph: Dict[tuple, set] = {}
|
|
486
|
+
|
|
487
|
+
def known(addr: str) -> bool:
|
|
488
|
+
return addr in program.nodes or addr in program.data
|
|
489
|
+
|
|
490
|
+
for node in program.nodes.values():
|
|
491
|
+
for name, expr in node.attrs.items():
|
|
492
|
+
if is_static(expr):
|
|
493
|
+
continue
|
|
494
|
+
deps = set()
|
|
495
|
+
for ref in collect_refs(expr):
|
|
496
|
+
if not known(ref.addr):
|
|
497
|
+
diags.append(Diagnostic("BIND_UNKNOWN_REF", ERROR,
|
|
498
|
+
"引用 %s 不存在" % _hash(ref.addr),
|
|
499
|
+
line=node.line))
|
|
500
|
+
continue
|
|
501
|
+
if ref.field is not None and ref.addr in program.nodes:
|
|
502
|
+
target = program.nodes[ref.addr]
|
|
503
|
+
if ref.field not in target.attrs and ref.field not in vocab.STATE_FLAGS:
|
|
504
|
+
diags.append(Diagnostic("BIND_UNKNOWN_REF", ERROR,
|
|
505
|
+
"节点 %s 没有属性 %s" % (_hash(ref.addr), ref.field),
|
|
506
|
+
line=node.line))
|
|
507
|
+
continue
|
|
508
|
+
deps.add((ref.addr, ref.field))
|
|
509
|
+
graph[(node.id, name)] = deps
|
|
510
|
+
|
|
511
|
+
cycles = _find_cycle(graph)
|
|
512
|
+
for key in sorted(cycles):
|
|
513
|
+
diags.append(Diagnostic("BIND_CYCLE", ERROR,
|
|
514
|
+
"绑定成环:%s.%s" % (_hash(key[0]), key[1])))
|
|
515
|
+
return diags
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _find_cycle(graph: Dict[tuple, set]) -> set:
|
|
519
|
+
"""返回参与环的键集合。"""
|
|
520
|
+
color: Dict[tuple, int] = {}
|
|
521
|
+
bad: set = set()
|
|
522
|
+
|
|
523
|
+
def visit(node, stack):
|
|
524
|
+
color[node] = 1
|
|
525
|
+
for dep in graph.get(node, ()):
|
|
526
|
+
if dep not in graph:
|
|
527
|
+
continue
|
|
528
|
+
state = color.get(dep, 0)
|
|
529
|
+
if state == 1:
|
|
530
|
+
bad.update(stack[stack.index(dep):] if dep in stack else [dep, node])
|
|
531
|
+
elif state == 0:
|
|
532
|
+
visit(dep, stack + [dep])
|
|
533
|
+
color[node] = 2
|
|
534
|
+
|
|
535
|
+
for node in list(graph):
|
|
536
|
+
if color.get(node, 0) == 0:
|
|
537
|
+
visit(node, [node])
|
|
538
|
+
return bad
|