diff --git a/scripts/commands/context.py b/scripts/commands/context.py index 3ed73ef7..a81ed958 100644 --- a/scripts/commands/context.py +++ b/scripts/commands/context.py @@ -1,18 +1,37 @@ -"""Context command — Get rich symbol context (code + callers + callees).""" +"""context command — DEPRECATED alias for ``query``. -import os -from typing import Any, Dict, List +The ``context `` command returned a subset of what +``query `` already provides (code + callers + callees + quality +metrics). It added no new information, so it was redundant (issue #99). + +``context`` still works for backward compatibility but prints a +deprecation warning to stderr and delegates to ``query``'s handler with +the same args. It will be removed in a future release — switch to +``codelens query``. +""" + +import sys -from context_engine import get_symbol_context -from complexity_engine import compute_complexity -from sideeffect_engine import analyze_side_effects -from smell_engine import detect_smells -from testmap_engine import map_test_coverage -from utils import is_file_path, deduplicate_callers, logger from commands import register_command +# Deprecation notice — printed once per invocation to stderr (NOT stdout, +# which is reserved for JSON/machine-readable output). Surfaced in both +# interactive and CI usage so users notice and migrate before the alias +# is removed. +_DEPRECATION_WARNING = ( + "DEPRECATED: codelens context is renamed to codelens query. " + "Use query instead.\n" +) + def add_args(parser): + """Register context (deprecated alias) arguments. + + Kept compatible with the legacy ``context`` interface so existing + scripts keep parsing. Flags that ``query`` does not understand + (``--context-lines``, ``--no-code``) are accepted but ignored — + output now comes from ``query``. + """ parser.add_argument("name", help="Symbol name") parser.add_argument("workspace", nargs="?", default=None, help="Path to workspace root (auto-detected if omitted)") @@ -24,154 +43,42 @@ def add_args(parser): def execute(args, workspace): - symbol = args.name - - # ─── File path lookup ───────────────────────────── - if is_file_path(symbol): - from registry import load_backend_registry - workspace_abs = os.path.abspath(workspace) - backend = load_backend_registry(workspace_abs) - # Try exact match first - matching_nodes = [n for n in backend.get("nodes", []) if n.get("file", "") == symbol] - - # If no exact match, try substring/partial match - if not matching_nodes: - # Match by end-of-path (e.g., "layout.tsx" matches "apps/web/app/layout.tsx") - matching_nodes = [n for n in backend.get("nodes", []) - if n.get("file", "").endswith(symbol) - or n.get("file", "").endswith('/' + symbol)] - - if matching_nodes: - # Group by file if multiple files match - file_groups = {} - for node in matching_nodes: - f = node.get("file", "") - if f not in file_groups: - file_groups[f] = [] - file_groups[f].append({ - "fn": node["fn"], - "line": node.get("line", 0), - "status": node.get("status", "active"), - "async": node.get("async", False), - "ref_count": node.get("ref_count", 0) - }) - - # If single file, return flat; if multiple, return grouped - if len(file_groups) == 1: - f, syms = list(file_groups.items())[0] - return { - "status": "ok", - "found": True, - "context": { - "type": "file", - "file": f, - "symbols": syms - } - } - else: - return { - "status": "ok", - "found": True, - "context": { - "type": "files", - "files": [{"file": f, "symbols": syms} for f, syms in file_groups.items()] - } - } - else: - return { - "status": "ok", - "found": False, - "symbol": symbol, - "context": None - } - - # ─── Normal symbol lookup ────────────────────────── - result = get_symbol_context( - args.name, workspace, - domain=args.domain, - context_lines=args.context_lines, - include_code=not args.no_code - ) - - # Deduplicate callers - if result.get("found") and result.get("context"): - result["context"]["callers"] = deduplicate_callers(result["context"].get("callers", [])) - - # Enrich with quality metrics — skip on large codebases (>10K nodes) to prevent timeout - _LARGE_CODEBASE_THRESHOLD = 10000 - is_large = False - try: - from registry import load_backend_registry - backend = load_backend_registry(workspace) - is_large = len(backend.get("nodes", [])) > _LARGE_CODEBASE_THRESHOLD - except Exception: - logger.debug("Could not determine codebase size, assuming not large", exc_info=True) - - if result.get("found") and result.get("context") and not is_large: - quality = {} - try: - comp = compute_complexity(workspace, function_name=args.name) - if comp.get("status") == "ok" and comp.get("result"): - fn_data = comp["result"] - if isinstance(fn_data, dict): - quality["complexity"] = fn_data.get("cyclomatic", "N/A") - quality["complexity_level"] = fn_data.get("complexity_level", "N/A") - elif isinstance(fn_data, list) and fn_data: - quality["complexity"] = fn_data[0].get("cyclomatic", "N/A") - quality["complexity_level"] = fn_data[0].get("complexity_level", "N/A") - except Exception: - logger.debug("Complexity analysis failed for %s", args.name, exc_info=True) - - try: - se = analyze_side_effects(workspace, function_name=args.name) - if se.get("status") == "ok": - analyses = se.get("analyses", []) - if analyses: - fn_se = analyses[0] - quality["side_effects"] = fn_se.get("classification", "unknown") != "pure" - quality["side_effect_types"] = [e.get("type") for e in fn_se.get("side_effects", [])] - else: - quality["side_effects"] = False - quality["side_effect_types"] = [] - except Exception: - logger.debug("Side-effect analysis failed for %s", args.name, exc_info=True) - - # Determine safety from existing data - defn = result["context"].get("definition") or {} - status = defn.get("status", "") - ref_count = defn.get("ref_count", 0) - - if status == "dead": - quality["safety"] = "safe_to_remove" - elif ref_count == 0: - quality["safety"] = "safe_to_modify" - elif ref_count <= 2: - quality["safety"] = "caution" - else: - quality["safety"] = "high_impact" - - # Check if in smell top_priority - try: - smells = detect_smells(workspace) - for s in smells.get("top_priority", []): - fn_name = s.get("fn", "") - if fn_name == args.name: - quality.setdefault("smells", []).append(s.get("category", "")) - except Exception: - logger.debug("Smell detection failed for %s", args.name, exc_info=True) - - # Test coverage hint - try: - tc = map_test_coverage(workspace, function_name=args.name) - if tc.get("status") == "ok": - coverage = tc.get("coverage", {}) - quality["test_coverage"] = "covered" if coverage.get("has_tests") else "untested" - except Exception: - logger.debug("Test coverage check failed for %s", args.name, exc_info=True) - - if quality: - result["context"]["quality"] = quality - return result - - -register_command("context", "Get rich symbol context (code + callers + callees)", add_args, execute) + """Execute the deprecated context command. + + Prints a deprecation warning to stderr, then delegates to + ``query``'s handler with the same args. + + Args: + args: Parsed argparse namespace (``name``, ``workspace``, ...). + workspace: Resolved workspace root path. + + Returns: + Dict with the query result. + """ + print(_DEPRECATION_WARNING, file=sys.stderr, end="") + + # Lazy import to avoid module-load-order coupling between command + # modules (commands/__init__.py auto-imports all of them in sorted + # order, and ``context`` sorts before ``query``). + from commands import query as query_cmd + + # Normalize args for query compatibility. ``query.execute`` reads + # ``args.file`` directly and treats ``args.domain`` of None as + # "search both domains". ``context`` allowed ``--domain auto`` + # (default) which query does not understand — map it to None. + # query uses getattr-with-default for ``all``/``limit``/``fuzzy`` so + # those are safe, but ``file`` must exist as an attribute. + if not hasattr(args, "file"): + args.file = None + if getattr(args, "domain", None) == "auto": + args.domain = None + + return query_cmd.execute(args, workspace) + + +register_command( + "context", + "DEPRECATED — use `query` instead", + add_args, + execute, +)