Initial SQL-only 1C adapter baseline

This commit is contained in:
2026-07-22 03:03:47 +03:00
commit e2503b77e7
545 changed files with 184711 additions and 0 deletions
+322
View File
@@ -0,0 +1,322 @@
#!/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())