@dforge-core/dforge-mcp 0.2.44 → 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.
- package/CHANGELOG.md +73 -0
- package/README.md +3 -2
- package/dist/server.js +109 -84
- package/package.json +48 -46
- package/resources/docs/conventions.md +17 -14
- package/resources/docs/dsl-reference.md +15 -5
- package/resources/schemas/entity.schema.json +1 -1
- package/resources/schemas/registers.schema.json +8 -3
- package/scripts/install-skills.mjs +1 -0
- package/skills/dforge-1c-import/SKILL.md +374 -0
- package/skills/dforge-1c-import/references/environment.md +97 -0
- package/skills/dforge-1c-import/references/loading.md +161 -0
- package/skills/dforge-1c-import/references/measurements.md +210 -0
- package/skills/dforge-1c-import/references/print-forms.md +148 -0
- package/skills/dforge-1c-import/scripts/1c.py +178 -0
- package/skills/dforge-1c-import/scripts/README.md +110 -0
- package/skills/dforge-1c-import/scripts/lib/analyze.py +83 -0
- package/skills/dforge-1c-import/scripts/lib/attrs.py +278 -0
- package/skills/dforge-1c-import/scripts/lib/autospec.py +964 -0
- package/skills/dforge-1c-import/scripts/lib/binding.py +257 -0
- package/skills/dforge-1c-import/scripts/lib/bsl.py +811 -0
- package/skills/dforge-1c-import/scripts/lib/callgraph.py +717 -0
- package/skills/dforge-1c-import/scripts/lib/cli.py +53 -0
- package/skills/dforge-1c-import/scripts/lib/columnmap.py +346 -0
- package/skills/dforge-1c-import/scripts/lib/coverage.py +242 -0
- package/skills/dforge-1c-import/scripts/lib/custom.py +83 -0
- package/skills/dforge-1c-import/scripts/lib/delta.py +741 -0
- package/skills/dforge-1c-import/scripts/lib/deps.py +136 -0
- package/skills/dforge-1c-import/scripts/lib/docusage.py +105 -0
- package/skills/dforge-1c-import/scripts/lib/dump1c.py +251 -0
- package/skills/dforge-1c-import/scripts/lib/explore.py +55 -0
- package/skills/dforge-1c-import/scripts/lib/external.py +201 -0
- package/skills/dforge-1c-import/scripts/lib/extract_code.py +251 -0
- package/skills/dforge-1c-import/scripts/lib/forms.py +239 -0
- package/skills/dforge-1c-import/scripts/lib/genmodule.py +1432 -0
- package/skills/dforge-1c-import/scripts/lib/handlers.py +202 -0
- package/skills/dforge-1c-import/scripts/lib/handmap.py +142 -0
- package/skills/dforge-1c-import/scripts/lib/i18n_coverage.py +87 -0
- package/skills/dforge-1c-import/scripts/lib/idioms.py +273 -0
- package/skills/dforge-1c-import/scripts/lib/integrity.py +264 -0
- package/skills/dforge-1c-import/scripts/lib/inventory.py +535 -0
- package/skills/dforge-1c-import/scripts/lib/layers.py +106 -0
- package/skills/dforge-1c-import/scripts/lib/meta.py +315 -0
- package/skills/dforge-1c-import/scripts/lib/migrate.py +1299 -0
- package/skills/dforge-1c-import/scripts/lib/mxl.py +368 -0
- package/skills/dforge-1c-import/scripts/lib/naming.py +251 -0
- package/skills/dforge-1c-import/scripts/lib/paths.py +90 -0
- package/skills/dforge-1c-import/scripts/lib/peek.py +70 -0
- package/skills/dforge-1c-import/scripts/lib/posted.py +218 -0
- package/skills/dforge-1c-import/scripts/lib/posting.py +655 -0
- package/skills/dforge-1c-import/scripts/lib/printgen.py +239 -0
- package/skills/dforge-1c-import/scripts/lib/rebuild.py +194 -0
- package/skills/dforge-1c-import/scripts/lib/recorded.py +170 -0
- package/skills/dforge-1c-import/scripts/lib/recorders.py +94 -0
- package/skills/dforge-1c-import/scripts/lib/reftargets.py +95 -0
- package/skills/dforge-1c-import/scripts/lib/reportgen.py +157 -0
- package/skills/dforge-1c-import/scripts/lib/roles.py +33 -0
- package/skills/dforge-1c-import/scripts/lib/seedstate.py +488 -0
- package/skills/dforge-1c-import/scripts/lib/sizing.py +184 -0
- package/skills/dforge-1c-import/scripts/lib/spec.py +400 -0
- package/skills/dforge-1c-import/scripts/lib/statements.py +774 -0
- package/skills/dforge-1c-import/scripts/lib/subsystems.py +480 -0
- package/skills/dforge-1c-import/scripts/lib/terms.py +451 -0
- package/skills/dforge-1c-import/scripts/lib/terms_uk.py +555 -0
- package/skills/dforge-1c-import/scripts/lib/totals.py +387 -0
- package/skills/dforge-1c-import/scripts/lib/typemap.py +73 -0
- package/skills/dforge-1c-import/scripts/lib/unpack.py +136 -0
- package/skills/dforge-1c-import/scripts/lib/validate.py +508 -0
- package/skills/dforge-1c-import/scripts/project-template/contours.py +47 -0
- package/skills/dforge-1c-import/scripts/project-template/sources.py +14 -0
- package/skills/dforge-mcp-author/SKILL.md +8 -2
- package/skills/dforge-mcp-author/examples/column-security/entities/product.json +1 -1
- package/skills/dforge-mcp-author/examples/column-security/manifest.json +0 -1
- package/skills/dforge-mcp-author/examples/column-security/security/roles.json +2 -2
- package/skills/dforge-mcp-author/references/action-dsl.md +20 -7
- package/skills/dforge-mcp-author/references/conventions.md +7 -1
- package/skills/dforge-mcp-author/references/field-types.md +3 -2
- package/skills/dforge-mcp-author/references/formulas.md +5 -1
- package/skills/dforge-mcp-author/references/print-templates.md +5 -2
- package/skills/dforge-mcp-author/references/queries.md +1 -1
- package/skills/dforge-mcp-author/references/registers.md +18 -3
- package/skills/dforge-mcp-author/references/reports.md +4 -1
- package/skills/dforge-mcp-author/references/schema-import.md +1 -1
- package/skills/dforge-mcp-author/references/security.md +1 -1
- package/skills/dforge-mcp-author/references/translations.md +38 -15
- package/skills/dforge-mcp-author/references/validation-checklist.md +2 -3
- package/skills/dforge-module-build/SKILL.md +10 -1
- package/skills/dforge-module-ship/SKILL.md +31 -3
- package/skills/dforge-mcp-author/examples/column-security/translations/en-US.json +0 -35
|
@@ -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()
|