Repository navigation
feat: מפת סימבולים ב-get_repo_file — ניווט בקבצים גדולים במקום ניחוש #3321
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
6b9b928
feat(mcp): outline=true returns a symbol map for navigating large files
claude a1d04cb
fix(mcp): make a lost outline symbol unrepresentable instead of detec…
claude 57904e1
fix(mcp): type _start_line's node param precisely for mypy
github-actions[bot] 607a86f
fix(mcp): measure the outline page in bytes and stop truncating names
claude 806529b
Merge remote-tracking branch 'origin/claude/posthog-mcp-analytics-set…
claude 87e3e3e
refactor(mcp): read decorator_list directly now that the type guarant…
claude de7562b
fix(mcp): remove dead _bounded helper that referenced a deleted constant
github-actions[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| """חילוץ מפת סימבולים מקובץ פייתון, לניווט בקבצים גדולים. | ||
|
|
||
| הכלי מחזיר שם, שורת התחלה ושורת סיום לכל פונקציה ומחלקה, כדי שהקורא | ||
| יוכל להמשיך ל-``lines=[start, end]`` במקום לנחש חלון. ``webapp/app.py`` | ||
| הוא 20,035 שורות; בלי מפה, סוכן שמחפש בו פונקציה קורא ומנחש. | ||
|
|
||
| **ערוץ הכשל הוא ערך ההחזרה, לא חריגה.** הפונקציה מחזירה תמיד מילון עם | ||
| ``status``: ``"ok"`` או ``"no_outline"``. קורא שבודק רק אם נזרקה חריגה | ||
| יקבל "אין אאוטליין" בשקט ויחשוב שהקובץ ריק — זהו דפוס K11 ב- | ||
| ``amir-bug-patterns``, ולכן הבחירה מוצהרת כאן ולא משתמעת. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import ast | ||
| from typing import Any | ||
|
|
||
| #: מרחב שמות בפייתון הוא פונקציה או מחלקה. ``if``/``try``/``with``/``for`` | ||
| #: **אינם** — הם משנים זרימה, לא שיוך. לכן המעבר חוצה אותם בלי להוסיף | ||
| #: תחילית, ופונקציה שהוגדרה בתוך ``except ImportError`` נשארת ברמה שלה. | ||
| _FUNCTION_NODES = (ast.FunctionDef, ast.AsyncFunctionDef) | ||
| _SCOPE_NODES = _FUNCTION_NODES + (ast.ClassDef,) | ||
|
|
||
| #: ``.pyi`` הוא פייתון תקין ש-``ast.parse`` מנתחת, והוא נפוץ הרבה יותר | ||
| #: מסיומת באותיות גדולות. הבדיקה מנורמלת, ולא השוואת מחרוזת אחת. | ||
| _PYTHON_SUFFIXES = frozenset({".py", ".pyi"}) | ||
|
|
||
|
|
||
| def extract_outline(text: str, path: str, symbol: str | None = None) -> dict[str, Any]: | ||
| """מפת הסימבולים של ``text``, או ``no_outline`` עם הסיבה. | ||
|
|
||
| ``symbol`` מסנן לפי תת-מחרוזת ב**שם המלא**, ללא תלות ברישיות. לכן | ||
| ``symbol="build_mcp"`` מחזיר גם את הפונקציה וגם את כל מה שמוגדר | ||
| בתוכה — "תן לי הכול תחת המרחב הזה". ``total`` סופר את ההתאמות אחרי | ||
| הסינון, כי עליו נשען העימוד. | ||
| """ | ||
| if not any(path.lower().endswith(suffix) for suffix in _PYTHON_SUFFIXES): | ||
| return {"status": "no_outline", "reason": "unsupported_language"} | ||
|
|
||
| # ההרחבה הגורפת עוטפת **רק** את הפרסינג, ובכוונה. הקלט הוא קובץ | ||
| # שמישהו אחר כתב, וסוג החריגה משתנה בין גרסאות פייתון: בייט אפס הוא | ||
| # ``SyntaxError`` ב-3.11 ו-``ValueError`` במקומות אחרים, ו-surrogate | ||
| # לא חוקי הוא ``UnicodeEncodeError``. מניית סוגים כאן היא בדיוק | ||
| # הגישה השברירית שהכלל נגד הרחבת except בא למנוע — הרחבה מנומקת | ||
| # עדיפה על רשימה שתתיישן בשקט. | ||
| # | ||
| # מעבר העץ שמתחת רץ **מחוץ** ל-``try``: באג שלנו חייב ליפול בקול ולא | ||
| # להתחפש ל"אין אאוטליין" על קובץ תקין לגמרי. | ||
| try: | ||
| tree = ast.parse(text) | ||
| except Exception as error: | ||
| return { | ||
| "status": "no_outline", | ||
| "reason": "parse_error", | ||
| "error_type": type(error).__name__, | ||
| "line": getattr(error, "lineno", None), | ||
| } | ||
|
|
||
| rows = _collect(tree, text.split("\n")) | ||
|
|
||
| # ממוין לפי שורת התחלה, ושובר-שוויון לפי שם. אין כאן מקרה של מעטר | ||
| # משותף — מעטר שייך לסימבול אחד — אבל שובר-שוויון קבוע הוא מה שהופך | ||
| # את גבול העמוד ליציב בין קריאה לקריאה. | ||
| rows.sort(key=lambda row: (row["start"], row["name"])) | ||
|
|
||
| if symbol: | ||
| # ``casefold`` ולא ``lower``: זה הפרימיטיב להשוואה חסרת-רישיות. | ||
| # ``lower`` מפספסת מיפויים של יותר מתו אחד, למשל ``straße`` מול | ||
| # ``STRASSE``. | ||
| needle = symbol.casefold() | ||
| rows = [row for row in rows if needle in row["name"].casefold()] | ||
|
|
||
| return {"status": "ok", "symbols": rows, "total": len(rows)} | ||
|
|
||
|
|
||
| def _start_line( | ||
| node: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef, lines: list[str] | ||
| ) -> int: | ||
| """שורת ההתחלה: המעטר הראשון, ולא ה-``def``. | ||
|
|
||
| ב-``webapp/app.py`` 203 מתוך 408 הפונקציות ברמה העליונה מעוטרות, וטווח | ||
| שהיה מתחיל ב-``def`` היה מחמיץ את ``@app.route(...)``. | ||
|
|
||
| ``decorator_list[i].lineno`` הוא שורת ה**ביטוי**, לא שורת ה-``@``. | ||
| בכתיב הרגיל הם זהים, אבל ב-``@(``ואז ירידת שורה הביטוי מתחיל שורה | ||
| מאוחר יותר — נמדד. לכן סריקה אחורה עד השורה שמתחילה ב-``@``. מעטר | ||
| קודם שייתפס בדרך שייך לאותו סימבול ממילא, ולכן הרחבה כזו נכונה. | ||
| """ | ||
| # ``decorator_list`` קיים בוודאות בשלושת הטיפוסים שבחתימה, ולכן גישה | ||
| # ישירה ולא ``getattr`` עם ברירת מחדל — הגנה שהטיפוס כבר נותן היא | ||
| # רעש שמסתיר את מה שבאמת יכול להיכשל. | ||
| decorators = node.decorator_list | ||
| start = min([node.lineno] + [d.lineno for d in decorators]) | ||
| if not decorators: | ||
| return start | ||
| # סריקה אחורה עד השורה שפותחת ב-``@``. בין ה-``@`` לבין הביטוי יכולות | ||
| # לשבת שורות ריקות והערות, ולכן הן מדולגות — אבל **רק** אם בסוף נוחתים | ||
| # על ``@`` אמיתי. אם לא, נשארים במקום: עדיף טווח מדויק-חלקית מטווח | ||
| # שבלע שורות של סימבול קודם. | ||
| index = start - 1 # 0-based | ||
| scan = index | ||
| while scan > 0: | ||
| previous = lines[scan - 1].strip() | ||
| if previous.startswith("@"): | ||
| scan -= 1 | ||
| index = scan | ||
| continue | ||
| if previous == "" or previous.startswith("#"): | ||
| scan -= 1 | ||
| continue | ||
| break | ||
| return index + 1 | ||
|
|
||
|
|
||
| def _collect(tree: ast.AST, lines: list[str]) -> list[dict[str, Any]]: | ||
| """מעבר על העץ עם מחסנית מפורשת, ולא ברקורסיה. | ||
|
|
||
| קינון עמוק לא יכול לייצר ``RecursionError`` בצד שלנו — וזה מה שמאפשר | ||
| ל-``except`` שלמעלה להישאר צר סביב הפרסינג בלבד, בלי פיתוי להרחיב | ||
| אותו כדי לבלוע נפילה של המעבר. | ||
| """ | ||
| rows: list[dict[str, Any]] = [] | ||
| stack: list[tuple[ast.AST, str]] = [(tree, "")] | ||
| while stack: | ||
| node, prefix = stack.pop() | ||
| for child in ast.iter_child_nodes(node): | ||
| if isinstance(child, _SCOPE_NODES): | ||
| name = f"{prefix}{child.name}" | ||
| rows.append( | ||
| { | ||
| "name": name, | ||
| # שורת ההתחלה היא של המעטר הראשון ולא של ה-``def``. | ||
| # ב-``webapp/app.py`` 203 מתוך 408 הפונקציות ברמה | ||
| # העליונה מעוטרות, ובלי זה טווח שנקרא לפי האאוטליין | ||
| # היה מתחיל **אחרי** ``@app.route(...)`` — כלומר | ||
| # מחמיץ את השורה שמזהה את הנתיב. | ||
| "start": _start_line(child, lines), | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
|
||
| "end": child.end_lineno or child.lineno, | ||
| } | ||
| ) | ||
| stack.append((child, f"{name}.")) | ||
| else: | ||
| stack.append((child, prefix)) | ||
| return rows | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.