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.

Files changed (161) hide show
  1. shotgun/agents/agent_manager.py +497 -30
  2. shotgun/agents/cancellation.py +103 -0
  3. shotgun/agents/common.py +90 -77
  4. shotgun/agents/config/README.md +0 -1
  5. shotgun/agents/config/manager.py +52 -8
  6. shotgun/agents/config/models.py +48 -45
  7. shotgun/agents/config/provider.py +44 -29
  8. shotgun/agents/conversation/history/file_content_deduplication.py +66 -43
  9. shotgun/agents/conversation/history/token_counting/base.py +51 -9
  10. shotgun/agents/export.py +12 -13
  11. shotgun/agents/file_read.py +176 -0
  12. shotgun/agents/messages.py +15 -3
  13. shotgun/agents/models.py +90 -2
  14. shotgun/agents/plan.py +12 -13
  15. shotgun/agents/research.py +13 -10
  16. shotgun/agents/router/__init__.py +47 -0
  17. shotgun/agents/router/models.py +384 -0
  18. shotgun/agents/router/router.py +185 -0
  19. shotgun/agents/router/tools/__init__.py +18 -0
  20. shotgun/agents/router/tools/delegation_tools.py +557 -0
  21. shotgun/agents/router/tools/plan_tools.py +403 -0
  22. shotgun/agents/runner.py +17 -2
  23. shotgun/agents/specify.py +12 -13
  24. shotgun/agents/tasks.py +12 -13
  25. shotgun/agents/tools/__init__.py +8 -0
  26. shotgun/agents/tools/codebase/directory_lister.py +27 -39
  27. shotgun/agents/tools/codebase/file_read.py +26 -35
  28. shotgun/agents/tools/codebase/query_graph.py +9 -0
  29. shotgun/agents/tools/codebase/retrieve_code.py +9 -0
  30. shotgun/agents/tools/file_management.py +81 -3
  31. shotgun/agents/tools/file_read_tools/__init__.py +7 -0
  32. shotgun/agents/tools/file_read_tools/multimodal_file_read.py +167 -0
  33. shotgun/agents/tools/markdown_tools/__init__.py +62 -0
  34. shotgun/agents/tools/markdown_tools/insert_section.py +148 -0
  35. shotgun/agents/tools/markdown_tools/models.py +86 -0
  36. shotgun/agents/tools/markdown_tools/remove_section.py +114 -0
  37. shotgun/agents/tools/markdown_tools/replace_section.py +119 -0
  38. shotgun/agents/tools/markdown_tools/utils.py +453 -0
  39. shotgun/agents/tools/registry.py +41 -0
  40. shotgun/agents/tools/web_search/__init__.py +1 -2
  41. shotgun/agents/tools/web_search/gemini.py +1 -3
  42. shotgun/agents/tools/web_search/openai.py +42 -23
  43. shotgun/attachments/__init__.py +41 -0
  44. shotgun/attachments/errors.py +60 -0
  45. shotgun/attachments/models.py +107 -0
  46. shotgun/attachments/parser.py +257 -0
  47. shotgun/attachments/processor.py +193 -0
  48. shotgun/cli/clear.py +2 -2
  49. shotgun/cli/codebase/commands.py +181 -65
  50. shotgun/cli/compact.py +2 -2
  51. shotgun/cli/context.py +2 -2
  52. shotgun/cli/run.py +90 -0
  53. shotgun/cli/spec/backup.py +2 -1
  54. shotgun/cli/spec/commands.py +2 -0
  55. shotgun/cli/spec/models.py +18 -0
  56. shotgun/cli/spec/pull_service.py +122 -68
  57. shotgun/codebase/__init__.py +2 -0
  58. shotgun/codebase/benchmarks/__init__.py +35 -0
  59. shotgun/codebase/benchmarks/benchmark_runner.py +309 -0
  60. shotgun/codebase/benchmarks/exporters.py +119 -0
  61. shotgun/codebase/benchmarks/formatters/__init__.py +49 -0
  62. shotgun/codebase/benchmarks/formatters/base.py +34 -0
  63. shotgun/codebase/benchmarks/formatters/json_formatter.py +106 -0
  64. shotgun/codebase/benchmarks/formatters/markdown.py +136 -0
  65. shotgun/codebase/benchmarks/models.py +129 -0
  66. shotgun/codebase/core/__init__.py +4 -0
  67. shotgun/codebase/core/call_resolution.py +91 -0
  68. shotgun/codebase/core/change_detector.py +11 -6
  69. shotgun/codebase/core/errors.py +159 -0
  70. shotgun/codebase/core/extractors/__init__.py +23 -0
  71. shotgun/codebase/core/extractors/base.py +138 -0
  72. shotgun/codebase/core/extractors/factory.py +63 -0
  73. shotgun/codebase/core/extractors/go/__init__.py +7 -0
  74. shotgun/codebase/core/extractors/go/extractor.py +122 -0
  75. shotgun/codebase/core/extractors/javascript/__init__.py +7 -0
  76. shotgun/codebase/core/extractors/javascript/extractor.py +132 -0
  77. shotgun/codebase/core/extractors/protocol.py +109 -0
  78. shotgun/codebase/core/extractors/python/__init__.py +7 -0
  79. shotgun/codebase/core/extractors/python/extractor.py +141 -0
  80. shotgun/codebase/core/extractors/rust/__init__.py +7 -0
  81. shotgun/codebase/core/extractors/rust/extractor.py +139 -0
  82. shotgun/codebase/core/extractors/types.py +15 -0
  83. shotgun/codebase/core/extractors/typescript/__init__.py +7 -0
  84. shotgun/codebase/core/extractors/typescript/extractor.py +92 -0
  85. shotgun/codebase/core/gitignore.py +252 -0
  86. shotgun/codebase/core/ingestor.py +644 -354
  87. shotgun/codebase/core/kuzu_compat.py +119 -0
  88. shotgun/codebase/core/language_config.py +239 -0
  89. shotgun/codebase/core/manager.py +256 -46
  90. shotgun/codebase/core/metrics_collector.py +310 -0
  91. shotgun/codebase/core/metrics_types.py +347 -0
  92. shotgun/codebase/core/parallel_executor.py +424 -0
  93. shotgun/codebase/core/work_distributor.py +254 -0
  94. shotgun/codebase/core/worker.py +768 -0
  95. shotgun/codebase/indexing_state.py +86 -0
  96. shotgun/codebase/models.py +94 -0
  97. shotgun/codebase/service.py +13 -0
  98. shotgun/exceptions.py +1 -1
  99. shotgun/main.py +2 -10
  100. shotgun/prompts/agents/export.j2 +2 -0
  101. shotgun/prompts/agents/file_read.j2 +48 -0
  102. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +20 -28
  103. shotgun/prompts/agents/partials/content_formatting.j2 +12 -33
  104. shotgun/prompts/agents/partials/interactive_mode.j2 +9 -32
  105. shotgun/prompts/agents/partials/router_delegation_mode.j2 +35 -0
  106. shotgun/prompts/agents/plan.j2 +43 -1
  107. shotgun/prompts/agents/research.j2 +75 -20
  108. shotgun/prompts/agents/router.j2 +713 -0
  109. shotgun/prompts/agents/specify.j2 +94 -4
  110. shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +14 -1
  111. shotgun/prompts/agents/state/system_state.j2 +24 -15
  112. shotgun/prompts/agents/tasks.j2 +77 -23
  113. shotgun/settings.py +44 -0
  114. shotgun/shotgun_web/shared_specs/upload_pipeline.py +38 -0
  115. shotgun/tui/app.py +90 -23
  116. shotgun/tui/commands/__init__.py +9 -1
  117. shotgun/tui/components/attachment_bar.py +87 -0
  118. shotgun/tui/components/mode_indicator.py +120 -25
  119. shotgun/tui/components/prompt_input.py +23 -28
  120. shotgun/tui/components/status_bar.py +5 -4
  121. shotgun/tui/dependencies.py +58 -8
  122. shotgun/tui/protocols.py +37 -0
  123. shotgun/tui/screens/chat/chat.tcss +24 -1
  124. shotgun/tui/screens/chat/chat_screen.py +1374 -211
  125. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +8 -4
  126. shotgun/tui/screens/chat_screen/attachment_hint.py +40 -0
  127. shotgun/tui/screens/chat_screen/command_providers.py +0 -97
  128. shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
  129. shotgun/tui/screens/chat_screen/history/chat_history.py +49 -6
  130. shotgun/tui/screens/chat_screen/history/formatters.py +75 -15
  131. shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
  132. shotgun/tui/screens/chat_screen/history/user_question.py +25 -3
  133. shotgun/tui/screens/chat_screen/messages.py +219 -0
  134. shotgun/tui/screens/database_locked_dialog.py +219 -0
  135. shotgun/tui/screens/database_timeout_dialog.py +158 -0
  136. shotgun/tui/screens/kuzu_error_dialog.py +135 -0
  137. shotgun/tui/screens/model_picker.py +14 -9
  138. shotgun/tui/screens/models.py +11 -0
  139. shotgun/tui/screens/shotgun_auth.py +50 -0
  140. shotgun/tui/screens/spec_pull.py +2 -0
  141. shotgun/tui/state/processing_state.py +19 -0
  142. shotgun/tui/utils/mode_progress.py +20 -86
  143. shotgun/tui/widgets/__init__.py +2 -1
  144. shotgun/tui/widgets/approval_widget.py +152 -0
  145. shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
  146. shotgun/tui/widgets/plan_panel.py +129 -0
  147. shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
  148. shotgun/tui/widgets/widget_coordinator.py +18 -0
  149. shotgun/utils/file_system_utils.py +4 -1
  150. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/METADATA +88 -34
  151. shotgun_sh-0.6.1.dev1.dist-info/RECORD +292 -0
  152. shotgun/cli/export.py +0 -81
  153. shotgun/cli/plan.py +0 -73
  154. shotgun/cli/research.py +0 -93
  155. shotgun/cli/specify.py +0 -70
  156. shotgun/cli/tasks.py +0 -78
  157. shotgun/tui/screens/onboarding.py +0 -580
  158. shotgun_sh-0.2.29.dev2.dist-info/RECORD +0 -229
  159. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/WHEEL +0 -0
  160. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/entry_points.txt +0 -0
  161. {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
- ("Module", "TRACKS_Module"),
336
- ("Class", "TRACKS_Class"),
337
- ("Function", "TRACKS_Function"),
338
- ("Method", "TRACKS_Method"),
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,7 @@
1
+ """Go language extractor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .extractor import GoExtractor
6
+
7
+ __all__ = ["GoExtractor"]
@@ -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,7 @@
1
+ """JavaScript language extractor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .extractor import JavaScriptExtractor
6
+
7
+ __all__ = ["JavaScriptExtractor"]
@@ -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)