#!/usr/bin/env python3 """Return module/file context for a 1C object with base/effective/extension views.""" from __future__ import annotations import argparse import base64 import json import sys from pathlib import Path from typing import Any sys.path.insert(0, str(Path(__file__).resolve().parent)) from resolve_1c_object import load_json, resolve_object # noqa: E402 MODULE_PATTERNS = { "Ext/ObjectModule.bsl": ("object_module", "МодульОбъекта"), "Ext/ManagerModule.bsl": ("manager_module", "МодульМенеджера"), } def decode_arg(value: str | None, encoded: str | None) -> str | None: if encoded: return base64.b64decode(encoded).decode("utf-8") return value def extension_name_from_path(path: str | None) -> str | None: parts = str(path or "").replace("/", "\\").split("\\") lowered = [part.casefold() for part in parts] for marker in ("расширения", "extensions"): if marker in lowered: index = lowered.index(marker) if index + 1 < len(parts): return parts[index + 1] return None def module_record(path: Path, *, owner_dir: Path, source: str, extension_name: str | None = None) -> dict[str, Any]: relative = str(path.relative_to(owner_dir)).replace("\\", "/") kind, name = MODULE_PATTERNS.get(relative, ("module", path.stem)) if relative.startswith("Forms/") and relative.endswith("/Ext/Form/Module.bsl"): form_name = relative.split("/", 2)[1] kind = "form_module" name = f"МодульФормы.{form_name}" elif relative.startswith("Commands/") and relative.endswith("/Ext/CommandModule.bsl"): command_name = relative.split("/", 2)[1] kind = "command_module" name = f"МодульКоманды.{command_name}" return { "name": name, "kind": kind, "relative_path": relative, "path": str(path), "size": path.stat().st_size, "origin": {"layer": source, **({"extension": extension_name} if extension_name else {})}, } def collect_modules(owner_xml_path: str | None, *, source: str, extension_name: str | None = None) -> list[dict[str, Any]]: if not owner_xml_path: return [] owner_xml = Path(owner_xml_path) owner_dir = owner_xml.with_suffix("") if not owner_dir.is_dir(): return [] result = [] for path in sorted(owner_dir.rglob("*.bsl")): result.append(module_record(path, owner_dir=owner_dir, source=source, extension_name=extension_name)) return result def merge_effective(base_modules: list[dict[str, Any]], extension_modules: list[dict[str, Any]]) -> list[dict[str, Any]]: result = [] by_key: dict[tuple[str, str], dict[str, Any]] = {} for module in base_modules: copy = dict(module) copy["effective_action"] = "base" by_key[(copy["kind"], copy["name"])] = copy result.append(copy) for module in extension_modules: key = (module["kind"], module["name"]) copy = dict(module) if key in by_key: copy["effective_action"] = "extended" by_key[key].setdefault("extension_overlays", []).append(copy) else: copy["effective_action"] = "added" result.append(copy) return result def filter_extension(modules: list[dict[str, Any]], extension: str) -> list[dict[str, Any]]: result = [] for module in modules: if (module.get("origin") or {}).get("extension") == extension: copy = dict(module) copy["effective_action"] = "extended_or_added" result.append(copy) return result def build_context(index: dict[str, Any], *, kind: str, name: str, view: str, extension: str | None) -> dict[str, Any]: resolution = resolve_object(index, kind=kind, name=name, limit=100) canonical = resolution.get("canonical") if not canonical: raise SystemExit(f"Object not found: {kind}.{name}") base_modules = collect_modules(canonical.get("path"), source="base") extension_modules = [] for overlay in resolution.get("extension_overlays") or []: overlay_extension = extension_name_from_path(overlay.get("path")) or overlay.get("extension_name") extension_modules.extend(collect_modules(overlay.get("path"), source="extension", extension_name=overlay_extension)) if view == "base": modules = [{**module, "effective_action": "base"} for module in base_modules] elif view == "effective": modules = merge_effective(base_modules, extension_modules) elif view == "extension": if not extension: raise SystemExit("Use --extension with --view extension.") modules = filter_extension(extension_modules, extension) else: raise SystemExit(f"Unsupported view: {view}") return { "schema": "onec_object_code_context.v1", "view": view, "extension": extension if view == "extension" else None, "object": { "kind": canonical.get("kind"), "name": canonical.get("name"), "synonym": canonical.get("synonym"), "uuid": canonical.get("guid"), }, "modules": modules, "resolution": { "schema": resolution.get("schema"), "canonical": canonical, "summary": resolution.get("summary"), }, "counts": { "modules": len(modules), "base_modules": len(base_modules), "extension_modules": len(extension_modules), }, } def main() -> int: parser = argparse.ArgumentParser(description="Get 1C object code context.") parser.add_argument("--index", type=Path, required=True) parser.add_argument("--kind") parser.add_argument("--name") parser.add_argument("--kind-b64") parser.add_argument("--name-b64") parser.add_argument("--view", choices=["effective", "base", "extension"], default="effective") parser.add_argument("--extension") parser.add_argument("--output", type=Path) args = parser.parse_args() kind = decode_arg(args.kind, args.kind_b64) name = decode_arg(args.name, args.name_b64) if not kind or not name: raise SystemExit("Use --kind/--name or --kind-b64/--name-b64.") result = build_context(load_json(args.index), kind=kind, name=name, view=args.view, extension=args.extension) 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), "counts": result["counts"], "view": result["view"]}, ensure_ascii=False)) else: print(text) return 0 if __name__ == "__main__": raise SystemExit(main())