@dforge-core/dforge-mcp 0.2.45 → 0.2.46-next

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.
Files changed (66) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -1
  3. package/package.json +48 -46
  4. package/scripts/install-skills.mjs +1 -0
  5. package/skills/dforge-1c-import/SKILL.md +374 -0
  6. package/skills/dforge-1c-import/references/environment.md +97 -0
  7. package/skills/dforge-1c-import/references/loading.md +161 -0
  8. package/skills/dforge-1c-import/references/measurements.md +210 -0
  9. package/skills/dforge-1c-import/references/print-forms.md +148 -0
  10. package/skills/dforge-1c-import/scripts/1c.py +178 -0
  11. package/skills/dforge-1c-import/scripts/README.md +110 -0
  12. package/skills/dforge-1c-import/scripts/lib/analyze.py +83 -0
  13. package/skills/dforge-1c-import/scripts/lib/attrs.py +278 -0
  14. package/skills/dforge-1c-import/scripts/lib/autospec.py +964 -0
  15. package/skills/dforge-1c-import/scripts/lib/binding.py +257 -0
  16. package/skills/dforge-1c-import/scripts/lib/bsl.py +811 -0
  17. package/skills/dforge-1c-import/scripts/lib/callgraph.py +717 -0
  18. package/skills/dforge-1c-import/scripts/lib/cli.py +53 -0
  19. package/skills/dforge-1c-import/scripts/lib/columnmap.py +346 -0
  20. package/skills/dforge-1c-import/scripts/lib/coverage.py +242 -0
  21. package/skills/dforge-1c-import/scripts/lib/custom.py +83 -0
  22. package/skills/dforge-1c-import/scripts/lib/delta.py +741 -0
  23. package/skills/dforge-1c-import/scripts/lib/deps.py +136 -0
  24. package/skills/dforge-1c-import/scripts/lib/docusage.py +105 -0
  25. package/skills/dforge-1c-import/scripts/lib/dump1c.py +251 -0
  26. package/skills/dforge-1c-import/scripts/lib/explore.py +55 -0
  27. package/skills/dforge-1c-import/scripts/lib/external.py +201 -0
  28. package/skills/dforge-1c-import/scripts/lib/extract_code.py +251 -0
  29. package/skills/dforge-1c-import/scripts/lib/forms.py +239 -0
  30. package/skills/dforge-1c-import/scripts/lib/genmodule.py +1432 -0
  31. package/skills/dforge-1c-import/scripts/lib/handlers.py +202 -0
  32. package/skills/dforge-1c-import/scripts/lib/handmap.py +142 -0
  33. package/skills/dforge-1c-import/scripts/lib/i18n_coverage.py +87 -0
  34. package/skills/dforge-1c-import/scripts/lib/idioms.py +273 -0
  35. package/skills/dforge-1c-import/scripts/lib/integrity.py +264 -0
  36. package/skills/dforge-1c-import/scripts/lib/inventory.py +535 -0
  37. package/skills/dforge-1c-import/scripts/lib/layers.py +106 -0
  38. package/skills/dforge-1c-import/scripts/lib/meta.py +315 -0
  39. package/skills/dforge-1c-import/scripts/lib/migrate.py +1299 -0
  40. package/skills/dforge-1c-import/scripts/lib/mxl.py +368 -0
  41. package/skills/dforge-1c-import/scripts/lib/naming.py +251 -0
  42. package/skills/dforge-1c-import/scripts/lib/paths.py +90 -0
  43. package/skills/dforge-1c-import/scripts/lib/peek.py +70 -0
  44. package/skills/dforge-1c-import/scripts/lib/posted.py +218 -0
  45. package/skills/dforge-1c-import/scripts/lib/posting.py +655 -0
  46. package/skills/dforge-1c-import/scripts/lib/printgen.py +239 -0
  47. package/skills/dforge-1c-import/scripts/lib/rebuild.py +194 -0
  48. package/skills/dforge-1c-import/scripts/lib/recorded.py +170 -0
  49. package/skills/dforge-1c-import/scripts/lib/recorders.py +94 -0
  50. package/skills/dforge-1c-import/scripts/lib/reftargets.py +95 -0
  51. package/skills/dforge-1c-import/scripts/lib/reportgen.py +157 -0
  52. package/skills/dforge-1c-import/scripts/lib/roles.py +33 -0
  53. package/skills/dforge-1c-import/scripts/lib/seedstate.py +488 -0
  54. package/skills/dforge-1c-import/scripts/lib/sizing.py +184 -0
  55. package/skills/dforge-1c-import/scripts/lib/spec.py +400 -0
  56. package/skills/dforge-1c-import/scripts/lib/statements.py +774 -0
  57. package/skills/dforge-1c-import/scripts/lib/subsystems.py +480 -0
  58. package/skills/dforge-1c-import/scripts/lib/terms.py +451 -0
  59. package/skills/dforge-1c-import/scripts/lib/terms_uk.py +555 -0
  60. package/skills/dforge-1c-import/scripts/lib/totals.py +387 -0
  61. package/skills/dforge-1c-import/scripts/lib/typemap.py +73 -0
  62. package/skills/dforge-1c-import/scripts/lib/unpack.py +136 -0
  63. package/skills/dforge-1c-import/scripts/lib/validate.py +508 -0
  64. package/skills/dforge-1c-import/scripts/project-template/contours.py +47 -0
  65. package/skills/dforge-1c-import/scripts/project-template/sources.py +14 -0
  66. package/skills/dforge-mcp-author/SKILL.md +5 -0
@@ -0,0 +1,717 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ u"""Which registers each document writes, followed through every module.
4
+
5
+ `handlers.py` finds `Движения.<name>` by text and follows calls one level, inside the
6
+ document's own module. In the vendor configuration that misses most of it: the set is taken
7
+ into a variable (`ПроводкиБУ = Движения.Хозрасчетный`), the variable is handed to a common
8
+ module (`БухгалтерскийУчет.ПереоценкаСчетаРегл(..., ПроводкиБУ, ...)`), and the row is added
9
+ three calls away. Or the document object itself goes (`УправлениеЗапасами.X(ЭтотОбъект)`)
10
+ and the callee reaches `ДокументОбъект.Движения.<name>`.
11
+
12
+ So this parses every module (`bsl.py`) and follows VALUES, not text. A value of interest is
13
+ a path from a root - the document itself, or a method's parameter - such as
14
+ `this.Движения.Хозрасчетный`. Each method gets a summary: the paths it writes through,
15
+ in terms of its own parameters. A call site maps the callee's summary through its
16
+ arguments, and the whole thing iterates until no summary grows. Recursion converges
17
+ because summaries only grow and are finite.
18
+
19
+ What counts as a write: one of `WRITE_OPS` called on a register set. Reading a set (a
20
+ query, `Выгрузить`) is not one, and neither is clearing it - the platform clears movements
21
+ on its own.
22
+
23
+ Reported, not dropped:
24
+ * calls that cannot be resolved (a method on a value of unknown type, `Выполнить`)
25
+ are counted per kind - that count is the call graph's own blind spot;
26
+ * a register name that the metadata does not have is listed rather than counted.
27
+
28
+ python tools/1c.py callgraph # summary; writes out/callgraph.json
29
+ python tools/1c.py callgraph <Document> # one document, with the call chains
30
+ """
31
+ from __future__ import print_function
32
+
33
+ import codecs
34
+ import collections
35
+ import io
36
+ import json
37
+ import os
38
+ import sys
39
+
40
+ import bsl
41
+ import handlers
42
+ import paths
43
+
44
+ CODE = os.path.join(paths.OUT, "code")
45
+ TARGET = os.path.join(paths.OUT, "callgraph.json")
46
+
47
+ # 1C's own metadata class GUIDs - the platform's, not this infobase's.
48
+ COMMON_MODULE_CLASS = "class_0fe48980"
49
+ SUBSCRIPTION_CLASS = "4e828da6-0f44-4b5b-b1c0-a2b3cfe7bdcc"
50
+
51
+ # The document events a handler can write movements from, and what each one means.
52
+ EVENTS = {
53
+ u"обработкапроведения": "posting",
54
+ u"обработкаудаленияпроведения": "unposting",
55
+ u"призаписи": "on-write",
56
+ u"передзаписью": "before-write",
57
+ }
58
+
59
+ # Methods of a register record set that put rows into it.
60
+ WRITE_OPS = frozenset(bsl.norm(x) for x in (
61
+ u"Добавить", u"Загрузить", u"Записать", u"ВыполнитьПриход", u"ВыполнитьРасход",
62
+ u"ВыполнитьДвижения", u"ДобавитьПриход", u"ДобавитьРасход", u"ДобавитьДвижение",
63
+ u"ВыполнитьДвижениеПриход", u"ВыполнитьДвижениеРасход",
64
+ u"Add", u"Load", u"Write"))
65
+
66
+ MOVEMENTS = (u"движения", u"registerrecords")
67
+ THIS = (u"этотобъект", u"thisobject")
68
+ MANAGERS = {u"документы": "Document", u"documents": "Document",
69
+ u"справочники": "Reference", u"catalogs": "Reference",
70
+ u"обработки": "DataProcessor", u"отчеты": "Report",
71
+ u"регистрынакопления": "AccumRg", u"регистрысведений": "InfoRg",
72
+ u"регистрыбухгалтерии": "AccRg"}
73
+
74
+ REGISTER_MANAGERS = frozenset(bsl.norm(x) for x in (
75
+ u"РегистрыНакопления", u"РегистрыСведений", u"РегистрыБухгалтерии", u"РегистрыРасчета",
76
+ u"AccumulationRegisters", u"InformationRegisters", u"AccountingRegisters"))
77
+ INSERT = frozenset((bsl.norm(u"Вставить"), u"insert"))
78
+ CREATE_SET = frozenset(bsl.norm(x) for x in (u"СоздатьНаборЗаписей", u"CreateRecordSet"))
79
+
80
+ MAX_PATH = 3
81
+
82
+
83
+ # -- loading ----------------------------------------------------------------------------
84
+
85
+ class Module(object):
86
+ """One parsed module: which object it belongs to and what kind it is."""
87
+
88
+ def __init__(self, family, obj, kind, path, tree, guid=None):
89
+ self.family, self.object, self.kind, self.path = family, obj, kind, path
90
+ self.guid = guid
91
+ self.methods = {}
92
+ self.variables = set()
93
+ _, variables, methods, _body = tree
94
+ for v in variables:
95
+ for name in v[2]:
96
+ self.variables.add(bsl.norm(name))
97
+ for m in methods:
98
+ self.methods[bsl.norm(m[1])] = m
99
+
100
+ @property
101
+ def key(self):
102
+ return u"%s.%s.%s" % (self.family, self.object, self.kind)
103
+
104
+
105
+ def _kind(entry, owner_guid):
106
+ """object / manager / recordset / form, from the container's place in its object."""
107
+ own = entry["guid"] == owner_guid
108
+ suffix = entry["container"].rsplit(".", 1)[-1]
109
+ if entry["family"] == COMMON_MODULE_CLASS:
110
+ return "common"
111
+ if not own:
112
+ return "form"
113
+ return {"0": "object", "1": "recordset", "2": "manager"}.get(suffix, "other" + suffix)
114
+
115
+
116
+ def load(verbose=True):
117
+ with io.open(os.path.join(CODE, "index.json"), encoding="utf-8") as f:
118
+ index = json.load(f)
119
+ with io.open(os.path.join(paths.OUT, "objects.json"), encoding="utf-8") as f:
120
+ owners = dict(((o["family"], o["name"]), o["guid"]) for o in json.load(f))
121
+ modules, failures = [], []
122
+ for entry in index:
123
+ path = os.path.join(paths.OUT, entry["path"])
124
+ with io.open(path, encoding="utf-8-sig") as f:
125
+ text = f.read()
126
+ if text.lstrip().startswith("{"): # a form layout saved beside the code
127
+ continue
128
+ owner = owners.get((entry["family"], entry["object"]))
129
+ kind = _kind(entry, owner)
130
+ tree, errors = bsl.parse(text)
131
+ for error in errors:
132
+ failures.append((entry["path"], str(error)))
133
+ modules.append(Module(entry["family"], entry["object"], kind, entry["path"], tree,
134
+ entry["guid"]))
135
+ if verbose:
136
+ sys.stderr.write("[i] %d modules parsed, %d methods failed to parse\n"
137
+ % (len(modules), len(failures)))
138
+ return modules, failures
139
+
140
+
141
+ def register_names():
142
+ """Every register the metadata declares, by case-folded name -> (family, name)."""
143
+ with io.open(os.path.join(paths.OUT, "objects.json"), encoding="utf-8") as f:
144
+ objects = json.load(f)
145
+ return dict((bsl.norm(o["name"]), (o["family"], o["name"])) for o in objects
146
+ if o.get("family") in ("AccumRg", "InfoRg", "AccRg", "CRg"))
147
+
148
+
149
+ # -- the analysis -----------------------------------------------------------------------
150
+
151
+ class Graph(object):
152
+
153
+ def __init__(self, modules, registers):
154
+ self.registers = registers
155
+ self.modules = modules
156
+ self.common = {}
157
+ self.by_object = {}
158
+ self.by_guid = dict((m.guid, m) for m in modules if m.kind == "common")
159
+ self.recordsets = dict((bsl.norm(m.object), m) for m in modules if m.kind == "recordset")
160
+ for m in modules:
161
+ if m.kind == "common":
162
+ self.common[bsl.norm(m.object)] = m
163
+ self.by_object[(m.family, bsl.norm(m.object), m.kind)] = m
164
+ # a common-module method called without its module name: the module is global
165
+ exported = collections.defaultdict(list)
166
+ for m in self.common.values():
167
+ for name, method in m.methods.items():
168
+ if method[4]:
169
+ exported[name].append(m)
170
+ self.global_methods = dict((n, ms[0]) for n, ms in exported.items() if len(ms) == 1)
171
+ # summaries: (module key, method) -> {written path: witness}
172
+ self.summary = collections.defaultdict(dict)
173
+ # beside each summary path, every write site behind it: (module path, line, op)
174
+ self.sites = collections.defaultdict(dict)
175
+ self.module_env = collections.defaultdict(lambda: collections.defaultdict(set))
176
+ self.module_changes = 0
177
+ # a created set kept in a structure field, by field name: УТП builds a structure of
178
+ # record sets in one function and writes them in another
179
+ self.fields = collections.defaultdict(set)
180
+ self.undeclared = {}
181
+ # (document, event, register) -> every write site behind that movement
182
+ self.document_sites = {}
183
+ self.unresolved = collections.Counter()
184
+ self.foreign_ops = collections.Counter()
185
+
186
+ # A path is (root, tuple of case-folded member names). root is "this" or ("param", i).
187
+
188
+ def register_of(self, path):
189
+ """The register a path names, if it names one: (..., 'движения', X)."""
190
+ names = path[1]
191
+ if len(names) >= 2 and names[-2] in MOVEMENTS:
192
+ return names[-1]
193
+ return None
194
+
195
+ def resolve(self, module, callee, env):
196
+ """(target module, method) for a call's callee expression, or a reason it has none."""
197
+ if callee[0] == "name":
198
+ name = bsl.norm(callee[1])
199
+ if name in module.methods:
200
+ return module, module.methods[name]
201
+ if name in self.global_methods:
202
+ target = self.global_methods[name]
203
+ return target, target.methods[name]
204
+ return "unknown-global"
205
+ if callee[0] != "member":
206
+ return "computed"
207
+ name = bsl.norm(callee[2])
208
+ parts = bsl.dotted(callee[1])
209
+ if parts is None:
210
+ return "method-of-expression"
211
+ head = bsl.norm(parts[0])
212
+ if len(parts) == 1:
213
+ if head in THIS:
214
+ return (module, module.methods[name]) if name in module.methods else "this-unknown"
215
+ if head in env:
216
+ return "method-of-variable"
217
+ target = self.common.get(head)
218
+ if target is not None:
219
+ if name in target.methods:
220
+ return target, target.methods[name]
221
+ return "common-missing"
222
+ return "method-of-variable"
223
+ if len(parts) == 2 and head in MANAGERS:
224
+ target = self.by_object.get((MANAGERS[head], bsl.norm(parts[1]), "manager"))
225
+ if target is not None and name in target.methods:
226
+ return target, target.methods[name]
227
+ return "manager-missing"
228
+ return "method-of-expression"
229
+
230
+ def evaluate(self, module, expr, env):
231
+ """The set of paths an expression may denote."""
232
+ kind = expr[0]
233
+ if kind == "name":
234
+ name = bsl.norm(expr[1])
235
+ if name in module.variables:
236
+ return env.get(name, set()) | self.module_env[module.key].get(name, set())
237
+ if name in env:
238
+ return env[name]
239
+ if name in THIS and module.kind == "object":
240
+ return {("this", ())}
241
+ if name in THIS and module.kind == "recordset":
242
+ return {("this", (MOVEMENTS[0], bsl.norm(module.object)))}
243
+ if name in MOVEMENTS and module.kind == "object":
244
+ return {("this", (name,))}
245
+ return set()
246
+ if kind == "member":
247
+ found = set()
248
+ member = bsl.norm(expr[2])
249
+ for root, names in self.evaluate(module, expr[1], env):
250
+ if root != "str" and len(names) < MAX_PATH:
251
+ found.add((root, names + (member,)))
252
+ found.update(self.fields.get(member, ()))
253
+ return found
254
+ if kind == "lit":
255
+ if isinstance(expr[1], type(u"")) and expr[1] and len(expr[1]) < 80:
256
+ return {("str", bsl.norm(expr[1]))}
257
+ return set()
258
+ if kind == "index":
259
+ keys = [v[1] for v in self.evaluate(module, expr[2], env) if v[0] == "str"]
260
+ if not keys:
261
+ return set()
262
+ parts = bsl.dotted(expr[1])
263
+ if parts and len(parts) == 1 and bsl.norm(parts[0]) in REGISTER_MANAGERS:
264
+ return set(("manager", (k,)) for k in keys)
265
+ found = set()
266
+ for root, names in self.evaluate(module, expr[1], env):
267
+ if root != "str" and len(names) < MAX_PATH:
268
+ found.update((root, names + (k,)) for k in keys)
269
+ return found
270
+ if kind == "callx":
271
+ return self.created_set(module, expr, env)
272
+ if kind == "tern":
273
+ return self.evaluate(module, expr[2], env) | self.evaluate(module, expr[3], env)
274
+ return set()
275
+
276
+ @staticmethod
277
+ def put(writes, sites, path, witness, new_sites):
278
+ """Record a write: the first witness found for a path, and every site behind it."""
279
+ writes.setdefault(path, witness)
280
+ sites.setdefault(path, set()).update(new_sites)
281
+
282
+ def recordset_method(self, module, path, op, writes, sites, line):
283
+ """`Набор.Расход()` where `Расход` is a method of the register's own record-set
284
+ module: what it writes through its own set lands on the set the caller holds."""
285
+ register = self.register_of(path)
286
+ rs = self.recordsets.get(register) if register else None
287
+ if rs is None or op not in rs.methods:
288
+ return
289
+ key = (rs.key, op)
290
+ summary = self.summary.get(key)
291
+ if not summary:
292
+ return
293
+ callee_sites = self.sites.get(key, {})
294
+ root, names = path
295
+ prefix = names[:-2]
296
+ step = u"%s:%d -> record set %s.%s" % (module.path, line, rs.object, rs.methods[op][1])
297
+ for spath, witness in summary.items():
298
+ sroot, snames = spath
299
+ if sroot == "ctx":
300
+ target = spath
301
+ elif sroot == "this" and len(prefix + snames) <= MAX_PATH:
302
+ target = (root, prefix + snames)
303
+ else:
304
+ continue
305
+ self.put(writes, sites, target, (step,) + witness, callee_sites.get(spath, ()))
306
+
307
+ @staticmethod
308
+ def is_insert(call):
309
+ """`Х.Вставить("Имя", Значение)`: a structure field set by name."""
310
+ if call[0] != "callx" or call[1][0] != "member" or bsl.norm(call[1][2]) not in INSERT:
311
+ return False
312
+ args = call[2]
313
+ return (len(args) >= 2 and args[0] is not None and args[0][0] == "lit"
314
+ and isinstance(args[0][1], type(u"")) and args[1] is not None)
315
+
316
+ def keep_field(self, field, values):
317
+ for value in values:
318
+ if value[0] == "ctx" and value not in self.fields[field]:
319
+ self.fields[field].add(value)
320
+ self.module_changes += 1
321
+
322
+ def created_set(self, module, call, env):
323
+ """`РегистрыНакопления.X.СоздатьНаборЗаписей()`: a set written under the context's
324
+ document. Rooted at "ctx" so it travels up through every caller."""
325
+ callee = call[1]
326
+ if callee[0] != "member" or bsl.norm(callee[2]) not in CREATE_SET:
327
+ return set()
328
+ owner = callee[1]
329
+ parts = bsl.dotted(owner)
330
+ if parts and len(parts) == 2 and bsl.norm(parts[0]) in REGISTER_MANAGERS:
331
+ return {("ctx", (MOVEMENTS[0], bsl.norm(parts[1])))}
332
+ found = set()
333
+ for root, names in self.evaluate(module, owner, env):
334
+ if root == "manager":
335
+ found.add(("ctx", (MOVEMENTS[0],) + names))
336
+ return found
337
+
338
+ def analyse(self, module, method):
339
+ """One pass over a method: its writes, as paths from its own roots."""
340
+ key = (module.key, bsl.norm(method[1]))
341
+ params = method[3]
342
+ env = {}
343
+ for i, (pname, _, _) in enumerate(params):
344
+ env[bsl.norm(pname)] = {(("param", i), ())}
345
+ writes = {}
346
+ body = method[5]
347
+ # flow-insensitive: two passes let a value assigned late reach a use above it in a loop
348
+ for _ in range(2):
349
+ for st in bsl.statements(body):
350
+ if st[0] == "assign" and st[2][0] == "name":
351
+ target = bsl.norm(st[2][1])
352
+ values = self.evaluate(module, st[3], env)
353
+ if values:
354
+ env.setdefault(target, set()).update(values)
355
+ elif st[0] == "assign" and st[2][0] == "member":
356
+ self.keep_field(bsl.norm(st[2][2]), self.evaluate(module, st[3], env))
357
+ elif st[0] == "foreach":
358
+ values = self.evaluate(module, st[3], env)
359
+ if values:
360
+ env.setdefault(bsl.norm(st[2]), set()).update(values)
361
+ if st[0] in ("assign", "call"):
362
+ for expr in bsl.expressions(st):
363
+ for sub in bsl.subexpressions(expr):
364
+ if self.is_insert(sub):
365
+ self.keep_field(bsl.norm(sub[2][0][1]),
366
+ self.evaluate(module, sub[2][1], env))
367
+ # a module variable assigned here is seen by every method of the module; a value
368
+ # rooted in this method's parameters means nothing outside it, so it stays local
369
+ shared = self.module_env[module.key]
370
+ param_names = set(bsl.norm(p[0]) for p in params)
371
+ for name in module.variables:
372
+ if name in env and name not in param_names:
373
+ for value in env[name]:
374
+ if not isinstance(value[0], tuple) and value not in shared[name]:
375
+ shared[name].add(value)
376
+ self.module_changes += 1
377
+ sites = {}
378
+ for st in bsl.statements(body):
379
+ for expr in bsl.expressions(st):
380
+ for sub in bsl.subexpressions(expr):
381
+ if sub[0] == "callx":
382
+ self.call(module, sub, env, writes, sites, st[1])
383
+ return key, writes, sites
384
+
385
+ def call(self, module, call, env, writes, sites, line):
386
+ callee, args = call[1], call[2]
387
+ # an operation on a value we follow
388
+ if callee[0] == "member":
389
+ op = bsl.norm(callee[2])
390
+ for path in self.evaluate(module, callee[1], env):
391
+ if path[0] in ("str", "manager"):
392
+ continue
393
+ if op in WRITE_OPS:
394
+ self.put(writes, sites, path,
395
+ (u"%s:%d %s" % (module.path, line, callee[2]),),
396
+ ((module.path, line, op),))
397
+ continue
398
+ self.recordset_method(module, path, op, writes, sites, line)
399
+ elif (callee[0] == "name" and module.kind == "recordset"
400
+ and bsl.norm(callee[1]) in WRITE_OPS and bsl.norm(callee[1]) not in module.methods):
401
+ # `Добавить()` inside a record-set module adds to the set itself
402
+ self.put(writes, sites, ("this", (MOVEMENTS[0], bsl.norm(module.object))),
403
+ (u"%s:%d %s" % (module.path, line, callee[1]),),
404
+ ((module.path, line, bsl.norm(callee[1])),))
405
+ target = self.resolve(module, callee, env)
406
+ if not isinstance(target, tuple):
407
+ self.unresolved[target] += 1
408
+ return
409
+ tmodule, tmethod = target
410
+ key = (tmodule.key, bsl.norm(tmethod[1]))
411
+ summary = self.summary.get(key)
412
+ if not summary:
413
+ return
414
+ callee_sites = self.sites.get(key, {})
415
+ step = u"%s:%d -> %s.%s" % (module.path, line, tmodule.object, tmethod[1])
416
+ arg_values = [self.evaluate(module, a, env) if a is not None else set() for a in args]
417
+ for spath, witness in summary.items():
418
+ root, names = spath
419
+ behind = callee_sites.get(spath, ())
420
+ if root == "ctx":
421
+ self.put(writes, sites, spath, (step,) + witness, behind)
422
+ continue
423
+ if root == "this":
424
+ # the callee's own object: the same object when it is this module's method
425
+ if tmodule is module:
426
+ self.put(writes, sites, spath, witness, behind)
427
+ continue
428
+ index = root[1]
429
+ if index >= len(arg_values):
430
+ continue
431
+ for aroot, anames in arg_values[index]:
432
+ if aroot in ("str", "manager"):
433
+ continue
434
+ joined = anames + names
435
+ if len(joined) <= MAX_PATH:
436
+ self.put(writes, sites, (aroot, joined), (step,) + witness, behind)
437
+
438
+ def run(self, verbose=True):
439
+ methods = [(m, method) for m in self.modules for method in m.methods.values()]
440
+ rounds = 0
441
+ while True:
442
+ rounds += 1
443
+ self.unresolved = collections.Counter()
444
+ self.module_changes = 0
445
+ changed = 0
446
+ for module, method in methods:
447
+ key, writes, sites = self.analyse(module, method)
448
+ current = self.summary[key]
449
+ for path, witness in writes.items():
450
+ if path not in current:
451
+ current[path] = witness
452
+ changed += 1
453
+ known = self.sites[key]
454
+ for path, found in sites.items():
455
+ have = known.setdefault(path, set())
456
+ if not found <= have:
457
+ have.update(found)
458
+ changed += 1
459
+ if verbose:
460
+ sys.stderr.write("[i] round %d: %d new paths\n" % (rounds, changed))
461
+ changed += self.module_changes
462
+ if not changed:
463
+ return rounds
464
+
465
+ def _handler_writes(self, module, method, base):
466
+ """What one handler writes, with its first parameter (or its own object) at `base`.
467
+
468
+ `base` is the path the handler's object stands for in the document's terms: () for
469
+ the document itself, ('движения', R) for the record set of register R."""
470
+ found = {}
471
+ key = (module.key, bsl.norm(method[1]))
472
+ sites = self.sites.get(key, {})
473
+ for (root, names), witness in self.summary.get(key, {}).items():
474
+ if root == "ctx":
475
+ path = names
476
+ elif root == "this" and module.kind in ("object", "recordset"):
477
+ path = names
478
+ elif root == ("param", 0) and module.kind == "common":
479
+ path = base + names
480
+ else:
481
+ continue
482
+ register = self.register_of(("this", path))
483
+ if register is not None:
484
+ entry = found.setdefault(register, (witness, set()))
485
+ entry[1].update(sites.get((root, names), ()))
486
+ return found
487
+
488
+ def document_writes(self, subscriptions, declared=None):
489
+ """Document -> event -> register -> the shortest call chain that writes it.
490
+
491
+ Beyond the document's own handlers: the subscriptions on the document's event, and,
492
+ for every register written, that register's record-set module and the subscriptions
493
+ on its write - which can write further registers, so this runs to a closure."""
494
+ recordsets = dict((bsl.norm(m.object), m) for m in self.modules if m.kind == "recordset")
495
+ result = {}
496
+ # every document, not only those with an object module: one that posts purely
497
+ # through subscriptions has none
498
+ with io.open(os.path.join(paths.OUT, "objects.json"), encoding="utf-8") as f:
499
+ names = sorted(o["name"] for o in json.load(f) if o.get("family") == "Document")
500
+ for name in names:
501
+ m = self.by_object.get(("Document", bsl.norm(name), "object"))
502
+ events = {}
503
+ for event_key, event in EVENTS.items():
504
+ found = {}
505
+ behind = collections.defaultdict(set)
506
+
507
+ def add(writes, prefix):
508
+ for register, (witness, sites) in writes.items():
509
+ behind[register].update(sites)
510
+ chain = prefix + tuple(witness)
511
+ best = found.get(register)
512
+ if best is None or len(chain) < len(best):
513
+ found[register] = chain
514
+
515
+ if m is not None and event_key in m.methods:
516
+ add(self._handler_writes(m, m.methods[event_key], ()), ())
517
+ for sub in subscriptions.get((bsl.norm(name), event_key), []):
518
+ target = self._subscription_target(sub)
519
+ if target:
520
+ add(self._handler_writes(target[0], target[1], ()),
521
+ (u"subscription %s" % sub["name"],))
522
+ # a written register fires its own write events
523
+ done = set()
524
+ while True:
525
+ pending = [r for r in found if r not in done]
526
+ if not pending:
527
+ break
528
+ for register in pending:
529
+ done.add(register)
530
+ base = (MOVEMENTS[0], register)
531
+ via = found[register]
532
+ rs = recordsets.get(register)
533
+ for trigger in (u"передзаписью", u"призаписи"):
534
+ if rs is not None and trigger in rs.methods:
535
+ add(self._handler_writes(rs, rs.methods[trigger], base),
536
+ via + (u"record set %s %s" % (rs.object, trigger),))
537
+ for sub in subscriptions.get((register, trigger), []):
538
+ target = self._subscription_target(sub)
539
+ if target:
540
+ add(self._handler_writes(target[0], target[1], base),
541
+ via + (u"subscription %s" % sub["name"],))
542
+ allowed = declared.get(bsl.norm(name)) if declared is not None else None
543
+ if allowed is not None:
544
+ for register in list(found):
545
+ if register not in allowed:
546
+ self.undeclared[(name, register)] = found.pop(register)
547
+ if found:
548
+ events[event] = found
549
+ for register in found:
550
+ self.document_sites[(name, event, register)] = behind[register]
551
+ result[name] = events
552
+ return result
553
+
554
+ def _subscription_target(self, sub):
555
+ module = self.by_guid.get(sub["module"])
556
+ if module is None:
557
+ return None
558
+ method = module.methods.get(bsl.norm(sub["handler"]))
559
+ return (module, method) if method else None
560
+
561
+
562
+ def declared_movements():
563
+ """Document -> the registers its metadata lets it record into, case-folded.
564
+
565
+ 1C refuses a movement into a register the document does not list, whichever way the
566
+ set was reached, so this bounds what the code can write. It matters for handlers that
567
+ serve every document and guard with `Метаданные().Движения.Содержит(...)`: the guard
568
+ is the platform's own rule, and without it every document would reach every register
569
+ such a handler names."""
570
+ import re
571
+ config = os.path.join(paths.OUT, "config")
572
+ with io.open(os.path.join(paths.OUT, "objects_all.json"), encoding="utf-8") as f:
573
+ objects = json.load(f)
574
+ registers = dict((o["guid"], bsl.norm(o["name"])) for o in objects
575
+ if o.get("family") in ("AccumRg", "InfoRg", "AccRg", "CRg"))
576
+ reference = re.compile(r'\{"#",157fa490-4ce9-11d4-9415-008048da11f9,\s*\{1,([0-9a-f-]{36})\}')
577
+ found = {}
578
+ for obj in objects:
579
+ if obj.get("family") != "Document":
580
+ continue
581
+ path = os.path.join(config, obj["guid"] + ".txt")
582
+ if not os.path.exists(path):
583
+ continue
584
+ with io.open(path, encoding="utf-8-sig") as f:
585
+ text = f.read()
586
+ found[bsl.norm(obj["name"])] = set(registers[g] for g in reference.findall(text)
587
+ if g in registers)
588
+ return found
589
+
590
+
591
+ def subscriptions(modules):
592
+ """(source object, event) -> subscriptions, read from the configuration's definitions.
593
+
594
+ A subscription names its sources by TYPE guid; every object lists its own type guids at
595
+ the head of its definition, which is how a source maps back to an object."""
596
+ import re
597
+ config = os.path.join(paths.OUT, "config")
598
+ with io.open(os.path.join(paths.OUT, "objects_all.json"), encoding="utf-8") as f:
599
+ objects = json.load(f)
600
+ guid = re.compile(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")
601
+ types = {}
602
+ for obj in objects:
603
+ if obj.get("family") not in ("Document", "AccumRg", "InfoRg", "AccRg", "CRg"):
604
+ continue
605
+ path = os.path.join(config, obj["guid"] + ".txt")
606
+ if not os.path.exists(path):
607
+ continue
608
+ with io.open(path, encoding="utf-8-sig") as f:
609
+ head = f.read(4000)
610
+ head = head.split("{0,0," + obj["guid"], 1)[0]
611
+ for g in guid.findall(head):
612
+ types.setdefault(g, bsl.norm(obj["name"]))
613
+ found = collections.defaultdict(list)
614
+ for obj in objects:
615
+ if obj.get("classGuid") != SUBSCRIPTION_CLASS:
616
+ continue
617
+ path = os.path.join(config, obj["guid"] + ".txt")
618
+ if not os.path.exists(path):
619
+ continue
620
+ with io.open(path, encoding="utf-8-sig") as f:
621
+ text = f.read()
622
+ m = re.search(u'\{"Pattern",(.*?)\},"([A-Za-z]+)_([^"]+)",(%s),"([^"]+)"'
623
+ % guid.pattern, text, re.S | re.U)
624
+ if not m:
625
+ continue
626
+ event = bsl.norm(m.group(3))
627
+ record = {"name": obj["name"], "module": m.group(4), "handler": m.group(5)}
628
+ for g in guid.findall(m.group(1)):
629
+ source = types.get(g)
630
+ if source:
631
+ found[(source, event)].append(record)
632
+ return found
633
+
634
+
635
+ # -- output -----------------------------------------------------------------------------
636
+
637
+ def main():
638
+ args = sys.argv[1:]
639
+ modules, failures = load()
640
+ registers = register_names()
641
+ graph = Graph(modules, registers)
642
+ rounds = graph.run()
643
+ documents = graph.document_writes(subscriptions(modules), declared_movements())
644
+
645
+ out = codecs.getwriter("utf-8")(sys.stdout.buffer)
646
+ if args:
647
+ wanted = args[0]
648
+ for event, found in sorted(documents.get(wanted, {}).items()):
649
+ out.write(u"%s\n" % event)
650
+ for register, chain in sorted(found.items()):
651
+ known = registers.get(register)
652
+ out.write(u" %s%s\n" % (known[1] if known else register,
653
+ u"" if known else u" (not a register)"))
654
+ for step in chain:
655
+ out.write(u" %s\n" % step)
656
+ return
657
+
658
+ old = {}
659
+ with io.open(os.path.join(paths.OUT, "handlers.json"), encoding="utf-8") as f:
660
+ for row in json.load(f):
661
+ regs = set(row.get("direct") or [])
662
+ for found in (row.get("viaCalls") or {}).values():
663
+ regs.update(found)
664
+ for found in (row.get("outsidePosting") or {}).values():
665
+ regs.update(found)
666
+ old[row.get("doc")] = regs
667
+ records = []
668
+ unknown = collections.Counter()
669
+ for document in sorted(documents):
670
+ events = documents[document]
671
+ posting = {}
672
+ for event in ("posting", "on-write", "before-write"):
673
+ for register, chain in events.get(event, {}).items():
674
+ posting.setdefault(register, (event, chain))
675
+ named, strays = [], []
676
+ for register in posting:
677
+ known = registers.get(register)
678
+ if known:
679
+ named.append(known[1])
680
+ else:
681
+ strays.append(register)
682
+ unknown[register] += 1
683
+ records.append({
684
+ "document": document,
685
+ "client": document.startswith(handlers.CLIENT),
686
+ "registers": sorted(named),
687
+ "events": dict((event, dict(
688
+ (registers[r][1] if r in registers else r, list(chain))
689
+ for r, chain in sorted(found.items())))
690
+ for event, found in events.items()),
691
+ "not_registers": sorted(strays),
692
+ })
693
+ with codecs.open(TARGET, "w", "utf-8") as f:
694
+ json.dump({"rounds": rounds,
695
+ "unresolved_calls": dict(graph.unresolved.most_common()),
696
+ "parse_failures": failures,
697
+ "documents": records}, f, ensure_ascii=False, indent="\t")
698
+
699
+ writing = [r for r in records if r["registers"]]
700
+ out.write(u"%d documents, %d write a register (%d rounds)\n"
701
+ % (len(records), len(writing), rounds))
702
+ out.write(u"unresolved calls: %s\n" % u", ".join(
703
+ u"%s %d" % kv for kv in graph.unresolved.most_common()))
704
+ if unknown:
705
+ out.write(u"paths that name no register: %s\n" % u", ".join(
706
+ u"%s %d" % kv for kv in unknown.most_common(10)))
707
+ out.write(u"\n%-48s %5s %5s %s\n" % (u"document", u"now", u"was", u"registers"))
708
+ for r in sorted(writing, key=lambda r: (-len(r["registers"]), r["document"])):
709
+ was = old.get(r["document"])
710
+ out.write(u"%-48s %5d %5s %s\n" % (r["document"][:48], len(r["registers"]),
711
+ len(was) if was is not None else u"-",
712
+ u", ".join(r["registers"])[:120]))
713
+ out.write(u"\nwritten %s\n" % TARGET)
714
+
715
+
716
+ if __name__ == "__main__":
717
+ main()