apisec-code-bolt 0.1.0__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.
Files changed (111) hide show
  1. apisec_code_bolt/__init__.py +42 -0
  2. apisec_code_bolt/__main__.py +11 -0
  3. apisec_code_bolt/analysis/__init__.py +96 -0
  4. apisec_code_bolt/analysis/analyzer.py +2309 -0
  5. apisec_code_bolt/analysis/binding_tracker.py +341 -0
  6. apisec_code_bolt/analysis/call_graph.py +1197 -0
  7. apisec_code_bolt/analysis/call_graph_types.py +332 -0
  8. apisec_code_bolt/analysis/call_resolver.py +988 -0
  9. apisec_code_bolt/analysis/capability_tagger.py +322 -0
  10. apisec_code_bolt/analysis/config_scanner.py +197 -0
  11. apisec_code_bolt/analysis/data_flow.py +1883 -0
  12. apisec_code_bolt/analysis/dependency_extractor.py +959 -0
  13. apisec_code_bolt/analysis/flow_analysis.py +1406 -0
  14. apisec_code_bolt/analysis/hof_catalog.py +61 -0
  15. apisec_code_bolt/analysis/integration_detector.py +1399 -0
  16. apisec_code_bolt/analysis/literal_scanner.py +300 -0
  17. apisec_code_bolt/analysis/path_normalizer.py +55 -0
  18. apisec_code_bolt/analysis/read_site_detector.py +310 -0
  19. apisec_code_bolt/analysis/request_patterns.py +162 -0
  20. apisec_code_bolt/analysis/sensitivity_classifier.py +224 -0
  21. apisec_code_bolt/analysis/sink_evidence.py +333 -0
  22. apisec_code_bolt/analysis/url_prefix_resolver.py +338 -0
  23. apisec_code_bolt/cli/__init__.py +5 -0
  24. apisec_code_bolt/cli/exit_codes.py +17 -0
  25. apisec_code_bolt/cli/main.py +1069 -0
  26. apisec_code_bolt/cloud/__init__.py +1 -0
  27. apisec_code_bolt/cloud/apisec_client.py +118 -0
  28. apisec_code_bolt/cloud/client.py +255 -0
  29. apisec_code_bolt/core/__init__.py +75 -0
  30. apisec_code_bolt/core/config.py +528 -0
  31. apisec_code_bolt/core/credentials.py +65 -0
  32. apisec_code_bolt/core/discovery.py +433 -0
  33. apisec_code_bolt/core/log_format.py +115 -0
  34. apisec_code_bolt/core/manifest.py +1009 -0
  35. apisec_code_bolt/core/repo.py +280 -0
  36. apisec_code_bolt/core/state.py +59 -0
  37. apisec_code_bolt/core/telemetry.py +451 -0
  38. apisec_code_bolt/core/types.py +587 -0
  39. apisec_code_bolt/fingerprinting/__init__.py +1 -0
  40. apisec_code_bolt/frameworks/__init__.py +29 -0
  41. apisec_code_bolt/frameworks/_jwt_common.py +50 -0
  42. apisec_code_bolt/frameworks/auth_helpers.py +437 -0
  43. apisec_code_bolt/frameworks/base.py +608 -0
  44. apisec_code_bolt/frameworks/dotnet/__init__.py +17 -0
  45. apisec_code_bolt/frameworks/dotnet/_path_helpers.py +43 -0
  46. apisec_code_bolt/frameworks/dotnet/aspnet_plugin.py +2546 -0
  47. apisec_code_bolt/frameworks/dotnet/grpc_plugin.py +559 -0
  48. apisec_code_bolt/frameworks/dotnet/jwt_config_extractor.py +545 -0
  49. apisec_code_bolt/frameworks/dotnet/legacy_aspnet_plugin.py +732 -0
  50. apisec_code_bolt/frameworks/dotnet/refit_plugin.py +374 -0
  51. apisec_code_bolt/frameworks/dotnet/wcf_plugin.py +1239 -0
  52. apisec_code_bolt/frameworks/java/__init__.py +6 -0
  53. apisec_code_bolt/frameworks/java/_annotations.py +167 -0
  54. apisec_code_bolt/frameworks/java/_constraints.py +128 -0
  55. apisec_code_bolt/frameworks/java/graphql_plugin.py +287 -0
  56. apisec_code_bolt/frameworks/java/jaxrs_plugin.py +748 -0
  57. apisec_code_bolt/frameworks/java/jwt_config_extractor.py +361 -0
  58. apisec_code_bolt/frameworks/java/micronaut_plugin.py +1059 -0
  59. apisec_code_bolt/frameworks/java/spring_plugin.py +1293 -0
  60. apisec_code_bolt/frameworks/js/__init__.py +8 -0
  61. apisec_code_bolt/frameworks/js/express_plugin.py +391 -0
  62. apisec_code_bolt/frameworks/js/fastify_plugin.py +381 -0
  63. apisec_code_bolt/frameworks/js/graphql_plugin.py +198 -0
  64. apisec_code_bolt/frameworks/js/nestjs_plugin.py +423 -0
  65. apisec_code_bolt/frameworks/python/__init__.py +19 -0
  66. apisec_code_bolt/frameworks/python/celery_plugin.py +393 -0
  67. apisec_code_bolt/frameworks/python/click_plugin.py +427 -0
  68. apisec_code_bolt/frameworks/python/django_plugin.py +867 -0
  69. apisec_code_bolt/frameworks/python/fastapi/__init__.py +28 -0
  70. apisec_code_bolt/frameworks/python/fastapi/plugin.py +1390 -0
  71. apisec_code_bolt/frameworks/python/flask_plugin.py +205 -0
  72. apisec_code_bolt/frameworks/python/graphql_plugin.py +274 -0
  73. apisec_code_bolt/frameworks/python/prefect_plugin.py +251 -0
  74. apisec_code_bolt/frameworks/python/webhook_plugin.py +255 -0
  75. apisec_code_bolt/parsing/__init__.py +62 -0
  76. apisec_code_bolt/parsing/base.py +554 -0
  77. apisec_code_bolt/parsing/csharp/__init__.py +5 -0
  78. apisec_code_bolt/parsing/csharp/language_services.py +203 -0
  79. apisec_code_bolt/parsing/csharp/literals.py +72 -0
  80. apisec_code_bolt/parsing/csharp/parser.py +1158 -0
  81. apisec_code_bolt/parsing/csharp/type_resolver.py +568 -0
  82. apisec_code_bolt/parsing/js/__init__.py +5 -0
  83. apisec_code_bolt/parsing/js/language_services.py +118 -0
  84. apisec_code_bolt/parsing/js/parser.py +622 -0
  85. apisec_code_bolt/parsing/jvm/__init__.py +7 -0
  86. apisec_code_bolt/parsing/jvm/language_services.py +270 -0
  87. apisec_code_bolt/parsing/jvm/parser.py +774 -0
  88. apisec_code_bolt/parsing/jvm/type_resolver.py +422 -0
  89. apisec_code_bolt/parsing/python/__init__.py +150 -0
  90. apisec_code_bolt/parsing/python/cbv_extractor.py +606 -0
  91. apisec_code_bolt/parsing/python/constant_resolver.py +500 -0
  92. apisec_code_bolt/parsing/python/cross_file_resolver.py +1054 -0
  93. apisec_code_bolt/parsing/python/dynamic_route_detector.py +532 -0
  94. apisec_code_bolt/parsing/python/expression_utils.py +221 -0
  95. apisec_code_bolt/parsing/python/extraction_types.py +271 -0
  96. apisec_code_bolt/parsing/python/language_services.py +487 -0
  97. apisec_code_bolt/parsing/python/parameter_analyzer.py +789 -0
  98. apisec_code_bolt/parsing/python/parser.py +719 -0
  99. apisec_code_bolt/parsing/python/path_resolver.py +576 -0
  100. apisec_code_bolt/parsing/python/router_registry.py +806 -0
  101. apisec_code_bolt/parsing/python/type_resolver.py +730 -0
  102. apisec_code_bolt/parsing/python/visitors.py +1544 -0
  103. apisec_code_bolt/parsing/services.py +544 -0
  104. apisec_code_bolt/query/__init__.py +1 -0
  105. apisec_code_bolt/query/ast_cache.py +182 -0
  106. apisec_code_bolt/query/executor.py +283 -0
  107. apisec_code_bolt/query/handlers.py +832 -0
  108. apisec_code_bolt-0.1.0.dist-info/METADATA +230 -0
  109. apisec_code_bolt-0.1.0.dist-info/RECORD +111 -0
  110. apisec_code_bolt-0.1.0.dist-info/WHEEL +4 -0
  111. apisec_code_bolt-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,251 @@
1
+ """
2
+ Prefect workflow framework plugin.
3
+
4
+ Detects flow and task entry points from Prefect 3.x decorator patterns.
5
+
6
+ Supported:
7
+ - @flow / @flow(name="...") — Prefect flow definitions
8
+ - @task / @task(name="...") — Prefect task definitions
9
+ - @prefect.flow / @prefect.task — qualified forms
10
+ - Function parameter extraction as task arguments
11
+ - Type annotation preservation (used by Prefect for Pydantic validation)
12
+
13
+ Not supported (yet):
14
+ - serve() / deploy() lifecycle methods
15
+ - Prefect blocks (Secret, etc.) as data sources
16
+ - Subflow detection (flow calling flow)
17
+ - Task result caching semantics
18
+ - Prefect events and automations
19
+ - Work pool / worker configuration analysis
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from typing import Any, ClassVar
25
+
26
+ from ...core.types import (
27
+ Confidence,
28
+ Framework,
29
+ HttpMethod,
30
+ Language,
31
+ ParameterLocation,
32
+ )
33
+ from ...parsing.base import ParsedDecorator, ParsedFile, ParsedFunction
34
+ from ..base import (
35
+ BaseFrameworkPlugin,
36
+ ExtractedAuthDependency,
37
+ ExtractedAuthScheme,
38
+ ExtractedDependency,
39
+ ExtractedMiddleware,
40
+ ExtractedParameter,
41
+ ExtractedResponse,
42
+ ExtractedRoute,
43
+ FrameworkPluginRegistry,
44
+ )
45
+
46
+ # ── Import detection ────────────────────────────────────────────────────────
47
+
48
+ PREFECT_IMPORTS = frozenset({"prefect"})
49
+
50
+
51
+ # ── Decorator name sets ─────────────────────────────────────────────────────
52
+
53
+ FLOW_DECORATORS: set[str] = {
54
+ "flow", # @flow (bare import)
55
+ "prefect.flow", # @prefect.flow (qualified)
56
+ }
57
+
58
+ TASK_DECORATORS: set[str] = {
59
+ "task", # @task (bare import)
60
+ "prefect.task", # @prefect.task (qualified)
61
+ }
62
+
63
+ ALL_PREFECT_DECORATORS = FLOW_DECORATORS | TASK_DECORATORS
64
+
65
+
66
+ # ── Plugin ──────────────────────────────────────────────────────────────────
67
+
68
+
69
+ class PrefectPlugin(BaseFrameworkPlugin):
70
+ """Plugin for Prefect 3.x flow and task framework extraction."""
71
+
72
+ FRAMEWORK: ClassVar[Framework] = Framework.PREFECT
73
+ LANGUAGE: ClassVar[Language] = Language.PYTHON
74
+ DETECTION_IMPORTS: ClassVar[frozenset[str]] = PREFECT_IMPORTS
75
+
76
+ # ------------------------------------------------------------------
77
+ # Detection
78
+ # ------------------------------------------------------------------
79
+
80
+ def detect(self, parsed_file: ParsedFile) -> bool:
81
+ for imp in parsed_file.imports:
82
+ mod = (imp.module or "").split(".")[0]
83
+ if mod == "prefect":
84
+ return True
85
+ for name in imp.names:
86
+ if name in ("prefect", "flow", "task") and (
87
+ imp.module is None or imp.module.split(".")[0] == "prefect"
88
+ ):
89
+ return True
90
+ return False
91
+
92
+ # ------------------------------------------------------------------
93
+ # Route (flow/task) extraction
94
+ # ------------------------------------------------------------------
95
+
96
+ def extract_routes(
97
+ self,
98
+ parsed_file: ParsedFile,
99
+ context: Any = None,
100
+ ) -> list[ExtractedRoute]:
101
+ routes: list[ExtractedRoute] = []
102
+
103
+ for func in parsed_file.functions:
104
+ route = self._try_extract_entry_point(func)
105
+ if route:
106
+ routes.append(route)
107
+
108
+ for cls in parsed_file.classes:
109
+ for method in cls.methods:
110
+ route = self._try_extract_entry_point(method)
111
+ if route:
112
+ routes.append(route)
113
+
114
+ return routes
115
+
116
+ # ------------------------------------------------------------------
117
+ # Stubs — Prefect has no HTTP auth/deps/middleware
118
+ # ------------------------------------------------------------------
119
+
120
+ def extract_dependencies(self, parsed_file: ParsedFile) -> list[ExtractedDependency]:
121
+ return []
122
+
123
+ def extract_auth_schemes(self, parsed_file: ParsedFile) -> list[ExtractedAuthScheme]:
124
+ return []
125
+
126
+ def extract_auth_dependencies(
127
+ self,
128
+ parsed_file: ParsedFile,
129
+ known_scheme_names: set[str] | None = None,
130
+ **kwargs: Any,
131
+ ) -> list[ExtractedAuthDependency]:
132
+ return []
133
+
134
+ def extract_middleware(self, parsed_file: ParsedFile) -> list[ExtractedMiddleware]:
135
+ return []
136
+
137
+ # ------------------------------------------------------------------
138
+ # Internal
139
+ # ------------------------------------------------------------------
140
+
141
+ def _try_extract_entry_point(
142
+ self,
143
+ func: ParsedFunction,
144
+ ) -> ExtractedRoute | None:
145
+ """If *func* has a @flow or @task decorator, return an ExtractedRoute."""
146
+ dec, is_flow = self._find_prefect_decorator(func)
147
+ if dec is None:
148
+ return None
149
+
150
+ entry_name = self._resolve_name(dec, func)
151
+ params = self._extract_parameters(func)
152
+
153
+ return ExtractedRoute(
154
+ kind="task",
155
+ method=HttpMethod.POST,
156
+ path=entry_name,
157
+ handler_function=func.qualified_name,
158
+ handler_location=func.location,
159
+ path_params=[],
160
+ query_params=params,
161
+ header_params=[],
162
+ cookie_params=[],
163
+ body=None,
164
+ response=ExtractedResponse(),
165
+ confidence=Confidence.HIGH,
166
+ )
167
+
168
+ def _find_prefect_decorator(
169
+ self,
170
+ func: ParsedFunction,
171
+ ) -> tuple[ParsedDecorator | None, bool]:
172
+ """Return (decorator, is_flow) if a Prefect decorator is found.
173
+
174
+ ``is_flow`` is True for @flow, False for @task.
175
+ """
176
+ for dec in func.decorators:
177
+ base = self._decorator_base_name(dec)
178
+ if base in FLOW_DECORATORS:
179
+ return dec, True
180
+ if base in TASK_DECORATORS:
181
+ return dec, False
182
+ return None, False
183
+
184
+ def _resolve_name(
185
+ self,
186
+ dec: ParsedDecorator,
187
+ func: ParsedFunction,
188
+ ) -> str:
189
+ """Derive the canonical flow/task name.
190
+
191
+ Priority: explicit ``name=`` kwarg > function name (no dash conversion).
192
+ """
193
+ explicit = self._extract_decorator_arg(dec, "name")
194
+ if explicit and isinstance(explicit, str):
195
+ return explicit
196
+ return func.qualified_name.full
197
+
198
+ def _extract_parameters(
199
+ self,
200
+ func: ParsedFunction,
201
+ ) -> list[ExtractedParameter]:
202
+ """Extract task/flow argument parameters from the function signature."""
203
+ params: list[ExtractedParameter] = []
204
+
205
+ for p in func.parameters:
206
+ if p.name in ("self", "cls"):
207
+ continue
208
+ if p.is_variadic or p.is_keyword_variadic:
209
+ continue
210
+
211
+ params.append(
212
+ ExtractedParameter(
213
+ name=p.name,
214
+ location=ParameterLocation.TASK_ARGUMENT,
215
+ type_annotation=p.type_annotation,
216
+ required=p.default_value is None,
217
+ default_value=p.default_value,
218
+ )
219
+ )
220
+
221
+ return params
222
+
223
+ @staticmethod
224
+ def _decorator_base_name(dec: ParsedDecorator) -> str:
225
+ """Normalise a decorator to a lookup key.
226
+
227
+ Handles:
228
+ - ``prefect.flow`` / ``prefect.task`` (qualified)
229
+ - ``flow`` / ``task`` (bare import)
230
+ """
231
+ if dec.qualified_name:
232
+ qn = dec.qualified_name.full
233
+ parts = qn.rsplit(".", 1)
234
+ if len(parts) == 2:
235
+ mod, attr = parts
236
+ if mod == "prefect":
237
+ return f"prefect.{attr}"
238
+ if attr in ("flow", "task"):
239
+ return attr
240
+ return qn
241
+
242
+ name = dec.name
243
+ if "." in name:
244
+ _obj, _, attr = name.rpartition(".")
245
+ if attr in ("flow", "task"):
246
+ return attr
247
+ return name
248
+
249
+
250
+ _prefect_plugin = PrefectPlugin()
251
+ FrameworkPluginRegistry.register(_prefect_plugin)
@@ -0,0 +1,255 @@
1
+ """
2
+ Generic webhook and event handler plugin.
3
+
4
+ Detects event-driven entry points across multiple frameworks and patterns.
5
+
6
+ Supported constructs:
7
+ - FastAPI/Starlette @app.on_event("startup") / @app.on_event("shutdown")
8
+ - Django signals: @receiver(post_save, ...) — from django.dispatch
9
+ - Generic webhook handlers: @app.webhook, @webhook_handler
10
+ - Flask-SocketIO @socketio.on("event_name")
11
+ - gidgethub Router: @router.register("event_type", action="...") — GitHub webhook bots
12
+ - asyncio event loop callbacks (limited)
13
+
14
+ Not supported (yet):
15
+ - Django Channels consumers
16
+ - AWS Lambda handler convention (def handler(event, context))
17
+ - Cloud Functions / Azure Functions triggers
18
+ - Pub/Sub subscription callbacks
19
+ - Custom event bus implementations
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from typing import Any, ClassVar
25
+
26
+ from ...core.types import (
27
+ Confidence,
28
+ Framework,
29
+ HttpMethod,
30
+ Language,
31
+ ParameterLocation,
32
+ )
33
+ from ...parsing.base import ParsedDecorator, ParsedFile, ParsedFunction
34
+ from ..base import (
35
+ BaseFrameworkPlugin,
36
+ ExtractedAuthDependency,
37
+ ExtractedAuthScheme,
38
+ ExtractedDependency,
39
+ ExtractedMiddleware,
40
+ ExtractedParameter,
41
+ ExtractedResponse,
42
+ ExtractedRoute,
43
+ FrameworkPluginRegistry,
44
+ )
45
+
46
+ # ── Decorator sets ──────────────────────────────────────────────────────────
47
+
48
+ EVENT_DECORATORS: set[str] = {
49
+ "on_event", # @app.on_event("startup")
50
+ "on", # @socketio.on("message")
51
+ "receiver", # Django signal: @receiver(post_save, ...)
52
+ "on_event_handler",
53
+ }
54
+
55
+ WEBHOOK_DECORATORS: set[str] = {
56
+ "webhook", # @app.webhook(...)
57
+ "webhook_handler",
58
+ }
59
+
60
+ ALL_EVENT_DECORATORS = EVENT_DECORATORS | WEBHOOK_DECORATORS
61
+
62
+ # FastAPI/Starlette lifecycle event names. These are not HTTP endpoints: they
63
+ # run only during application startup/shutdown and cannot receive external
64
+ # requests. They should be classified as kind="lifecycle" so downstream rules
65
+ # (missing_auth, rate_limiting, etc.) can skip them.
66
+ LIFECYCLE_EVENT_NAMES: set[str] = {
67
+ "startup",
68
+ "shutdown",
69
+ "lifespan",
70
+ }
71
+
72
+
73
+ class WebhookEventPlugin(BaseFrameworkPlugin):
74
+ """Generic plugin for webhook and event handler entry points."""
75
+
76
+ FRAMEWORK: ClassVar[Framework] = Framework.GENERIC
77
+ LANGUAGE: ClassVar[Language] = Language.PYTHON
78
+ DETECTION_IMPORTS: ClassVar[frozenset[str]] = frozenset(
79
+ {
80
+ "django.dispatch",
81
+ "flask_socketio",
82
+ "gidgethub",
83
+ }
84
+ )
85
+
86
+ def detect(self, parsed_file: ParsedFile) -> bool:
87
+ is_gidgethub = self._has_gidgethub_import(parsed_file)
88
+ for func in parsed_file.functions:
89
+ for dec in func.decorators:
90
+ base = self._decorator_base_name(dec)
91
+ if base in ALL_EVENT_DECORATORS:
92
+ return True
93
+ if is_gidgethub and base == "register":
94
+ return True
95
+ return False
96
+
97
+ def extract_routes(
98
+ self,
99
+ parsed_file: ParsedFile,
100
+ context: Any = None,
101
+ ) -> list[ExtractedRoute]:
102
+ routes: list[ExtractedRoute] = []
103
+ is_gidgethub = self._has_gidgethub_import(parsed_file)
104
+
105
+ for func in parsed_file.functions:
106
+ route = self._try_extract_event(func, is_gidgethub=is_gidgethub)
107
+ if route:
108
+ routes.append(route)
109
+
110
+ for cls in parsed_file.classes:
111
+ for method in cls.methods:
112
+ route = self._try_extract_event(method, is_gidgethub=is_gidgethub)
113
+ if route:
114
+ routes.append(route)
115
+
116
+ return routes
117
+
118
+ @staticmethod
119
+ def _has_gidgethub_import(parsed_file: ParsedFile) -> bool:
120
+ return any((imp.module or "").startswith("gidgethub") for imp in parsed_file.imports)
121
+
122
+ def extract_dependencies(self, parsed_file: ParsedFile) -> list[ExtractedDependency]:
123
+ return []
124
+
125
+ def extract_auth_schemes(self, parsed_file: ParsedFile) -> list[ExtractedAuthScheme]:
126
+ return []
127
+
128
+ def extract_auth_dependencies(
129
+ self,
130
+ parsed_file: ParsedFile,
131
+ known_scheme_names: set[str] | None = None,
132
+ **kwargs: Any,
133
+ ) -> list[ExtractedAuthDependency]:
134
+ return []
135
+
136
+ def extract_middleware(self, parsed_file: ParsedFile) -> list[ExtractedMiddleware]:
137
+ return []
138
+
139
+ def _try_extract_event(
140
+ self, func: ParsedFunction, *, is_gidgethub: bool = False
141
+ ) -> ExtractedRoute | None:
142
+ dec = self._find_event_decorator(func, is_gidgethub=is_gidgethub)
143
+ if dec is None:
144
+ return None
145
+
146
+ base = self._decorator_base_name(dec)
147
+ is_webhook = base in WEBHOOK_DECORATORS
148
+ is_gh_register = is_gidgethub and base == "register"
149
+ event_name = self._resolve_event_name(dec, func, is_gh_register=is_gh_register)
150
+ params = self._extract_parameters(func)
151
+
152
+ if is_webhook or is_gh_register:
153
+ kind = "webhook"
154
+ elif base == "on_event" and event_name in LIFECYCLE_EVENT_NAMES:
155
+ # FastAPI @app.on_event("startup"|"shutdown") — not an HTTP endpoint.
156
+ # The engine's missing_auth / rate_limiting rules must skip entry
157
+ # points with kind="lifecycle" since they cannot receive external
158
+ # requests.
159
+ kind = "lifecycle"
160
+ else:
161
+ kind = "event"
162
+
163
+ return ExtractedRoute(
164
+ kind=kind,
165
+ method=HttpMethod.POST,
166
+ path=event_name,
167
+ handler_function=func.qualified_name,
168
+ handler_location=func.location,
169
+ path_params=[],
170
+ query_params=params,
171
+ header_params=[],
172
+ cookie_params=[],
173
+ body=None,
174
+ response=ExtractedResponse(),
175
+ confidence=Confidence.MEDIUM,
176
+ )
177
+
178
+ def _find_event_decorator(
179
+ self, func: ParsedFunction, *, is_gidgethub: bool = False
180
+ ) -> ParsedDecorator | None:
181
+ for dec in func.decorators:
182
+ base = self._decorator_base_name(dec)
183
+ if base in ALL_EVENT_DECORATORS:
184
+ return dec
185
+ if is_gidgethub and base == "register":
186
+ return dec
187
+ return None
188
+
189
+ def _resolve_event_name(
190
+ self,
191
+ dec: ParsedDecorator,
192
+ func: ParsedFunction,
193
+ *,
194
+ is_gh_register: bool = False,
195
+ ) -> str:
196
+ if dec.positional_args:
197
+ first = dec.positional_args[0]
198
+ if isinstance(first, str):
199
+ event = first
200
+ if is_gh_register:
201
+ # @router.register("pull_request", action="opened") →
202
+ # emit "pull_request:opened" so distinct actions are separate routes.
203
+ action = dec.arguments.get("action")
204
+ if action and isinstance(action, str):
205
+ event = f"{event}:{action}"
206
+ return event
207
+ explicit = self._extract_decorator_arg(dec, "name")
208
+ if explicit and isinstance(explicit, str):
209
+ return explicit
210
+ signal_arg = self._extract_decorator_arg(dec, "signal")
211
+ if signal_arg and isinstance(signal_arg, str):
212
+ return signal_arg
213
+ return func.name
214
+
215
+ def _extract_parameters(self, func: ParsedFunction) -> list[ExtractedParameter]:
216
+ params: list[ExtractedParameter] = []
217
+ for p in func.parameters:
218
+ if p.name in ("self", "cls", "sender", "signal", "**kwargs"):
219
+ continue
220
+ if p.is_variadic or p.is_keyword_variadic:
221
+ continue
222
+ params.append(
223
+ ExtractedParameter(
224
+ name=p.name,
225
+ location=ParameterLocation.TASK_ARGUMENT,
226
+ type_annotation=p.type_annotation,
227
+ required=p.default_value is None,
228
+ default_value=p.default_value,
229
+ )
230
+ )
231
+ return params
232
+
233
+ @staticmethod
234
+ def _decorator_base_name(dec: ParsedDecorator) -> str:
235
+ if dec.qualified_name:
236
+ qn = dec.qualified_name.full
237
+ parts = qn.rsplit(".", 1)
238
+ if len(parts) == 2:
239
+ _, attr = parts
240
+ # Return the attribute name for known decorators and for
241
+ # `register` (gidgethub Router pattern).
242
+ if attr in ALL_EVENT_DECORATORS or attr == "register":
243
+ return attr
244
+ return qn
245
+
246
+ name = dec.name
247
+ if "." in name:
248
+ _obj, _, attr = name.rpartition(".")
249
+ if attr in ALL_EVENT_DECORATORS or attr == "register":
250
+ return attr
251
+ return name
252
+
253
+
254
+ _webhook_plugin = WebhookEventPlugin()
255
+ FrameworkPluginRegistry.register(_webhook_plugin)
@@ -0,0 +1,62 @@
1
+ """Language-specific parsing modules."""
2
+
3
+ from .base import (
4
+ BaseParser,
5
+ ParsedArgument,
6
+ ParsedAssignment,
7
+ ParsedCallSite,
8
+ ParsedClass,
9
+ ParsedDecorator,
10
+ ParsedFile,
11
+ ParsedFunction,
12
+ ParsedImport,
13
+ ParsedParameter,
14
+ ParsedSymbol,
15
+ ParserRegistry,
16
+ )
17
+ from .services import (
18
+ # Data types
19
+ AnalysisContext,
20
+ ConstantResolver,
21
+ # Abstract base
22
+ LanguageServices,
23
+ PathResolver,
24
+ ResolvedConstant,
25
+ ResolvedField,
26
+ ResolvedPath,
27
+ ResolvedType,
28
+ RouterInfo,
29
+ RouterRegistry,
30
+ # Protocols
31
+ TypeResolver,
32
+ )
33
+
34
+ __all__ = [
35
+ # Base parsing
36
+ "BaseParser",
37
+ "ParserRegistry",
38
+ "ParsedFile",
39
+ "ParsedFunction",
40
+ "ParsedClass",
41
+ "ParsedImport",
42
+ "ParsedCallSite",
43
+ "ParsedArgument",
44
+ "ParsedAssignment",
45
+ "ParsedSymbol",
46
+ "ParsedParameter",
47
+ "ParsedDecorator",
48
+ # Service protocols
49
+ "TypeResolver",
50
+ "ConstantResolver",
51
+ "PathResolver",
52
+ "RouterRegistry",
53
+ # Service data types
54
+ "AnalysisContext",
55
+ "ResolvedType",
56
+ "ResolvedField",
57
+ "ResolvedConstant",
58
+ "ResolvedPath",
59
+ "RouterInfo",
60
+ # Language services
61
+ "LanguageServices",
62
+ ]