Files
llm/scripts/resolve_1c_bsl_symbol.py
T

323 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())