#!/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(""), 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())