Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -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())
|
||||
Reference in New Issue
Block a user