@dforge-core/dforge-mcp 0.2.45 → 0.2.47
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 +45 -0
- package/README.md +2 -1
- package/dist/server.js +71 -3
- package/package.json +3 -3
- package/resources/docs/dsl-reference.md +1 -1
- package/resources/schemas/data-views.schema.json +50 -0
- package/scripts/install-skills.mjs +1 -0
- package/skills/dforge-1c-import/SKILL.md +375 -0
- package/skills/dforge-1c-import/references/environment.md +98 -0
- package/skills/dforge-1c-import/references/loading.md +164 -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 +164 -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/changes.py +326 -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 +770 -0
- package/skills/dforge-1c-import/scripts/lib/deps.py +136 -0
- package/skills/dforge-1c-import/scripts/lib/docusage.py +106 -0
- package/skills/dforge-1c-import/scripts/lib/dump1c.py +408 -0
- package/skills/dforge-1c-import/scripts/lib/explore.py +55 -0
- package/skills/dforge-1c-import/scripts/lib/external.py +202 -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 +325 -0
- package/skills/dforge-1c-import/scripts/lib/migrate.py +1318 -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/onecd.py +269 -0
- package/skills/dforge-1c-import/scripts/lib/paths.py +111 -0
- package/skills/dforge-1c-import/scripts/lib/peek.py +68 -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 +91 -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 +493 -0
- package/skills/dforge-1c-import/scripts/lib/sizing.py +184 -0
- package/skills/dforge-1c-import/scripts/lib/source_file.py +227 -0
- package/skills/dforge-1c-import/scripts/lib/source_mssql.py +158 -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/store.py +126 -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/.env.example +24 -0
- package/skills/dforge-1c-import/scripts/project-template/contours.py +47 -0
- package/skills/dforge-1c-import/scripts/project-template/sources.py +24 -0
- package/skills/dforge-mcp-author/SKILL.md +5 -0
- package/skills/dforge-mcp-author/references/action-dsl.md +1 -1
- package/skills/dforge-mcp-author/references/data-views.md +38 -2
- package/skills/dforge-mcp-author/references/formulas.md +32 -1
- package/skills/dforge-mcp-author/references/print-templates.md +30 -5
- package/skills/dforge-mcp-author/references/queries.md +17 -2
- package/skills/dforge-mcp-author/references/registers.md +1 -0
- package/skills/dforge-mcp-author/references/reports.md +1 -1
- package/skills/dforge-mcp-author/references/validation-checklist.md +1 -0
|
@@ -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
|