shotgun-sh 0.2.29.dev2__py3-none-any.whl → 0.6.1.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of shotgun-sh might be problematic. Click here for more details.
- shotgun/agents/agent_manager.py +497 -30
- shotgun/agents/cancellation.py +103 -0
- shotgun/agents/common.py +90 -77
- shotgun/agents/config/README.md +0 -1
- shotgun/agents/config/manager.py +52 -8
- shotgun/agents/config/models.py +48 -45
- shotgun/agents/config/provider.py +44 -29
- shotgun/agents/conversation/history/file_content_deduplication.py +66 -43
- shotgun/agents/conversation/history/token_counting/base.py +51 -9
- shotgun/agents/export.py +12 -13
- shotgun/agents/file_read.py +176 -0
- shotgun/agents/messages.py +15 -3
- shotgun/agents/models.py +90 -2
- shotgun/agents/plan.py +12 -13
- shotgun/agents/research.py +13 -10
- shotgun/agents/router/__init__.py +47 -0
- shotgun/agents/router/models.py +384 -0
- shotgun/agents/router/router.py +185 -0
- shotgun/agents/router/tools/__init__.py +18 -0
- shotgun/agents/router/tools/delegation_tools.py +557 -0
- shotgun/agents/router/tools/plan_tools.py +403 -0
- shotgun/agents/runner.py +17 -2
- shotgun/agents/specify.py +12 -13
- shotgun/agents/tasks.py +12 -13
- shotgun/agents/tools/__init__.py +8 -0
- shotgun/agents/tools/codebase/directory_lister.py +27 -39
- shotgun/agents/tools/codebase/file_read.py +26 -35
- shotgun/agents/tools/codebase/query_graph.py +9 -0
- shotgun/agents/tools/codebase/retrieve_code.py +9 -0
- shotgun/agents/tools/file_management.py +81 -3
- shotgun/agents/tools/file_read_tools/__init__.py +7 -0
- shotgun/agents/tools/file_read_tools/multimodal_file_read.py +167 -0
- shotgun/agents/tools/markdown_tools/__init__.py +62 -0
- shotgun/agents/tools/markdown_tools/insert_section.py +148 -0
- shotgun/agents/tools/markdown_tools/models.py +86 -0
- shotgun/agents/tools/markdown_tools/remove_section.py +114 -0
- shotgun/agents/tools/markdown_tools/replace_section.py +119 -0
- shotgun/agents/tools/markdown_tools/utils.py +453 -0
- shotgun/agents/tools/registry.py +41 -0
- shotgun/agents/tools/web_search/__init__.py +1 -2
- shotgun/agents/tools/web_search/gemini.py +1 -3
- shotgun/agents/tools/web_search/openai.py +42 -23
- shotgun/attachments/__init__.py +41 -0
- shotgun/attachments/errors.py +60 -0
- shotgun/attachments/models.py +107 -0
- shotgun/attachments/parser.py +257 -0
- shotgun/attachments/processor.py +193 -0
- shotgun/cli/clear.py +2 -2
- shotgun/cli/codebase/commands.py +181 -65
- shotgun/cli/compact.py +2 -2
- shotgun/cli/context.py +2 -2
- shotgun/cli/run.py +90 -0
- shotgun/cli/spec/backup.py +2 -1
- shotgun/cli/spec/commands.py +2 -0
- shotgun/cli/spec/models.py +18 -0
- shotgun/cli/spec/pull_service.py +122 -68
- shotgun/codebase/__init__.py +2 -0
- shotgun/codebase/benchmarks/__init__.py +35 -0
- shotgun/codebase/benchmarks/benchmark_runner.py +309 -0
- shotgun/codebase/benchmarks/exporters.py +119 -0
- shotgun/codebase/benchmarks/formatters/__init__.py +49 -0
- shotgun/codebase/benchmarks/formatters/base.py +34 -0
- shotgun/codebase/benchmarks/formatters/json_formatter.py +106 -0
- shotgun/codebase/benchmarks/formatters/markdown.py +136 -0
- shotgun/codebase/benchmarks/models.py +129 -0
- shotgun/codebase/core/__init__.py +4 -0
- shotgun/codebase/core/call_resolution.py +91 -0
- shotgun/codebase/core/change_detector.py +11 -6
- shotgun/codebase/core/errors.py +159 -0
- shotgun/codebase/core/extractors/__init__.py +23 -0
- shotgun/codebase/core/extractors/base.py +138 -0
- shotgun/codebase/core/extractors/factory.py +63 -0
- shotgun/codebase/core/extractors/go/__init__.py +7 -0
- shotgun/codebase/core/extractors/go/extractor.py +122 -0
- shotgun/codebase/core/extractors/javascript/__init__.py +7 -0
- shotgun/codebase/core/extractors/javascript/extractor.py +132 -0
- shotgun/codebase/core/extractors/protocol.py +109 -0
- shotgun/codebase/core/extractors/python/__init__.py +7 -0
- shotgun/codebase/core/extractors/python/extractor.py +141 -0
- shotgun/codebase/core/extractors/rust/__init__.py +7 -0
- shotgun/codebase/core/extractors/rust/extractor.py +139 -0
- shotgun/codebase/core/extractors/types.py +15 -0
- shotgun/codebase/core/extractors/typescript/__init__.py +7 -0
- shotgun/codebase/core/extractors/typescript/extractor.py +92 -0
- shotgun/codebase/core/gitignore.py +252 -0
- shotgun/codebase/core/ingestor.py +644 -354
- shotgun/codebase/core/kuzu_compat.py +119 -0
- shotgun/codebase/core/language_config.py +239 -0
- shotgun/codebase/core/manager.py +256 -46
- shotgun/codebase/core/metrics_collector.py +310 -0
- shotgun/codebase/core/metrics_types.py +347 -0
- shotgun/codebase/core/parallel_executor.py +424 -0
- shotgun/codebase/core/work_distributor.py +254 -0
- shotgun/codebase/core/worker.py +768 -0
- shotgun/codebase/indexing_state.py +86 -0
- shotgun/codebase/models.py +94 -0
- shotgun/codebase/service.py +13 -0
- shotgun/exceptions.py +1 -1
- shotgun/main.py +2 -10
- shotgun/prompts/agents/export.j2 +2 -0
- shotgun/prompts/agents/file_read.j2 +48 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +20 -28
- shotgun/prompts/agents/partials/content_formatting.j2 +12 -33
- shotgun/prompts/agents/partials/interactive_mode.j2 +9 -32
- shotgun/prompts/agents/partials/router_delegation_mode.j2 +35 -0
- shotgun/prompts/agents/plan.j2 +43 -1
- shotgun/prompts/agents/research.j2 +75 -20
- shotgun/prompts/agents/router.j2 +713 -0
- shotgun/prompts/agents/specify.j2 +94 -4
- shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +14 -1
- shotgun/prompts/agents/state/system_state.j2 +24 -15
- shotgun/prompts/agents/tasks.j2 +77 -23
- shotgun/settings.py +44 -0
- shotgun/shotgun_web/shared_specs/upload_pipeline.py +38 -0
- shotgun/tui/app.py +90 -23
- shotgun/tui/commands/__init__.py +9 -1
- shotgun/tui/components/attachment_bar.py +87 -0
- shotgun/tui/components/mode_indicator.py +120 -25
- shotgun/tui/components/prompt_input.py +23 -28
- shotgun/tui/components/status_bar.py +5 -4
- shotgun/tui/dependencies.py +58 -8
- shotgun/tui/protocols.py +37 -0
- shotgun/tui/screens/chat/chat.tcss +24 -1
- shotgun/tui/screens/chat/chat_screen.py +1374 -211
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +8 -4
- shotgun/tui/screens/chat_screen/attachment_hint.py +40 -0
- shotgun/tui/screens/chat_screen/command_providers.py +0 -97
- shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
- shotgun/tui/screens/chat_screen/history/chat_history.py +49 -6
- shotgun/tui/screens/chat_screen/history/formatters.py +75 -15
- shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
- shotgun/tui/screens/chat_screen/history/user_question.py +25 -3
- shotgun/tui/screens/chat_screen/messages.py +219 -0
- shotgun/tui/screens/database_locked_dialog.py +219 -0
- shotgun/tui/screens/database_timeout_dialog.py +158 -0
- shotgun/tui/screens/kuzu_error_dialog.py +135 -0
- shotgun/tui/screens/model_picker.py +14 -9
- shotgun/tui/screens/models.py +11 -0
- shotgun/tui/screens/shotgun_auth.py +50 -0
- shotgun/tui/screens/spec_pull.py +2 -0
- shotgun/tui/state/processing_state.py +19 -0
- shotgun/tui/utils/mode_progress.py +20 -86
- shotgun/tui/widgets/__init__.py +2 -1
- shotgun/tui/widgets/approval_widget.py +152 -0
- shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
- shotgun/tui/widgets/plan_panel.py +129 -0
- shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
- shotgun/tui/widgets/widget_coordinator.py +18 -0
- shotgun/utils/file_system_utils.py +4 -1
- {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/METADATA +88 -34
- shotgun_sh-0.6.1.dev1.dist-info/RECORD +292 -0
- shotgun/cli/export.py +0 -81
- shotgun/cli/plan.py +0 -73
- shotgun/cli/research.py +0 -93
- shotgun/cli/specify.py +0 -70
- shotgun/cli/tasks.py +0 -78
- shotgun/tui/screens/onboarding.py +0 -580
- shotgun_sh-0.2.29.dev2.dist-info/RECORD +0 -229
- {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
"""Change detection for incremental graph updates."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import hashlib
|
|
4
6
|
import os
|
|
5
7
|
from enum import Enum
|
|
6
8
|
from pathlib import Path
|
|
7
|
-
from typing import Any, cast
|
|
9
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
8
10
|
|
|
9
11
|
import aiofiles
|
|
10
|
-
import kuzu
|
|
11
12
|
|
|
13
|
+
from shotgun.codebase.models import NodeLabel, RelationshipType
|
|
12
14
|
from shotgun.logging_config import get_logger
|
|
13
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
import real_ladybug as kuzu
|
|
18
|
+
|
|
14
19
|
logger = get_logger(__name__)
|
|
15
20
|
|
|
16
21
|
|
|
@@ -332,10 +337,10 @@ class ChangeDetector:
|
|
|
332
337
|
|
|
333
338
|
# Query each TRACKS relationship type
|
|
334
339
|
for node_type, rel_type in [
|
|
335
|
-
(
|
|
336
|
-
(
|
|
337
|
-
(
|
|
338
|
-
(
|
|
340
|
+
(NodeLabel.MODULE, RelationshipType.TRACKS_MODULE),
|
|
341
|
+
(NodeLabel.CLASS, RelationshipType.TRACKS_CLASS),
|
|
342
|
+
(NodeLabel.FUNCTION, RelationshipType.TRACKS_FUNCTION),
|
|
343
|
+
(NodeLabel.METHOD, RelationshipType.TRACKS_METHOD),
|
|
339
344
|
]:
|
|
340
345
|
try:
|
|
341
346
|
result = self.conn.execute(
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Error classification for Kuzu database operations.
|
|
2
|
+
|
|
3
|
+
This module provides error classification for Kuzu database errors,
|
|
4
|
+
allowing the application to distinguish between different failure modes
|
|
5
|
+
(lock contention, corruption, permissions, etc.) and handle each appropriately.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from enum import StrEnum
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class KuzuErrorType(StrEnum):
|
|
15
|
+
"""Classification of Kuzu database errors."""
|
|
16
|
+
|
|
17
|
+
LOCKED = "locked" # Another process has DB open
|
|
18
|
+
CORRUPTION = "corruption" # Database file is invalid/corrupted
|
|
19
|
+
PERMISSION = "permission" # Permission denied (transient)
|
|
20
|
+
MISSING = "missing" # File not found
|
|
21
|
+
SCHEMA = "schema" # Table doesn't exist (incomplete build)
|
|
22
|
+
TIMEOUT = "timeout" # Operation timed out
|
|
23
|
+
UNKNOWN = "unknown" # Unrecognized error
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def classify_kuzu_error(exception: Exception) -> KuzuErrorType:
|
|
27
|
+
"""Classify a Kuzu RuntimeError by its message pattern.
|
|
28
|
+
|
|
29
|
+
Note: Kuzu only throws generic RuntimeError exceptions with no error codes
|
|
30
|
+
or custom exception types. String matching on the error message is the only
|
|
31
|
+
way to distinguish between different failure modes.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
exception: The exception to classify
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
KuzuErrorType indicating the category of error
|
|
38
|
+
"""
|
|
39
|
+
error_str = str(exception)
|
|
40
|
+
|
|
41
|
+
# Lock contention - another process has the database open
|
|
42
|
+
if "Could not set lock" in error_str:
|
|
43
|
+
return KuzuErrorType.LOCKED
|
|
44
|
+
|
|
45
|
+
# True corruption - database file is invalid
|
|
46
|
+
if "Unable to open database" in error_str:
|
|
47
|
+
return KuzuErrorType.CORRUPTION
|
|
48
|
+
if "Reading past the end of the file" in error_str:
|
|
49
|
+
return KuzuErrorType.CORRUPTION
|
|
50
|
+
if "not a valid" in error_str.lower() and "database" in error_str.lower():
|
|
51
|
+
return KuzuErrorType.CORRUPTION
|
|
52
|
+
|
|
53
|
+
# C++ internal errors - likely corruption
|
|
54
|
+
if "unordered_map" in error_str:
|
|
55
|
+
return KuzuErrorType.CORRUPTION
|
|
56
|
+
if "key not found" in error_str.lower():
|
|
57
|
+
return KuzuErrorType.CORRUPTION
|
|
58
|
+
if "std::exception" in error_str:
|
|
59
|
+
return KuzuErrorType.CORRUPTION
|
|
60
|
+
|
|
61
|
+
# Permission errors - transient, may resolve on retry
|
|
62
|
+
if "Permission denied" in error_str:
|
|
63
|
+
return KuzuErrorType.PERMISSION
|
|
64
|
+
|
|
65
|
+
# Missing file - nothing to delete
|
|
66
|
+
if "No such file or directory" in error_str:
|
|
67
|
+
return KuzuErrorType.MISSING
|
|
68
|
+
|
|
69
|
+
# Schema errors - incomplete build, table doesn't exist
|
|
70
|
+
if "Table" in error_str and "does not exist" in error_str:
|
|
71
|
+
return KuzuErrorType.SCHEMA
|
|
72
|
+
if "Binder exception" in error_str and "does not exist" in error_str:
|
|
73
|
+
return KuzuErrorType.SCHEMA
|
|
74
|
+
|
|
75
|
+
return KuzuErrorType.UNKNOWN
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class DatabaseIssue(BaseModel):
|
|
79
|
+
"""Structured information about a database issue.
|
|
80
|
+
|
|
81
|
+
Attributes:
|
|
82
|
+
graph_id: The ID of the affected graph
|
|
83
|
+
graph_path: Path to the database file
|
|
84
|
+
error_type: Classification of the error
|
|
85
|
+
message: Human-readable error message
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
89
|
+
|
|
90
|
+
graph_id: str
|
|
91
|
+
graph_path: Path
|
|
92
|
+
error_type: KuzuErrorType
|
|
93
|
+
message: str
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class KuzuDatabaseError(Exception):
|
|
97
|
+
"""Base exception for Kuzu database errors with classification."""
|
|
98
|
+
|
|
99
|
+
def __init__(
|
|
100
|
+
self, message: str, graph_id: str, graph_path: str, error_type: KuzuErrorType
|
|
101
|
+
) -> None:
|
|
102
|
+
super().__init__(message)
|
|
103
|
+
self.graph_id = graph_id
|
|
104
|
+
self.graph_path = graph_path
|
|
105
|
+
self.error_type = error_type
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class DatabaseLockedError(KuzuDatabaseError):
|
|
109
|
+
"""Raised when the database is locked by another process."""
|
|
110
|
+
|
|
111
|
+
def __init__(self, graph_id: str, graph_path: str) -> None:
|
|
112
|
+
super().__init__(
|
|
113
|
+
f"Database '{graph_id}' is locked by another process. "
|
|
114
|
+
"Only one shotgun instance can access a codebase at a time.",
|
|
115
|
+
graph_id=graph_id,
|
|
116
|
+
graph_path=graph_path,
|
|
117
|
+
error_type=KuzuErrorType.LOCKED,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class DatabaseCorruptedError(KuzuDatabaseError):
|
|
122
|
+
"""Raised when the database is corrupted."""
|
|
123
|
+
|
|
124
|
+
def __init__(self, graph_id: str, graph_path: str, details: str = "") -> None:
|
|
125
|
+
message = f"Database '{graph_id}' is corrupted"
|
|
126
|
+
if details:
|
|
127
|
+
message += f": {details}"
|
|
128
|
+
super().__init__(
|
|
129
|
+
message,
|
|
130
|
+
graph_id=graph_id,
|
|
131
|
+
graph_path=graph_path,
|
|
132
|
+
error_type=KuzuErrorType.CORRUPTION,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class DatabaseSchemaError(KuzuDatabaseError):
|
|
137
|
+
"""Raised when the database schema is incomplete (interrupted build)."""
|
|
138
|
+
|
|
139
|
+
def __init__(self, graph_id: str, graph_path: str) -> None:
|
|
140
|
+
super().__init__(
|
|
141
|
+
f"Database '{graph_id}' has incomplete schema (build was interrupted)",
|
|
142
|
+
graph_id=graph_id,
|
|
143
|
+
graph_path=graph_path,
|
|
144
|
+
error_type=KuzuErrorType.SCHEMA,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class DatabaseTimeoutError(KuzuDatabaseError):
|
|
149
|
+
"""Raised when database operation times out."""
|
|
150
|
+
|
|
151
|
+
def __init__(self, graph_id: str, graph_path: str, timeout_seconds: float) -> None:
|
|
152
|
+
super().__init__(
|
|
153
|
+
f"Database '{graph_id}' operation timed out after {timeout_seconds}s. "
|
|
154
|
+
"This can happen with large codebases.",
|
|
155
|
+
graph_id=graph_id,
|
|
156
|
+
graph_path=graph_path,
|
|
157
|
+
error_type=KuzuErrorType.TIMEOUT,
|
|
158
|
+
)
|
|
159
|
+
self.timeout_seconds = timeout_seconds
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Language-specific AST extraction framework.
|
|
2
|
+
|
|
3
|
+
This module provides a Protocol-based architecture for extracting
|
|
4
|
+
definitions, relationships, and metadata from source code ASTs.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from shotgun.codebase.core.extractors import get_extractor, SupportedLanguage
|
|
8
|
+
|
|
9
|
+
extractor = get_extractor(SupportedLanguage.PYTHON)
|
|
10
|
+
decorators = extractor.extract_decorators(node)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from .factory import get_extractor
|
|
16
|
+
from .protocol import LanguageExtractor
|
|
17
|
+
from .types import SupportedLanguage
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"SupportedLanguage",
|
|
21
|
+
"LanguageExtractor",
|
|
22
|
+
"get_extractor",
|
|
23
|
+
]
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Base extractor with shared extraction logic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from tree_sitter import Node
|
|
10
|
+
|
|
11
|
+
from .types import SupportedLanguage
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseExtractor(ABC):
|
|
15
|
+
"""Abstract base class for language extractors.
|
|
16
|
+
|
|
17
|
+
Provides shared extraction logic that works across languages,
|
|
18
|
+
with abstract methods for language-specific operations.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def language(self) -> SupportedLanguage:
|
|
24
|
+
"""The language this extractor handles."""
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def extract_decorators(self, node: Node) -> list[str]:
|
|
29
|
+
"""Extract decorators/attributes from a function or class node."""
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def extract_docstring(self, node: Node) -> str | None:
|
|
34
|
+
"""Extract documentation string from a function or class node."""
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def extract_inheritance(self, class_node: Node) -> list[str]:
|
|
39
|
+
"""Extract parent class names from a class definition."""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def parse_call_node(self, call_node: Node) -> tuple[str | None, str | None]:
|
|
44
|
+
"""Parse a call expression node to extract callee information."""
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def _class_definition_types(self) -> list[str]:
|
|
49
|
+
"""Return node types that represent class definitions."""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def _function_definition_types(self) -> list[str]:
|
|
54
|
+
"""Return node types that represent function definitions."""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
def find_parent_class(self, func_node: Node, module_qn: str) -> str | None:
|
|
58
|
+
"""Find the parent class of a function node.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
func_node: The function definition AST node
|
|
62
|
+
module_qn: Module qualified name
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Qualified name of parent class, or None if not in a class
|
|
66
|
+
"""
|
|
67
|
+
current = func_node.parent
|
|
68
|
+
|
|
69
|
+
while current:
|
|
70
|
+
if current.type in self._class_definition_types():
|
|
71
|
+
for child in current.children:
|
|
72
|
+
if child.type == "identifier" and child.text:
|
|
73
|
+
class_name = child.text.decode("utf-8")
|
|
74
|
+
return f"{module_qn}.{class_name}"
|
|
75
|
+
|
|
76
|
+
current = current.parent
|
|
77
|
+
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
def find_containing_function(self, node: Node, module_qn: str) -> str | None:
|
|
81
|
+
"""Find the containing function/method of a node.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
node: The AST node
|
|
85
|
+
module_qn: Module qualified name
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Qualified name of containing function, or None
|
|
89
|
+
"""
|
|
90
|
+
current = node.parent
|
|
91
|
+
|
|
92
|
+
while current:
|
|
93
|
+
if current.type in self._function_definition_types():
|
|
94
|
+
for child in current.children:
|
|
95
|
+
if child.type == "identifier" and child.text:
|
|
96
|
+
func_name = child.text.decode("utf-8")
|
|
97
|
+
|
|
98
|
+
parent_class = self.find_parent_class(current, module_qn)
|
|
99
|
+
if parent_class:
|
|
100
|
+
return f"{parent_class}.{func_name}"
|
|
101
|
+
else:
|
|
102
|
+
return f"{module_qn}.{func_name}"
|
|
103
|
+
|
|
104
|
+
current = current.parent
|
|
105
|
+
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
def count_ast_nodes(self, node: Node) -> int:
|
|
109
|
+
"""Count total AST nodes for metrics.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
node: Root AST node
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Total node count
|
|
116
|
+
"""
|
|
117
|
+
count = 1
|
|
118
|
+
for child in node.children:
|
|
119
|
+
count += self.count_ast_nodes(child)
|
|
120
|
+
return count
|
|
121
|
+
|
|
122
|
+
def _extract_full_name(self, node: Node, parts: list[str]) -> None:
|
|
123
|
+
"""Recursively extract full qualified name from attribute access.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
node: The AST node
|
|
127
|
+
parts: List to accumulate name parts (modified in place)
|
|
128
|
+
"""
|
|
129
|
+
if node.type == "identifier" and node.text:
|
|
130
|
+
parts.insert(0, node.text.decode("utf-8"))
|
|
131
|
+
elif node.type == "attribute":
|
|
132
|
+
attr_node = node.child_by_field_name("attribute")
|
|
133
|
+
if attr_node and attr_node.text:
|
|
134
|
+
parts.insert(0, attr_node.text.decode("utf-8"))
|
|
135
|
+
|
|
136
|
+
obj_node = node.child_by_field_name("object")
|
|
137
|
+
if obj_node:
|
|
138
|
+
self._extract_full_name(obj_node, parts)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Factory for creating language-specific extractors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .protocol import LanguageExtractor
|
|
6
|
+
from .types import SupportedLanguage
|
|
7
|
+
|
|
8
|
+
# Cache of extractor instances (created lazily)
|
|
9
|
+
_extractors: dict[SupportedLanguage, LanguageExtractor] = {}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_extractor(language: SupportedLanguage | str) -> LanguageExtractor:
|
|
13
|
+
"""Get the extractor for a language.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
language: The language as enum or string
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
The language extractor instance
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
ValueError: If language is not supported
|
|
23
|
+
"""
|
|
24
|
+
if isinstance(language, str):
|
|
25
|
+
language = SupportedLanguage(language)
|
|
26
|
+
|
|
27
|
+
if language not in _extractors:
|
|
28
|
+
_extractors[language] = _create_extractor(language)
|
|
29
|
+
|
|
30
|
+
return _extractors[language]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _create_extractor(language: SupportedLanguage) -> LanguageExtractor:
|
|
34
|
+
"""Create a new extractor instance for the language.
|
|
35
|
+
|
|
36
|
+
Uses lazy imports to avoid loading all extractors at once.
|
|
37
|
+
"""
|
|
38
|
+
match language:
|
|
39
|
+
case SupportedLanguage.PYTHON:
|
|
40
|
+
from .python.extractor import PythonExtractor
|
|
41
|
+
|
|
42
|
+
return PythonExtractor()
|
|
43
|
+
case SupportedLanguage.JAVASCRIPT:
|
|
44
|
+
from .javascript.extractor import JavaScriptExtractor
|
|
45
|
+
|
|
46
|
+
return JavaScriptExtractor()
|
|
47
|
+
case SupportedLanguage.TYPESCRIPT:
|
|
48
|
+
from .typescript.extractor import TypeScriptExtractor
|
|
49
|
+
|
|
50
|
+
return TypeScriptExtractor()
|
|
51
|
+
case SupportedLanguage.GO:
|
|
52
|
+
from .go.extractor import GoExtractor
|
|
53
|
+
|
|
54
|
+
return GoExtractor()
|
|
55
|
+
case SupportedLanguage.RUST:
|
|
56
|
+
from .rust.extractor import RustExtractor
|
|
57
|
+
|
|
58
|
+
return RustExtractor()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def clear_extractor_cache() -> None:
|
|
62
|
+
"""Clear the extractor cache (useful for testing)."""
|
|
63
|
+
_extractors.clear()
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Go language extractor implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from shotgun.codebase.core.extractors.base import BaseExtractor
|
|
8
|
+
from shotgun.codebase.core.extractors.types import SupportedLanguage
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from tree_sitter import Node
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GoExtractor(BaseExtractor):
|
|
15
|
+
"""Extractor for Go source code."""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def language(self) -> SupportedLanguage:
|
|
19
|
+
"""The language this extractor handles."""
|
|
20
|
+
return SupportedLanguage.GO
|
|
21
|
+
|
|
22
|
+
def _class_definition_types(self) -> list[str]:
|
|
23
|
+
"""Return node types that represent type definitions.
|
|
24
|
+
|
|
25
|
+
Go doesn't have classes but has type definitions and interfaces.
|
|
26
|
+
"""
|
|
27
|
+
return ["type_declaration", "type_spec"]
|
|
28
|
+
|
|
29
|
+
def _function_definition_types(self) -> list[str]:
|
|
30
|
+
"""Return node types that represent function definitions."""
|
|
31
|
+
return ["function_declaration", "method_declaration"]
|
|
32
|
+
|
|
33
|
+
def extract_decorators(self, node: Node) -> list[str]:
|
|
34
|
+
"""Extract decorators from a function node.
|
|
35
|
+
|
|
36
|
+
Go doesn't have decorators. Returns empty list.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
node: The AST node
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Empty list (Go has no decorators)
|
|
43
|
+
"""
|
|
44
|
+
return []
|
|
45
|
+
|
|
46
|
+
def extract_docstring(self, node: Node) -> str | None:
|
|
47
|
+
"""Extract godoc comment from a function or type node.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
node: The AST node
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
The godoc comment, or None if not present
|
|
54
|
+
"""
|
|
55
|
+
prev_sibling = node.prev_named_sibling
|
|
56
|
+
if prev_sibling and prev_sibling.type == "comment":
|
|
57
|
+
comment_text = prev_sibling.text
|
|
58
|
+
if comment_text:
|
|
59
|
+
text = comment_text.decode("utf-8")
|
|
60
|
+
if text.startswith("//"):
|
|
61
|
+
return text[2:].strip()
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
def extract_inheritance(self, class_node: Node) -> list[str]:
|
|
65
|
+
"""Extract embedded types from a struct or interface.
|
|
66
|
+
|
|
67
|
+
Go uses composition instead of inheritance.
|
|
68
|
+
This extracts embedded type names from struct/interface definitions.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
class_node: The type definition AST node
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
List of embedded type names
|
|
75
|
+
"""
|
|
76
|
+
embedded: list[str] = []
|
|
77
|
+
|
|
78
|
+
for child in class_node.children:
|
|
79
|
+
if child.type == "struct_type":
|
|
80
|
+
field_list = child.child_by_field_name("fields")
|
|
81
|
+
if field_list:
|
|
82
|
+
for field in field_list.children:
|
|
83
|
+
if field.type == "field_declaration":
|
|
84
|
+
if len(field.named_children) == 1:
|
|
85
|
+
type_node = field.named_children[0]
|
|
86
|
+
if (
|
|
87
|
+
type_node.type == "type_identifier"
|
|
88
|
+
and type_node.text
|
|
89
|
+
):
|
|
90
|
+
embedded.append(type_node.text.decode("utf-8"))
|
|
91
|
+
elif child.type == "interface_type":
|
|
92
|
+
for iface_child in child.children:
|
|
93
|
+
if iface_child.type == "type_identifier" and iface_child.text:
|
|
94
|
+
embedded.append(iface_child.text.decode("utf-8"))
|
|
95
|
+
|
|
96
|
+
return embedded
|
|
97
|
+
|
|
98
|
+
def parse_call_node(self, call_node: Node) -> tuple[str | None, str | None]:
|
|
99
|
+
"""Parse a call expression node to extract callee information.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
call_node: The call expression AST node
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Tuple of (callee_name, object_name)
|
|
106
|
+
"""
|
|
107
|
+
callee_name = None
|
|
108
|
+
object_name = None
|
|
109
|
+
|
|
110
|
+
func_node = call_node.child_by_field_name("function")
|
|
111
|
+
if func_node:
|
|
112
|
+
if func_node.type == "identifier" and func_node.text:
|
|
113
|
+
callee_name = func_node.text.decode("utf-8")
|
|
114
|
+
elif func_node.type == "selector_expression":
|
|
115
|
+
operand = func_node.child_by_field_name("operand")
|
|
116
|
+
field = func_node.child_by_field_name("field")
|
|
117
|
+
if operand and operand.text:
|
|
118
|
+
object_name = operand.text.decode("utf-8")
|
|
119
|
+
if field and field.text:
|
|
120
|
+
callee_name = field.text.decode("utf-8")
|
|
121
|
+
|
|
122
|
+
return callee_name, object_name
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""JavaScript language extractor implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from shotgun.codebase.core.extractors.base import BaseExtractor
|
|
8
|
+
from shotgun.codebase.core.extractors.types import SupportedLanguage
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from tree_sitter import Node
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class JavaScriptExtractor(BaseExtractor):
|
|
15
|
+
"""Extractor for JavaScript source code."""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def language(self) -> SupportedLanguage:
|
|
19
|
+
"""The language this extractor handles."""
|
|
20
|
+
return SupportedLanguage.JAVASCRIPT
|
|
21
|
+
|
|
22
|
+
def _class_definition_types(self) -> list[str]:
|
|
23
|
+
"""Return node types that represent class definitions."""
|
|
24
|
+
return ["class_declaration", "class"]
|
|
25
|
+
|
|
26
|
+
def _function_definition_types(self) -> list[str]:
|
|
27
|
+
"""Return node types that represent function definitions."""
|
|
28
|
+
return ["function_declaration", "method_definition", "arrow_function"]
|
|
29
|
+
|
|
30
|
+
def extract_decorators(self, node: Node) -> list[str]:
|
|
31
|
+
"""Extract decorators from a function or class node.
|
|
32
|
+
|
|
33
|
+
JavaScript doesn't have native decorators (they're a proposal).
|
|
34
|
+
Returns empty list.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
node: The AST node
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Empty list (JavaScript has no decorators)
|
|
41
|
+
"""
|
|
42
|
+
return []
|
|
43
|
+
|
|
44
|
+
def extract_docstring(self, node: Node) -> str | None:
|
|
45
|
+
"""Extract JSDoc comment from a function or class node.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
node: The AST node
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
The JSDoc content, or None if not present
|
|
52
|
+
"""
|
|
53
|
+
prev_sibling = node.prev_named_sibling
|
|
54
|
+
if prev_sibling and prev_sibling.type == "comment":
|
|
55
|
+
comment_text = prev_sibling.text
|
|
56
|
+
if comment_text:
|
|
57
|
+
text = comment_text.decode("utf-8")
|
|
58
|
+
if text.startswith("/**"):
|
|
59
|
+
text = text[3:]
|
|
60
|
+
if text.endswith("*/"):
|
|
61
|
+
text = text[:-2]
|
|
62
|
+
return text.strip()
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
def extract_inheritance(self, class_node: Node) -> list[str]:
|
|
66
|
+
"""Extract parent class names from a class definition.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
class_node: The class definition AST node
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
List of parent class names
|
|
73
|
+
"""
|
|
74
|
+
parent_names: list[str] = []
|
|
75
|
+
|
|
76
|
+
heritage = class_node.child_by_field_name("heritage")
|
|
77
|
+
if heritage:
|
|
78
|
+
for child in heritage.children:
|
|
79
|
+
if child.type == "identifier" and child.text:
|
|
80
|
+
parent_names.append(child.text.decode("utf-8"))
|
|
81
|
+
elif child.type == "member_expression":
|
|
82
|
+
parts: list[str] = []
|
|
83
|
+
self._extract_member_expression(child, parts)
|
|
84
|
+
if parts:
|
|
85
|
+
parent_names.append(".".join(parts))
|
|
86
|
+
|
|
87
|
+
return parent_names
|
|
88
|
+
|
|
89
|
+
def parse_call_node(self, call_node: Node) -> tuple[str | None, str | None]:
|
|
90
|
+
"""Parse a call expression node to extract callee information.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
call_node: The call expression AST node
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Tuple of (callee_name, object_name)
|
|
97
|
+
"""
|
|
98
|
+
callee_name = None
|
|
99
|
+
object_name = None
|
|
100
|
+
|
|
101
|
+
for child in call_node.children:
|
|
102
|
+
if child.type == "identifier" and child.text:
|
|
103
|
+
callee_name = child.text.decode("utf-8")
|
|
104
|
+
break
|
|
105
|
+
elif child.type == "member_expression":
|
|
106
|
+
obj_node = child.child_by_field_name("object")
|
|
107
|
+
prop_node = child.child_by_field_name("property")
|
|
108
|
+
if obj_node and obj_node.text:
|
|
109
|
+
object_name = obj_node.text.decode("utf-8")
|
|
110
|
+
if prop_node and prop_node.text:
|
|
111
|
+
callee_name = prop_node.text.decode("utf-8")
|
|
112
|
+
break
|
|
113
|
+
|
|
114
|
+
return callee_name, object_name
|
|
115
|
+
|
|
116
|
+
def _extract_member_expression(self, node: Node, parts: list[str]) -> None:
|
|
117
|
+
"""Recursively extract full name from member expression.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
node: The AST node
|
|
121
|
+
parts: List to accumulate name parts (modified in place)
|
|
122
|
+
"""
|
|
123
|
+
if node.type == "identifier" and node.text:
|
|
124
|
+
parts.insert(0, node.text.decode("utf-8"))
|
|
125
|
+
elif node.type == "member_expression":
|
|
126
|
+
prop_node = node.child_by_field_name("property")
|
|
127
|
+
if prop_node and prop_node.text:
|
|
128
|
+
parts.insert(0, prop_node.text.decode("utf-8"))
|
|
129
|
+
|
|
130
|
+
obj_node = node.child_by_field_name("object")
|
|
131
|
+
if obj_node:
|
|
132
|
+
self._extract_member_expression(obj_node, parts)
|