323 lines
12 KiB
Python
323 lines
12 KiB
Python
#!/usr/bin/env python3
|
||
"""Resolve a BSL expression inside an explicit 1C code context.
|
||
|
||
The resolver is intentionally conservative: a dotted BSL expression is not a
|
||
metadata path unless the expression itself is a full 1C path or the current
|
||
module context proves the first segment, for example an object-module
|
||
attribute. Local variables and parameters win over metadata-name guesses.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import base64
|
||
import json
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||
from resolve_1c_fact import load_json, resolve_from_snapshot, split_fact_path # noqa: E402
|
||
from resolve_1c_object import canonical_kind, normalize # noqa: E402
|
||
|
||
|
||
KIND_ALIASES = {
|
||
"Справочник": "Catalog",
|
||
"Документ": "Document",
|
||
"Перечисление": "Enum",
|
||
"РегистрСведений": "InformationRegister",
|
||
"РегистрНакопления": "AccumulationRegister",
|
||
"РегистрБухгалтерии": "AccountingRegister",
|
||
"Отчет": "Report",
|
||
"Обработка": "DataProcessor",
|
||
"ОбщийМодуль": "CommonModule",
|
||
}
|
||
|
||
STANDARD_OBJECT_MEMBERS = {
|
||
"Catalog": {"Наименование", "Код", "ПометкаУдаления", "Ссылка"},
|
||
"Document": {"Дата", "Номер", "ПометкаУдаления", "Ссылка", "Проведен"},
|
||
}
|
||
|
||
|
||
def decode_arg(value: str | None, encoded: str | None) -> str | None:
|
||
if encoded:
|
||
return base64.b64decode(encoded).decode("utf-8")
|
||
return value
|
||
|
||
|
||
def path_join(*parts: Any) -> str:
|
||
return ".".join(str(part).strip() for part in parts if str(part or "").strip())
|
||
|
||
|
||
def split_expression(expression: str) -> list[str]:
|
||
return [part.strip() for part in str(expression or "").split(".") if part.strip()]
|
||
|
||
|
||
def same_name(left: Any, right: Any) -> bool:
|
||
return normalize(str(left or "")) == normalize(str(right or ""))
|
||
|
||
|
||
def find_object(snapshot: dict[str, Any], *, kind: str | None, name: str | None) -> dict[str, Any] | None:
|
||
if not name:
|
||
return None
|
||
wanted_kind = canonical_kind(kind) if kind else None
|
||
for item in snapshot.get("objects") or []:
|
||
actual_kind = canonical_kind(str(item.get("kind") or ""))
|
||
if wanted_kind and actual_kind != wanted_kind:
|
||
continue
|
||
if same_name(item.get("name"), name) or same_name(item.get("full_name"), name) or same_name(item.get("synonym"), name):
|
||
return item
|
||
return None
|
||
|
||
|
||
def find_module(modules: dict[str, Any], *, module_id: str | None, object_name: str | None, object_kind: str | None) -> dict[str, Any] | None:
|
||
items = modules.get("modules") or []
|
||
if module_id:
|
||
return next((item for item in items if str(item.get("module_id") or "") == module_id), None)
|
||
if not object_name:
|
||
return None
|
||
wanted_kind = canonical_kind(object_kind) if object_kind else None
|
||
for item in items:
|
||
actual_kind = canonical_kind(str(item.get("object_kind") or ""))
|
||
if wanted_kind and actual_kind != wanted_kind:
|
||
continue
|
||
if same_name(item.get("object_name"), object_name):
|
||
return item
|
||
return None
|
||
|
||
|
||
def routine_params(module: dict[str, Any], routine_name: str | None) -> set[str]:
|
||
if not routine_name:
|
||
routines = list(module.get("procedures") or []) + list(module.get("functions") or [])
|
||
else:
|
||
routines = [
|
||
item
|
||
for item in list(module.get("procedures") or []) + list(module.get("functions") or [])
|
||
if same_name(item.get("name"), routine_name)
|
||
]
|
||
result: set[str] = set()
|
||
for routine in routines:
|
||
for param in routine.get("params") or []:
|
||
text = str(param or "").strip()
|
||
if text:
|
||
result.add(text)
|
||
return result
|
||
|
||
|
||
def declared_symbols(text: str) -> set[str]:
|
||
symbols: set[str] = set()
|
||
for match in re.finditer(r"(?im)^\s*Перем\s+([^;\n]+)", text or ""):
|
||
for part in re.split(r",", match.group(1)):
|
||
name = re.sub(r"\s+Экспорт\b", "", part, flags=re.IGNORECASE).strip()
|
||
if name:
|
||
symbols.add(name)
|
||
for match in re.finditer(r"(?im)^\s*(?:Для\s+Каждого|Для каждого)\s+([A-Za-zА-Яа-я_][\wА-Яа-я]*)\s+Из\b", text or ""):
|
||
symbols.add(match.group(1))
|
||
for match in re.finditer(r"(?m)^\s*([A-Za-zА-Яа-я_][\wА-Яа-я]*)\s*=", text or ""):
|
||
symbols.add(match.group(1))
|
||
return symbols
|
||
|
||
|
||
def object_member_match(obj: dict[str, Any], name: str) -> tuple[str, dict[str, Any]] | None:
|
||
for area, items in (
|
||
("attribute", obj.get("attributes") or []),
|
||
("form", obj.get("forms") or []),
|
||
("command", obj.get("commands") or []),
|
||
("module", obj.get("modules") or []),
|
||
):
|
||
match = next((item for item in items if same_name(item.get("name"), name) or same_name(item.get("synonym"), name)), None)
|
||
if match:
|
||
return area, match
|
||
canonical = canonical_kind(str(obj.get("kind") or ""))
|
||
standard = next((item for item in STANDARD_OBJECT_MEMBERS.get(canonical or "", set()) if same_name(item, name)), None)
|
||
if standard:
|
||
return "standard_attribute", {"name": standard, "standard": True}
|
||
return None
|
||
|
||
|
||
def full_metadata_path_resolution(snapshot: dict[str, Any], expression: str) -> dict[str, Any] | None:
|
||
parts = split_expression(expression)
|
||
if len(parts) < 2:
|
||
return None
|
||
if canonical_kind(parts[0]) is None and parts[0] not in KIND_ALIASES:
|
||
return None
|
||
kind, object_name, section, member = split_fact_path(expression)
|
||
result = resolve_from_snapshot(
|
||
snapshot,
|
||
snapshot_path=Path("<memory>"),
|
||
kind=kind,
|
||
object_name=object_name,
|
||
member=member,
|
||
area="any",
|
||
table_section=section,
|
||
view="effective",
|
||
extension=None,
|
||
)
|
||
if result.get("exists"):
|
||
return result
|
||
return None
|
||
|
||
|
||
def resolve_symbol(
|
||
metadata: dict[str, Any],
|
||
modules: dict[str, Any],
|
||
*,
|
||
expression: str,
|
||
module_id: str | None = None,
|
||
object_kind: str | None = None,
|
||
object_name: str | None = None,
|
||
routine_name: str | None = None,
|
||
) -> dict[str, Any]:
|
||
parts = split_expression(expression)
|
||
query = {
|
||
"expression": expression,
|
||
"module_id": module_id,
|
||
"object_kind": canonical_kind(object_kind) if object_kind else None,
|
||
"object_name": object_name,
|
||
"routine_name": routine_name,
|
||
}
|
||
result: dict[str, Any] = {
|
||
"schema": "onec_bsl_symbol_resolution.v1",
|
||
"query": {key: value for key, value in query.items() if value not in (None, "")},
|
||
"status": "unresolved",
|
||
"path_kind": "code_symbol",
|
||
"segments": parts,
|
||
}
|
||
if not parts:
|
||
result.update({"status": "error", "reason": "empty_expression"})
|
||
return result
|
||
|
||
full_path = full_metadata_path_resolution(metadata, expression)
|
||
if full_path:
|
||
result.update(
|
||
{
|
||
"status": "resolved",
|
||
"resolution_kind": "metadata_path",
|
||
"path_kind": (full_path.get("match") or {}).get("path_kind") or "metadata_path",
|
||
"canonical_path": (full_path.get("match") or {}).get("canonical_path"),
|
||
"area": full_path.get("area"),
|
||
"match": full_path.get("match"),
|
||
"safe_as_metadata_path": True,
|
||
}
|
||
)
|
||
return result
|
||
|
||
module = find_module(modules, module_id=module_id, object_name=object_name, object_kind=object_kind)
|
||
if module:
|
||
result["module"] = {
|
||
key: module.get(key)
|
||
for key in ("module_id", "object_kind", "object_name", "module_type")
|
||
if module.get(key) not in (None, "")
|
||
}
|
||
params = routine_params(module, routine_name)
|
||
first = parts[0]
|
||
param = next((item for item in params if same_name(item, first)), None)
|
||
if param:
|
||
result.update(
|
||
{
|
||
"status": "resolved",
|
||
"resolution_kind": "parameter",
|
||
"symbol": param,
|
||
"context_path": path_join(param, *parts[1:]) if len(parts) > 1 else param,
|
||
"safe_as_metadata_path": False,
|
||
}
|
||
)
|
||
return result
|
||
local = next((item for item in declared_symbols(str(module.get("content") or "")) if same_name(item, first)), None)
|
||
if local:
|
||
result.update(
|
||
{
|
||
"status": "resolved",
|
||
"resolution_kind": "local_variable",
|
||
"symbol": local,
|
||
"context_path": path_join(local, *parts[1:]) if len(parts) > 1 else local,
|
||
"safe_as_metadata_path": False,
|
||
}
|
||
)
|
||
return result
|
||
|
||
obj = find_object(metadata, kind=object_kind, name=object_name)
|
||
if obj:
|
||
result["object"] = {
|
||
"kind": obj.get("kind"),
|
||
"name": obj.get("name"),
|
||
"canonical_path": obj.get("full_name") or path_join(object_kind, object_name),
|
||
}
|
||
matched = object_member_match(obj, parts[0])
|
||
if matched:
|
||
area, member = matched
|
||
canonical_path = path_join(result["object"]["canonical_path"], member.get("name"), *parts[1:])
|
||
result.update(
|
||
{
|
||
"status": "resolved",
|
||
"resolution_kind": "context_metadata_member",
|
||
"path_kind": "metadata_member",
|
||
"area": area,
|
||
"canonical_path": canonical_path,
|
||
"context_path": path_join(member.get("name"), *parts[1:]),
|
||
"match": {"area": area, "name": member.get("name"), "synonym": member.get("synonym")},
|
||
}
|
||
)
|
||
return result
|
||
|
||
candidates = []
|
||
for item in metadata.get("objects") or []:
|
||
if same_name(item.get("name"), parts[0]) or same_name(item.get("synonym"), parts[0]):
|
||
candidates.append(
|
||
{
|
||
"canonical_path": item.get("full_name"),
|
||
"kind": item.get("kind"),
|
||
"name": item.get("name"),
|
||
"reason": "short_object_name_requires_kind",
|
||
}
|
||
)
|
||
result.update(
|
||
{
|
||
"status": "unresolved",
|
||
"reason": "not_a_confirmed_metadata_path_or_local_symbol",
|
||
"safe_as_metadata_path": False,
|
||
"candidates": candidates[:10],
|
||
}
|
||
)
|
||
return result
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description="Resolve a BSL expression inside a concrete 1C module/form context.")
|
||
parser.add_argument("--metadata", type=Path, required=True, help="Metadata snapshot for current configuration.")
|
||
parser.add_argument("--modules", type=Path, required=True, help="BSL module snapshot for current configuration.")
|
||
parser.add_argument("--expression")
|
||
parser.add_argument("--expression-b64")
|
||
parser.add_argument("--module-id")
|
||
parser.add_argument("--object-kind")
|
||
parser.add_argument("--object-name")
|
||
parser.add_argument("--routine-name")
|
||
parser.add_argument("--output", type=Path)
|
||
args = parser.parse_args()
|
||
|
||
expression = decode_arg(args.expression, args.expression_b64)
|
||
if not expression:
|
||
raise SystemExit("Use --expression or --expression-b64.")
|
||
result = resolve_symbol(
|
||
load_json(args.metadata),
|
||
load_json(args.modules),
|
||
expression=expression,
|
||
module_id=args.module_id,
|
||
object_kind=args.object_kind,
|
||
object_name=args.object_name,
|
||
routine_name=args.routine_name,
|
||
)
|
||
text = json.dumps(result, ensure_ascii=False, indent=2) + "\n"
|
||
if args.output:
|
||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||
args.output.write_text(text, encoding="utf-8")
|
||
print(json.dumps({"output": str(args.output), "status": result.get("status")}, ensure_ascii=False))
|
||
else:
|
||
print(text)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|