@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.
Files changed (89) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +3 -2
  3. package/dist/server.js +109 -84
  4. package/package.json +48 -46
  5. package/resources/docs/conventions.md +17 -14
  6. package/resources/docs/dsl-reference.md +15 -5
  7. package/resources/schemas/entity.schema.json +1 -1
  8. package/resources/schemas/registers.schema.json +8 -3
  9. package/scripts/install-skills.mjs +1 -0
  10. package/skills/dforge-1c-import/SKILL.md +374 -0
  11. package/skills/dforge-1c-import/references/environment.md +97 -0
  12. package/skills/dforge-1c-import/references/loading.md +161 -0
  13. package/skills/dforge-1c-import/references/measurements.md +210 -0
  14. package/skills/dforge-1c-import/references/print-forms.md +148 -0
  15. package/skills/dforge-1c-import/scripts/1c.py +178 -0
  16. package/skills/dforge-1c-import/scripts/README.md +110 -0
  17. package/skills/dforge-1c-import/scripts/lib/analyze.py +83 -0
  18. package/skills/dforge-1c-import/scripts/lib/attrs.py +278 -0
  19. package/skills/dforge-1c-import/scripts/lib/autospec.py +964 -0
  20. package/skills/dforge-1c-import/scripts/lib/binding.py +257 -0
  21. package/skills/dforge-1c-import/scripts/lib/bsl.py +811 -0
  22. package/skills/dforge-1c-import/scripts/lib/callgraph.py +717 -0
  23. package/skills/dforge-1c-import/scripts/lib/cli.py +53 -0
  24. package/skills/dforge-1c-import/scripts/lib/columnmap.py +346 -0
  25. package/skills/dforge-1c-import/scripts/lib/coverage.py +242 -0
  26. package/skills/dforge-1c-import/scripts/lib/custom.py +83 -0
  27. package/skills/dforge-1c-import/scripts/lib/delta.py +741 -0
  28. package/skills/dforge-1c-import/scripts/lib/deps.py +136 -0
  29. package/skills/dforge-1c-import/scripts/lib/docusage.py +105 -0
  30. package/skills/dforge-1c-import/scripts/lib/dump1c.py +251 -0
  31. package/skills/dforge-1c-import/scripts/lib/explore.py +55 -0
  32. package/skills/dforge-1c-import/scripts/lib/external.py +201 -0
  33. package/skills/dforge-1c-import/scripts/lib/extract_code.py +251 -0
  34. package/skills/dforge-1c-import/scripts/lib/forms.py +239 -0
  35. package/skills/dforge-1c-import/scripts/lib/genmodule.py +1432 -0
  36. package/skills/dforge-1c-import/scripts/lib/handlers.py +202 -0
  37. package/skills/dforge-1c-import/scripts/lib/handmap.py +142 -0
  38. package/skills/dforge-1c-import/scripts/lib/i18n_coverage.py +87 -0
  39. package/skills/dforge-1c-import/scripts/lib/idioms.py +273 -0
  40. package/skills/dforge-1c-import/scripts/lib/integrity.py +264 -0
  41. package/skills/dforge-1c-import/scripts/lib/inventory.py +535 -0
  42. package/skills/dforge-1c-import/scripts/lib/layers.py +106 -0
  43. package/skills/dforge-1c-import/scripts/lib/meta.py +315 -0
  44. package/skills/dforge-1c-import/scripts/lib/migrate.py +1299 -0
  45. package/skills/dforge-1c-import/scripts/lib/mxl.py +368 -0
  46. package/skills/dforge-1c-import/scripts/lib/naming.py +251 -0
  47. package/skills/dforge-1c-import/scripts/lib/paths.py +90 -0
  48. package/skills/dforge-1c-import/scripts/lib/peek.py +70 -0
  49. package/skills/dforge-1c-import/scripts/lib/posted.py +218 -0
  50. package/skills/dforge-1c-import/scripts/lib/posting.py +655 -0
  51. package/skills/dforge-1c-import/scripts/lib/printgen.py +239 -0
  52. package/skills/dforge-1c-import/scripts/lib/rebuild.py +194 -0
  53. package/skills/dforge-1c-import/scripts/lib/recorded.py +170 -0
  54. package/skills/dforge-1c-import/scripts/lib/recorders.py +94 -0
  55. package/skills/dforge-1c-import/scripts/lib/reftargets.py +95 -0
  56. package/skills/dforge-1c-import/scripts/lib/reportgen.py +157 -0
  57. package/skills/dforge-1c-import/scripts/lib/roles.py +33 -0
  58. package/skills/dforge-1c-import/scripts/lib/seedstate.py +488 -0
  59. package/skills/dforge-1c-import/scripts/lib/sizing.py +184 -0
  60. package/skills/dforge-1c-import/scripts/lib/spec.py +400 -0
  61. package/skills/dforge-1c-import/scripts/lib/statements.py +774 -0
  62. package/skills/dforge-1c-import/scripts/lib/subsystems.py +480 -0
  63. package/skills/dforge-1c-import/scripts/lib/terms.py +451 -0
  64. package/skills/dforge-1c-import/scripts/lib/terms_uk.py +555 -0
  65. package/skills/dforge-1c-import/scripts/lib/totals.py +387 -0
  66. package/skills/dforge-1c-import/scripts/lib/typemap.py +73 -0
  67. package/skills/dforge-1c-import/scripts/lib/unpack.py +136 -0
  68. package/skills/dforge-1c-import/scripts/lib/validate.py +508 -0
  69. package/skills/dforge-1c-import/scripts/project-template/contours.py +47 -0
  70. package/skills/dforge-1c-import/scripts/project-template/sources.py +14 -0
  71. package/skills/dforge-mcp-author/SKILL.md +8 -2
  72. package/skills/dforge-mcp-author/examples/column-security/entities/product.json +1 -1
  73. package/skills/dforge-mcp-author/examples/column-security/manifest.json +0 -1
  74. package/skills/dforge-mcp-author/examples/column-security/security/roles.json +2 -2
  75. package/skills/dforge-mcp-author/references/action-dsl.md +20 -7
  76. package/skills/dforge-mcp-author/references/conventions.md +7 -1
  77. package/skills/dforge-mcp-author/references/field-types.md +3 -2
  78. package/skills/dforge-mcp-author/references/formulas.md +5 -1
  79. package/skills/dforge-mcp-author/references/print-templates.md +5 -2
  80. package/skills/dforge-mcp-author/references/queries.md +1 -1
  81. package/skills/dforge-mcp-author/references/registers.md +18 -3
  82. package/skills/dforge-mcp-author/references/reports.md +4 -1
  83. package/skills/dforge-mcp-author/references/schema-import.md +1 -1
  84. package/skills/dforge-mcp-author/references/security.md +1 -1
  85. package/skills/dforge-mcp-author/references/translations.md +38 -15
  86. package/skills/dforge-mcp-author/references/validation-checklist.md +2 -3
  87. package/skills/dforge-module-build/SKILL.md +10 -1
  88. package/skills/dforge-module-ship/SKILL.md +31 -3
  89. package/skills/dforge-mcp-author/examples/column-security/translations/en-US.json +0 -35
@@ -0,0 +1,811 @@
1
+ # -*- coding: utf-8 -*-
2
+ u"""A parser for 1C:Enterprise 8 built-in language (BSL), to a plain tree.
3
+
4
+ The first readers of this configuration's code found `Движения.<name>` by text and followed
5
+ calls one level. That cannot see what УТП actually does: a register set taken into a
6
+ variable (`ПроводкиБУ = Движения.Хозрасчетный`), a row added through it, the set handed to
7
+ a common module as an argument. Following values needs a parse tree, and this is it.
8
+
9
+ Why written here rather than taken: the grammar of `bsl-language-server` is ANTLR and
10
+ needs a JVM to generate a parser from, and there is no tree-sitter grammar for this
11
+ Python. BSL is small - a Pascal-like statement language with one expression grammar - so
12
+ a recursive-descent parser is a few hundred lines with no dependency, and it reads the
13
+ whole configuration in minutes.
14
+
15
+ What it does not do, on purpose:
16
+
17
+ * The preprocessor is evaluated for one context, the server by default, because that is
18
+ where posting runs; excluded lines are blanked so line numbers hold. This
19
+ configuration has `#Если` branches that split a single statement, so keeping both
20
+ branches does not parse.
21
+ * Query text is a string literal. What a query reads is a separate question.
22
+
23
+ The tree is tuples, first element the node kind, so it is cheap to build for two million
24
+ lines and easy to pattern-match. Every statement carries its line.
25
+
26
+ Module ('module', vars, methods, body)
27
+ Method ('method', name, is_function, params, export, body, line, end_line)
28
+ param (name, by_value, default_expr_or_None)
29
+
30
+ Statements ('assign', line, target, value)
31
+ ('call', line, expr) # a call used as a statement
32
+ ('if', line, [(cond, body)...], else_body_or_None)
33
+ ('while', line, cond, body)
34
+ ('for', line, var, start, stop, body)
35
+ ('foreach', line, var, collection, body)
36
+ ('try', line, body, except_body)
37
+ ('return', line, expr_or_None)
38
+ ('raise', line, expr_or_None)
39
+ ('break', line) ('continue', line) ('goto', line, label)
40
+ ('label', line, label) ('var', line, names) ('execute', line, expr)
41
+ ('handler', line, 'add'|'remove', event_expr, handler_expr)
42
+
43
+ Expressions ('name', id) ('lit', value) ('member', obj, name) ('index', obj, key)
44
+ ('callx', callee, args) - args hold None for an omitted argument
45
+ ('new', type_name_or_None, args) ('bin', op, a, b) ('not', a)
46
+ ('neg', a) ('tern', cond, a, b)
47
+
48
+ Names keep their source spelling; `norm()` gives the case-folded key 1C itself compares by.
49
+ """
50
+ from __future__ import print_function
51
+
52
+ import re
53
+
54
+ # Russian and English spellings of every keyword, mapped to one canonical form.
55
+ _KEYWORDS = {
56
+ u"если": "if", u"if": "if",
57
+ u"тогда": "then", u"then": "then",
58
+ u"иначеесли": "elsif", u"elsif": "elsif",
59
+ u"иначе": "else", u"else": "else",
60
+ u"конецесли": "endif", u"endif": "endif",
61
+ u"для": "for", u"for": "for",
62
+ u"каждого": "each", u"each": "each",
63
+ u"из": "in", u"in": "in",
64
+ u"по": "to", u"to": "to",
65
+ u"пока": "while", u"while": "while",
66
+ u"цикл": "do", u"do": "do",
67
+ u"конеццикла": "enddo", u"enddo": "enddo",
68
+ u"процедура": "procedure", u"procedure": "procedure",
69
+ u"функция": "function", u"function": "function",
70
+ u"конецпроцедуры": "endprocedure", u"endprocedure": "endprocedure",
71
+ u"конецфункции": "endfunction", u"endfunction": "endfunction",
72
+ u"перем": "var", u"var": "var",
73
+ u"экспорт": "export", u"export": "export",
74
+ u"знач": "val", u"val": "val",
75
+ u"возврат": "return", u"return": "return",
76
+ u"прервать": "break", u"break": "break",
77
+ u"продолжить": "continue", u"continue": "continue",
78
+ u"попытка": "try", u"try": "try",
79
+ u"исключение": "except", u"except": "except",
80
+ u"конецпопытки": "endtry", u"endtry": "endtry",
81
+ u"вызватьисключение": "raise", u"raise": "raise",
82
+ u"новый": "new", u"new": "new",
83
+ u"и": "and", u"and": "and",
84
+ u"или": "or", u"or": "or",
85
+ u"не": "not", u"not": "not",
86
+ u"истина": "true", u"true": "true",
87
+ u"ложь": "false", u"false": "false",
88
+ u"неопределено": "undefined", u"undefined": "undefined",
89
+ u"null": "null",
90
+ u"перейти": "goto", u"goto": "goto",
91
+ }
92
+
93
+ _TOKEN = re.compile(u"""
94
+ (?P<ws>[ \\t\\r\\f\\v\\ufeff\\xa0]+)
95
+ |(?P<nl>\\n)
96
+ |(?P<comment>//[^\\n]*)
97
+ |(?P<pre>(?<![^\\n \\t])\\#[^\\n]*)
98
+ |(?P<directive>&[^\\n]*)
99
+ |(?P<string>"(?:[^"\\n]|""|\\n(?:[ \\t\\r]*(?://[^\\n]*)?\\n)*[ \\t]*\\|)*")
100
+ |(?P<date>'[^'\\n]*')
101
+ |(?P<number>[0-9]+(?:\\.[0-9]+)?)
102
+ |(?P<ident>[^\\W\\d]\\w*)
103
+ |(?P<op><>|<=|>=|[-+*/%=<>(),.;\\[\\]?:~])
104
+ """, re.U | re.X)
105
+
106
+
107
+ class ParseError(Exception):
108
+ def __init__(self, message, line):
109
+ Exception.__init__(self, u"line %d: %s" % (line, message))
110
+ self.line = line
111
+
112
+
113
+ def norm(name):
114
+ return name.lower().replace(u"ё", u"е")
115
+
116
+
117
+ def tokenize(text):
118
+ """(kind, value, line) - kind is 'kw', 'id', 'str', 'num', 'date', 'op', 'eof'."""
119
+ out = []
120
+ line = 1
121
+ pos = 0
122
+ end = len(text)
123
+ match = _TOKEN.match
124
+ while pos < end:
125
+ m = match(text, pos)
126
+ if m is None:
127
+ raise ParseError(u"unexpected character %r" % text[pos], line)
128
+ kind = m.lastgroup
129
+ value = m.group(kind)
130
+ if kind == "nl":
131
+ line += 1
132
+ elif kind in ("ws", "comment", "pre", "directive"):
133
+ pass
134
+ elif kind == "string":
135
+ out.append(("str", _string(value), line))
136
+ line += value.count("\n")
137
+ elif kind == "ident":
138
+ key = _KEYWORDS.get(norm(value))
139
+ out.append(("kw", key, line) if key else ("id", value, line))
140
+ elif kind == "number":
141
+ out.append(("num", value, line))
142
+ elif kind == "date":
143
+ out.append(("date", value[1:-1], line))
144
+ else:
145
+ out.append(("op", value, line))
146
+ pos = m.end()
147
+ out.append(("eof", None, line))
148
+ return out
149
+
150
+
151
+ def _string(raw):
152
+ """The literal's value: quotes doubled inside, continuation lines start with `|`."""
153
+ body = raw[1:-1]
154
+ if "\n" in body:
155
+ parts = body.split("\n")
156
+ kept = [parts[0]]
157
+ for part in parts[1:]:
158
+ stripped = part.lstrip()
159
+ if stripped.startswith("|"):
160
+ kept.append(stripped[1:])
161
+ # a `//` line between continuation lines is a comment, not text
162
+ body = "\n".join(kept)
163
+ return body.replace('""', '"')
164
+
165
+
166
+ # `ДобавитьОбработчик Объект.Событие, Обработчик;` - a statement, not a call.
167
+ _HANDLER_STATEMENTS = {u"добавитьобработчик": "add", u"addhandler": "add",
168
+ u"удалитьобработчик": "remove", u"removehandler": "remove"}
169
+
170
+ # Statement-ending keywords: a block reads statements until it meets one of these.
171
+ _BLOCK_END = frozenset(["elsif", "else", "endif", "enddo", "endprocedure", "endfunction",
172
+ "except", "endtry"])
173
+
174
+
175
+ class Parser(object):
176
+
177
+ def __init__(self, tokens):
178
+ self.t = tokens
179
+ self.i = 0
180
+
181
+ # -- token helpers ------------------------------------------------------------------
182
+
183
+ def peek(self, offset=0):
184
+ return self.t[min(self.i + offset, len(self.t) - 1)]
185
+
186
+ def next(self):
187
+ tok = self.t[self.i]
188
+ if self.i < len(self.t) - 1:
189
+ self.i += 1
190
+ return tok
191
+
192
+ def at(self, kind, value=None):
193
+ tok = self.t[self.i]
194
+ return tok[0] == kind and (value is None or tok[1] == value)
195
+
196
+ def accept(self, kind, value=None):
197
+ if self.at(kind, value):
198
+ return self.next()
199
+ return None
200
+
201
+ def expect(self, kind, value=None):
202
+ tok = self.t[self.i]
203
+ if tok[0] == kind and (value is None or tok[1] == value):
204
+ return self.next()
205
+ raise ParseError(u"expected %s, got %s %r" % (value or kind, tok[0], tok[1]), tok[2])
206
+
207
+ def skip_semis(self):
208
+ while self.accept("op", ";"):
209
+ pass
210
+
211
+ # -- module -------------------------------------------------------------------------
212
+
213
+ def module(self):
214
+ """Module-level `Перем`, methods and trailing body. A method that fails to parse is
215
+ skipped to its end and reported, so one odd procedure does not lose the module."""
216
+ variables, methods, body, errors = [], [], [], []
217
+ while not self.at("eof"):
218
+ self.skip_semis()
219
+ if self.at("eof"):
220
+ break
221
+ if self.at("kw", "var") and not methods:
222
+ variables.append(self.var_statement())
223
+ continue
224
+ if self.at("kw", "procedure") or self.at("kw", "function"):
225
+ start = self.i
226
+ try:
227
+ methods.append(self.method())
228
+ except ParseError as error:
229
+ errors.append(error)
230
+ self.i = start
231
+ self._skip_method()
232
+ continue
233
+ try:
234
+ statement = self.statement()
235
+ except ParseError as error:
236
+ errors.append(error)
237
+ self._skip_line()
238
+ continue
239
+ if statement is not None:
240
+ body.append(statement)
241
+ return ("module", variables, methods, body), errors
242
+
243
+ def _skip_method(self):
244
+ self.next()
245
+ while not self.at("eof"):
246
+ tok = self.next()
247
+ if tok[0] == "kw" and tok[1] in ("endprocedure", "endfunction"):
248
+ return
249
+
250
+ def _skip_line(self):
251
+ line = self.peek()[2]
252
+ while not self.at("eof") and self.peek()[2] == line:
253
+ self.next()
254
+
255
+ def method(self):
256
+ head = self.next()
257
+ is_function = head[1] == "function"
258
+ name = self.expect("id")[1]
259
+ self.expect("op", "(")
260
+ params = []
261
+ while not self.at("op", ")"):
262
+ by_value = bool(self.accept("kw", "val"))
263
+ pname = self.expect("id")[1]
264
+ default = None
265
+ if self.accept("op", "="):
266
+ default = self.unary()
267
+ params.append((pname, by_value, default))
268
+ if not self.accept("op", ","):
269
+ break
270
+ self.expect("op", ")")
271
+ export = bool(self.accept("kw", "export"))
272
+ body = self.block()
273
+ end = self.next()
274
+ if end[0] != "kw" or end[1] not in ("endprocedure", "endfunction"):
275
+ raise ParseError(u"expected the end of %s, got %r" % (name, end[1]), end[2])
276
+ return ("method", name, is_function, params, export, body, head[2], end[2])
277
+
278
+ # -- statements ---------------------------------------------------------------------
279
+
280
+ def block(self):
281
+ body = []
282
+ while True:
283
+ self.skip_semis()
284
+ tok = self.peek()
285
+ if tok[0] == "eof" or (tok[0] == "kw" and tok[1] in _BLOCK_END):
286
+ return body
287
+ if tok[0] == "kw" and tok[1] in ("procedure", "function"):
288
+ raise ParseError(u"a method begins inside a block", tok[2])
289
+ statement = self.statement()
290
+ if statement is not None:
291
+ body.append(statement)
292
+
293
+ def statement(self):
294
+ tok = self.peek()
295
+ line = tok[2]
296
+ kind, value = tok[0], tok[1]
297
+ if kind == "op" and value == "~":
298
+ self.next()
299
+ label = self.expect("id")[1]
300
+ self.expect("op", ":")
301
+ return ("label", line, label)
302
+ if kind == "kw":
303
+ if value == "if":
304
+ return self.if_statement()
305
+ if value == "while":
306
+ self.next()
307
+ cond = self.expression()
308
+ self.expect("kw", "do")
309
+ body = self.block()
310
+ self.expect("kw", "enddo")
311
+ return ("while", line, cond, body)
312
+ if value == "for":
313
+ return self.for_statement()
314
+ if value == "try":
315
+ self.next()
316
+ body = self.block()
317
+ self.expect("kw", "except")
318
+ handler = self.block()
319
+ self.expect("kw", "endtry")
320
+ return ("try", line, body, handler)
321
+ if value == "return":
322
+ self.next()
323
+ expr = None if self._statement_ends() else self.expression()
324
+ return ("return", line, expr)
325
+ if value == "raise":
326
+ self.next()
327
+ expr = None if self._statement_ends() else self.expression()
328
+ return ("raise", line, expr)
329
+ if value == "break":
330
+ self.next()
331
+ return ("break", line)
332
+ if value == "continue":
333
+ self.next()
334
+ return ("continue", line)
335
+ if value == "goto":
336
+ self.next()
337
+ self.expect("op", "~")
338
+ return ("goto", line, self.expect("id")[1])
339
+ if value == "var":
340
+ return self.var_statement()
341
+ # `Выполнить Текст` without brackets; `Выполнить(Текст)` parses as a call
342
+ if kind == "id" and norm(value) in (u"выполнить", u"execute") \
343
+ and self.peek(1)[0] in ("id", "str"):
344
+ self.next()
345
+ return ("execute", line, self.expression())
346
+ if kind == "id" and norm(value) in _HANDLER_STATEMENTS and self.peek(1)[0] == "id":
347
+ self.next()
348
+ event = self.postfix()
349
+ self.expect("op", ",")
350
+ return ("handler", line, _HANDLER_STATEMENTS[norm(value)], event, self.postfix())
351
+ target = self.postfix()
352
+ if self.accept("op", "="):
353
+ return ("assign", line, target, self.expression())
354
+ if target[0] != "callx":
355
+ # `Выполнить(текст)` parses as a call; anything else bare is an error
356
+ raise ParseError(u"a statement that is neither a call nor an assignment", line)
357
+ return ("call", line, target)
358
+
359
+ def _statement_ends(self):
360
+ tok = self.peek()
361
+ return (tok[0] == "op" and tok[1] == ";") or tok[0] == "eof" or \
362
+ (tok[0] == "kw" and tok[1] in _BLOCK_END)
363
+
364
+ def var_statement(self):
365
+ line = self.next()[2]
366
+ names = []
367
+ while True:
368
+ names.append(self.expect("id")[1])
369
+ self.accept("kw", "export")
370
+ if not self.accept("op", ","):
371
+ break
372
+ return ("var", line, names)
373
+
374
+ def if_statement(self):
375
+ line = self.next()[2]
376
+ branches = []
377
+ cond = self.expression()
378
+ self.expect("kw", "then")
379
+ branches.append((cond, self.block()))
380
+ other = None
381
+ while True:
382
+ if self.accept("kw", "elsif"):
383
+ cond = self.expression()
384
+ self.expect("kw", "then")
385
+ branches.append((cond, self.block()))
386
+ continue
387
+ if self.accept("kw", "else"):
388
+ other = self.block()
389
+ self.expect("kw", "endif")
390
+ return ("if", line, branches, other)
391
+
392
+ def for_statement(self):
393
+ line = self.next()[2]
394
+ if self.accept("kw", "each"):
395
+ var = self.expect("id")[1]
396
+ self.expect("kw", "in")
397
+ collection = self.expression()
398
+ self.expect("kw", "do")
399
+ body = self.block()
400
+ self.expect("kw", "enddo")
401
+ return ("foreach", line, var, collection, body)
402
+ var = self.expect("id")[1]
403
+ self.expect("op", "=")
404
+ start = self.expression()
405
+ self.expect("kw", "to")
406
+ stop = self.expression()
407
+ self.expect("kw", "do")
408
+ body = self.block()
409
+ self.expect("kw", "enddo")
410
+ return ("for", line, var, start, stop, body)
411
+
412
+ # -- expressions --------------------------------------------------------------------
413
+
414
+ def expression(self):
415
+ left = self.and_expr()
416
+ while self.accept("kw", "or"):
417
+ left = ("bin", "or", left, self.and_expr())
418
+ return left
419
+
420
+ def and_expr(self):
421
+ left = self.not_expr()
422
+ while self.accept("kw", "and"):
423
+ left = ("bin", "and", left, self.not_expr())
424
+ return left
425
+
426
+ def not_expr(self):
427
+ if self.accept("kw", "not"):
428
+ return ("not", self.not_expr())
429
+ return self.comparison()
430
+
431
+ def comparison(self):
432
+ left = self.additive()
433
+ while True:
434
+ tok = self.peek()
435
+ if tok[0] == "op" and tok[1] in ("=", "<>", "<", ">", "<=", ">="):
436
+ self.next()
437
+ left = ("bin", tok[1], left, self.additive())
438
+ else:
439
+ return left
440
+
441
+ def additive(self):
442
+ left = self.multiplicative()
443
+ while True:
444
+ tok = self.peek()
445
+ if tok[0] == "op" and tok[1] in ("+", "-"):
446
+ self.next()
447
+ left = ("bin", tok[1], left, self.multiplicative())
448
+ else:
449
+ return left
450
+
451
+ def multiplicative(self):
452
+ left = self.unary()
453
+ while True:
454
+ tok = self.peek()
455
+ if tok[0] == "op" and tok[1] in ("*", "/", "%"):
456
+ self.next()
457
+ left = ("bin", tok[1], left, self.unary())
458
+ else:
459
+ return left
460
+
461
+ def unary(self):
462
+ if self.accept("op", "-"):
463
+ return ("neg", self.unary())
464
+ if self.accept("op", "+"):
465
+ return self.unary()
466
+ return self.postfix()
467
+
468
+ def postfix(self):
469
+ expr = self.primary()
470
+ while True:
471
+ if self.accept("op", "."):
472
+ tok = self.next()
473
+ # a member may be spelled like a keyword: `Запрос.Выполнить`, `Стр.Новый`
474
+ if tok[0] not in ("id", "kw"):
475
+ raise ParseError(u"expected a member name", tok[2])
476
+ expr = ("member", expr, tok[1] if tok[0] == "id" else self._kw_text(tok))
477
+ elif self.accept("op", "["):
478
+ key = self.expression()
479
+ self.expect("op", "]")
480
+ expr = ("index", expr, key)
481
+ elif self.at("op", "("):
482
+ expr = ("callx", expr, self.arguments())
483
+ else:
484
+ return expr
485
+
486
+ def _kw_text(self, tok):
487
+ # the canonical keyword is English; a member name only needs to be stable
488
+ return tok[1]
489
+
490
+ def arguments(self):
491
+ self.expect("op", "(")
492
+ args = []
493
+ if self.accept("op", ")"):
494
+ return args
495
+ while True:
496
+ if self.at("op", ",") or self.at("op", ")"):
497
+ args.append(None)
498
+ else:
499
+ args.append(self.expression())
500
+ if self.accept("op", ","):
501
+ continue
502
+ self.expect("op", ")")
503
+ return args
504
+
505
+ def primary(self):
506
+ tok = self.next()
507
+ kind, value = tok[0], tok[1]
508
+ if kind == "id":
509
+ return ("name", value)
510
+ if kind == "num":
511
+ return ("lit", float(value) if "." in value else int(value))
512
+ if kind == "str":
513
+ # adjacent literals concatenate: "a" "b"
514
+ parts = [value]
515
+ while self.at("str"):
516
+ parts.append(self.next()[1])
517
+ return ("lit", u"".join(parts))
518
+ if kind == "date":
519
+ return ("lit", ("date", value))
520
+ if kind == "kw":
521
+ if value == "true":
522
+ return ("lit", True)
523
+ if value == "false":
524
+ return ("lit", False)
525
+ if value in ("undefined", "null"):
526
+ return ("lit", None)
527
+ if value == "new":
528
+ if self.at("id"):
529
+ type_name = self.next()[1]
530
+ args = self.arguments() if self.at("op", "(") else []
531
+ return ("new", type_name, args)
532
+ return ("new", None, self.arguments())
533
+ if kind == "op":
534
+ if value == "(":
535
+ expr = self.expression()
536
+ self.expect("op", ")")
537
+ return expr
538
+ if value == "?":
539
+ args = self.arguments()
540
+ if len(args) != 3:
541
+ raise ParseError(u"?() takes three arguments", tok[2])
542
+ return ("tern", args[0], args[1], args[2])
543
+ raise ParseError(u"unexpected %s %r" % (kind, value), tok[2])
544
+
545
+
546
+ # -- preprocessor -----------------------------------------------------------------------
547
+
548
+ # Where posting runs. An ordinary-application configuration posts on the server in
549
+ # client-server mode, so that is the reading a requirement is taken from.
550
+ SERVER = frozenset([u"сервер", u"server", u"насервере", u"atserver",
551
+ u"внешнеесоединение", u"externalconnection"])
552
+
553
+ _PRE_LINE = re.compile(u"^[ \\t]*#[ \\t]*([^\\W\\d]+)(.*)$", re.U)
554
+
555
+
556
+ def _pre_condition(text, defined):
557
+ """Evaluate `Сервер И НЕ Клиент` and the like against the defined symbols."""
558
+ words = re.findall(u"[^\\W\\d]+|\\(|\\)", text, re.U)
559
+ pos = [0]
560
+
561
+ def atom():
562
+ word = words[pos[0]] if pos[0] < len(words) else u""
563
+ pos[0] += 1
564
+ key = norm(word)
565
+ if key in (u"не", u"not"):
566
+ return not atom()
567
+ if word == u"(":
568
+ value = disj()
569
+ pos[0] += 1
570
+ return value
571
+ return key in defined
572
+
573
+ def conj():
574
+ value = atom()
575
+ while pos[0] < len(words) and norm(words[pos[0]]) in (u"и", u"and"):
576
+ pos[0] += 1
577
+ value = atom() and value
578
+ return value
579
+
580
+ def disj():
581
+ value = conj()
582
+ while pos[0] < len(words) and norm(words[pos[0]]) in (u"или", u"or"):
583
+ pos[0] += 1
584
+ value = conj() or value
585
+ return value
586
+
587
+ words = [w for w in words if norm(w) not in (u"тогда", u"then")]
588
+ return disj()
589
+
590
+
591
+ def preprocess(text, defined=SERVER):
592
+ """The text with every line the preprocessor excludes blanked, so line numbers hold.
593
+
594
+ `#Область` and other directives that do not choose code are blanked and their content
595
+ kept. A `#Если` whose branches split one statement - this configuration has them -
596
+ then parses as the one statement the platform would compile."""
597
+ lines = text.split("\n")
598
+ stack = [] # (this branch active, some branch already taken, parent active)
599
+ active = True
600
+ for index, line in enumerate(lines):
601
+ m = _PRE_LINE.match(line)
602
+ if not m:
603
+ if not active:
604
+ lines[index] = u""
605
+ continue
606
+ word, rest = norm(m.group(1)), m.group(2)
607
+ if word in (u"если", u"if"):
608
+ chosen = active and _pre_condition(rest, defined)
609
+ stack.append((chosen, chosen, active))
610
+ active = chosen
611
+ elif word in (u"иначеесли", u"elsif") and stack:
612
+ _, taken, parent = stack[-1]
613
+ chosen = parent and not taken and _pre_condition(rest, defined)
614
+ stack[-1] = (chosen, taken or chosen, parent)
615
+ active = chosen
616
+ elif word in (u"иначе", u"else") and stack:
617
+ _, taken, parent = stack[-1]
618
+ chosen = parent and not taken
619
+ stack[-1] = (chosen, True, parent)
620
+ active = chosen
621
+ elif word in (u"конецесли", u"endif") and stack:
622
+ active = stack.pop()[2]
623
+ lines[index] = u""
624
+ return "\n".join(lines)
625
+
626
+
627
+ def parse(text, defined=SERVER):
628
+ """(module tree, [ParseError]) - errors are per method; the rest still parses.
629
+
630
+ `defined` is the preprocessor context; None keeps every branch."""
631
+ if defined is not None:
632
+ text = preprocess(text, defined)
633
+ return Parser(tokenize(text)).module()
634
+
635
+
636
+ def parse_expression(text):
637
+ return Parser(tokenize(text)).expression()
638
+
639
+
640
+ # -- walking ----------------------------------------------------------------------------
641
+
642
+ def statements(body):
643
+ """Every statement in a body, depth first, nested blocks included."""
644
+ for st in body:
645
+ yield st
646
+ kind = st[0]
647
+ if kind == "if":
648
+ for _, inner in st[2]:
649
+ for x in statements(inner):
650
+ yield x
651
+ if st[3]:
652
+ for x in statements(st[3]):
653
+ yield x
654
+ elif kind == "while":
655
+ for x in statements(st[3]):
656
+ yield x
657
+ elif kind == "for":
658
+ for x in statements(st[5]):
659
+ yield x
660
+ elif kind == "foreach":
661
+ for x in statements(st[4]):
662
+ yield x
663
+ elif kind == "try":
664
+ for x in statements(st[2]):
665
+ yield x
666
+ for x in statements(st[3]):
667
+ yield x
668
+
669
+
670
+ def expressions(st):
671
+ """The expressions a statement holds directly (not those of nested statements)."""
672
+ kind = st[0]
673
+ if kind == "assign":
674
+ return [st[2], st[3]]
675
+ if kind in ("call", "execute"):
676
+ return [st[2]]
677
+ if kind == "handler":
678
+ return [st[3], st[4]]
679
+ if kind in ("return", "raise"):
680
+ return [st[2]] if st[2] is not None else []
681
+ if kind == "if":
682
+ return [cond for cond, _ in st[2]]
683
+ if kind == "while":
684
+ return [st[2]]
685
+ if kind == "for":
686
+ return [st[3], st[4]]
687
+ if kind == "foreach":
688
+ return [st[3]]
689
+ return []
690
+
691
+
692
+ def subexpressions(expr):
693
+ """An expression and everything inside it."""
694
+ stack = [expr]
695
+ while stack:
696
+ e = stack.pop()
697
+ if e is None:
698
+ continue
699
+ yield e
700
+ kind = e[0]
701
+ if kind == "member":
702
+ stack.append(e[1])
703
+ elif kind == "index":
704
+ stack.extend((e[1], e[2]))
705
+ elif kind == "callx":
706
+ stack.append(e[1])
707
+ stack.extend(e[2])
708
+ elif kind == "new":
709
+ stack.extend(e[2])
710
+ elif kind == "bin":
711
+ stack.extend((e[2], e[3]))
712
+ elif kind in ("not", "neg"):
713
+ stack.append(e[1])
714
+ elif kind == "tern":
715
+ stack.extend((e[1], e[2], e[3]))
716
+
717
+
718
+ def walk(body, context=()):
719
+ """(statement, context) for every statement; context is the enclosing constructs,
720
+ outermost first: ('if', condition, negated earlier conditions), ('foreach', var,
721
+ collection), ('for', var, start, stop), ('while', condition), ('try',), ('except',)."""
722
+ for st in body:
723
+ yield st, context
724
+ kind = st[0]
725
+ if kind == "if":
726
+ earlier = []
727
+ for cond, inner in st[2]:
728
+ for x in walk(inner, context + (("if", cond, tuple(earlier)),)):
729
+ yield x
730
+ earlier.append(cond)
731
+ if st[3]:
732
+ for x in walk(st[3], context + (("if", None, tuple(earlier)),)):
733
+ yield x
734
+ elif kind == "while":
735
+ for x in walk(st[3], context + (("while", st[2]),)):
736
+ yield x
737
+ elif kind == "for":
738
+ for x in walk(st[5], context + (("for", st[2], st[3], st[4]),)):
739
+ yield x
740
+ elif kind == "foreach":
741
+ for x in walk(st[4], context + (("foreach", st[2], st[3]),)):
742
+ yield x
743
+ elif kind == "try":
744
+ for x in walk(st[2], context + (("try",),)):
745
+ yield x
746
+ for x in walk(st[3], context + (("except",),)):
747
+ yield x
748
+
749
+
750
+ _PRECEDENCE = {"or": 1, "and": 2, "=": 4, "<>": 4, "<": 4, ">": 4, "<=": 4, ">=": 4,
751
+ "+": 5, "-": 5, "*": 6, "/": 6, "%": 6}
752
+ _WORDS = {"or": u"ИЛИ", "and": u"И"}
753
+
754
+
755
+ def render(expr, parent=0):
756
+ """An expression as compact text - for a behaviour spec a person reads, not for
757
+ running. Keywords come out in Russian, the way the configuration spells them."""
758
+ if expr is None:
759
+ return u""
760
+ kind = expr[0]
761
+ if kind == "name":
762
+ return expr[1]
763
+ if kind == "lit":
764
+ value = expr[1]
765
+ if value is True:
766
+ return u"Истина"
767
+ if value is False:
768
+ return u"Ложь"
769
+ if value is None:
770
+ return u"Неопределено"
771
+ if isinstance(value, tuple):
772
+ return u"'%s'" % value[1]
773
+ if isinstance(value, type(u"")):
774
+ text = value if len(value) <= 60 else value[:57] + u"..."
775
+ return u'"%s"' % text.replace(u'"', u'""').replace(u"\n", u" ")
776
+ return u"%s" % value
777
+ if kind == "member":
778
+ return u"%s.%s" % (render(expr[1], 9), expr[2])
779
+ if kind == "index":
780
+ return u"%s[%s]" % (render(expr[1], 9), render(expr[2]))
781
+ if kind == "callx":
782
+ return u"%s(%s)" % (render(expr[1], 9), u", ".join(render(a) for a in expr[2]))
783
+ if kind == "new":
784
+ args = u", ".join(render(a) for a in expr[2])
785
+ return u"Новый %s(%s)" % (expr[1] or u"", args) if expr[1] else u"Новый(%s)" % args
786
+ if kind == "not":
787
+ text = u"НЕ " + render(expr[1], 3)
788
+ return u"(%s)" % text if parent > 3 else text
789
+ if kind == "neg":
790
+ return u"-" + render(expr[1], 7)
791
+ if kind == "tern":
792
+ return u"?(%s, %s, %s)" % (render(expr[1]), render(expr[2]), render(expr[3]))
793
+ if kind == "bin":
794
+ op = expr[1]
795
+ mine = _PRECEDENCE[op]
796
+ text = u"%s %s %s" % (render(expr[2], mine), _WORDS.get(op, op), render(expr[3], mine + 1))
797
+ return u"(%s)" % text if mine < parent else text
798
+ return u"?"
799
+
800
+
801
+ def dotted(expr):
802
+ """`А.Б.В` as ['А', 'Б', 'В'], or None if the expression is not a plain path."""
803
+ parts = []
804
+ while expr[0] == "member":
805
+ parts.append(expr[2])
806
+ expr = expr[1]
807
+ if expr[0] != "name":
808
+ return None
809
+ parts.append(expr[1])
810
+ parts.reverse()
811
+ return parts