@shortlink-org/portolan 0.2.3 → 0.3.0
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.
- package/README.md +23 -0
- package/catalog/enum_test.go +46 -0
- package/catalog/evidence_test.go +35 -0
- package/catalog/model.go +1066 -0
- package/catalog/roundtrip_test.go +203 -0
- package/catalog/via_test.go +38 -0
- package/cli/init.test.mjs +6 -1
- package/cli/portolan.mjs +8 -0
- package/cli/portolan.test.mjs +49 -0
- package/go.mod +14 -0
- package/go.sum +20 -0
- package/internal/gocall/README.md +19 -0
- package/internal/gocall/analyze.go +189 -0
- package/internal/gocall/analyze_test.go +107 -0
- package/internal/gohttp/analyze.go +2562 -0
- package/internal/gohttp/destination.go +373 -0
- package/internal/gohttp/endpoints.go +1067 -0
- package/internal/gohttp/roots.go +320 -0
- package/internal/gohttp/typed.go +96 -0
- package/internal/goscan/constants.go +85 -0
- package/internal/goscan/goscan_test.go +227 -0
- package/internal/goscan/index.go +629 -0
- package/internal/goscan/index_test.go +66 -0
- package/internal/goscan/names.go +52 -0
- package/internal/goscan/parse_test.go +11 -0
- package/internal/goscan/source.go +37 -0
- package/internal/goscan/tree.go +284 -0
- package/internal/goscan/types.go +99 -0
- package/internal/wsdl/ids.go +127 -0
- package/internal/wsdl/ids_test.go +21 -0
- package/internal/wsdl/model.go +70 -0
- package/internal/wsdl/parse.go +949 -0
- package/internal/wsdl/parse_test.go +170 -0
- package/package.json +22 -10
- package/plugin/describe.go +118 -0
- package/plugin/describe_test.go +114 -0
- package/plugin/protocol.go +141 -0
- package/plugin/schematest/schematest.go +126 -0
- package/plugins/README.md +114 -46
- package/plugins/cmd/portolan-http-clients/main.go +19 -0
- package/plugins/extract-celery/extract.py +0 -2
- package/plugins/extract-celery/extract_test.py +1 -1
- package/plugins/extract-django/README.md +39 -17
- package/plugins/extract-django/domain.py +28 -17
- package/plugins/extract-django/extract.py +21 -7
- package/plugins/extract-django/extract_test.py +55 -2
- package/plugins/extract-django/lifecycle.py +2 -0
- package/plugins/extract-django/operations.py +1 -1
- package/plugins/extract-django/routing_test.py +109 -1
- package/plugins/extract-django/store.py +1 -1
- package/plugins/extract-django/transport.py +101 -55
- package/plugins/extract-django/verbs.py +241 -0
- package/plugins/extract-go/README.md +47 -0
- package/plugins/extract-http-clients/describe.go +19 -0
- package/plugins/extract-http-clients/describe_test.go +11 -0
- package/plugins/extract-http-clients/extract.go +740 -0
- package/plugins/extract-http-clients/extract_test.go +1561 -0
- package/plugins/extract-http-clients/main.go +41 -0
- package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
- package/plugins/extract-python-kafka/README.md +6 -0
- package/plugins/extract-python-kafka/extract.py +2 -4
- package/plugins/extract-python-kafka/extract_test.py +18 -2
- package/plugins/extract-ts/extract.test.ts +2 -2
- package/plugins/extract-ts/extract.ts +4 -5
- package/plugins/extract-ts/graphql.test.ts +1 -1
- package/plugins/openapi/ids.go +261 -0
- package/plugins/openapi/ids_test.go +98 -0
- package/plugins/portolan-go.wasm +0 -0
- package/plugins/pyplugin/catalog.py +12 -1
- package/plugins/pyplugin/kafka.py +74 -3
- package/plugins/pyplugin/protocol.py +1 -5
- package/portolan.json +3 -2
- package/schema/portolan.schema.json +34 -0
- package/scripts/README.md +18 -12
- package/scripts/catalog-sources.mjs +6 -0
- package/scripts/delivery-presets.mjs +21 -11
- package/scripts/diff.mjs +5 -1
- package/scripts/django-aggregates.test.mjs +58 -0
- package/scripts/gen-likec4.mjs +79 -21
- package/scripts/gen-likec4.test.mjs +25 -2
- package/scripts/gen.mjs +118 -115
- package/scripts/go-discovery.test.mjs +30 -0
- package/scripts/history.mjs +186 -3
- package/scripts/history.test.mjs +1 -1
- package/scripts/host-plugins/fetch-git.mjs +77 -21
- package/scripts/host-plugins/fetch-git.test.mjs +62 -8
- package/scripts/local-api.mjs +71 -4
- package/scripts/local-api.test.mjs +63 -4
- package/scripts/local-discovery.mjs +82 -9
- package/scripts/manifest.mjs +5 -3
- package/scripts/manifest.test.mjs +24 -0
- package/scripts/output-diff.mjs +94 -0
- package/scripts/output-diff.test.mjs +36 -0
- package/scripts/package-smoke.mjs +62 -4
- package/scripts/plugin-host.mjs +22 -2
- package/scripts/plugin-host.test.mjs +9 -0
- package/scripts/plugin-wasm-worker.mjs +4 -1
- package/scripts/provenance.mjs +72 -0
- package/scripts/provenance.test.mjs +149 -0
- package/scripts/run-builtin.mjs +39 -5
- package/scripts/schema.mjs +29 -0
- package/scripts/warning-policy.mjs +161 -0
- package/scripts/warning-policy.test.mjs +56 -0
- package/src/app/Breadcrumbs.test.ts +4 -0
- package/src/app/Breadcrumbs.tsx +1 -0
- package/src/app/Sidebar.tsx +3 -3
- package/src/catalog-docs.test.ts +64 -0
- package/src/catalog-docs.ts +35 -0
- package/src/catalog-error.test.ts +15 -0
- package/src/catalog-model.ts +70 -6
- package/src/catalog-stores.test.ts +17 -0
- package/src/catalog-validation.ts +52 -2
- package/src/catalog.test.ts +13 -2
- package/src/chat/Starter.tsx +5 -11
- package/src/chat/tools.test.ts +27 -0
- package/src/chat/tools.ts +5 -9
- package/src/components/CatalogStamp.tsx +10 -8
- package/src/components/ChannelRows.messagepack.test.tsx +28 -0
- package/src/components/ChannelRows.test.tsx +54 -0
- package/src/components/ChannelRows.tsx +57 -10
- package/src/components/HTTPDestinationEvidence.test.tsx +23 -0
- package/src/components/HTTPDestinationEvidence.tsx +31 -0
- package/src/components/Integrations.tsx +1 -1
- package/src/components/LifecycleDiagram.tsx +28 -12
- package/src/components/MachineDocs.tsx +6 -5
- package/src/components/MethodRows.tsx +9 -2
- package/src/components/ProblemRow.tsx +4 -0
- package/src/components/RelationEvidence.test.tsx +14 -0
- package/src/components/RelationEvidence.tsx +53 -0
- package/src/components/WhatLinksHere.tsx +6 -4
- package/src/data.ts +25 -7
- package/src/enrich.test.ts +336 -6
- package/src/enrich.ts +206 -3
- package/src/flow/StepDetail.tsx +104 -54
- package/src/flow/answers.test.ts +18 -1
- package/src/flow/answers.ts +37 -8
- package/src/flow/evidence.test.ts +16 -0
- package/src/flow/evidence.ts +34 -0
- package/src/index.css +44 -0
- package/src/landing/DraggableReveal.tsx +3 -2
- package/src/landing/EvidencePipeline.tsx +105 -0
- package/src/landing/LandingPage.tsx +2 -59
- package/src/lib/backlinks.test.ts +16 -1
- package/src/lib/backlinks.ts +20 -0
- package/src/lib/catalog-diff.test.ts +18 -0
- package/src/lib/catalog-diff.ts +20 -2
- package/src/lib/derive.ts +1 -0
- package/src/lib/django-aggregates.d.mts +9 -0
- package/src/lib/django-aggregates.mjs +36 -0
- package/src/lib/django-aggregates.test.ts +29 -0
- package/src/lib/django-aggregates.ts +5 -0
- package/src/lib/kafka-ui.test.ts +87 -0
- package/src/lib/kafka-ui.ts +105 -0
- package/src/lib/local-api.ts +20 -2
- package/src/lib/setup-info.test.ts +17 -0
- package/src/lib/setup-info.ts +58 -0
- package/src/lib/warnings.test.ts +54 -0
- package/src/lib/warnings.ts +260 -0
- package/src/lib/wire-problems.test.ts +21 -0
- package/src/lib/wire-problems.ts +62 -1
- package/src/likec4/FlowView.tsx +2 -6
- package/src/likec4/flow-edges.test.ts +64 -1
- package/src/likec4/flow-edges.ts +43 -7
- package/src/likec4/view-index.ts +8 -2
- package/src/map/ContextMapGraph.tsx +76 -32
- package/src/merge.test.ts +23 -0
- package/src/merge.ts +33 -10
- package/src/pages/AggregatePage.tsx +8 -7
- package/src/pages/CatalogFailure.tsx +2 -2
- package/src/pages/ContextPage.tsx +6 -5
- package/src/pages/ServicePage.tsx +4 -3
- package/src/pages/Settings.tsx +200 -44
- package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
- package/src/pages/settings/IntegrationsSettings.tsx +117 -0
- package/src/routes.test.ts +2 -0
- package/src/routes.ts +2 -1
- package/src/selection/DetailPanel.tsx +61 -1
- package/src/virtual-provenance.d.ts +11 -0
- package/vite.config.ts +5 -0
- package/scripts/vendor-lock.mjs +0 -58
- package/scripts/vendor-lock.test.mjs +0 -69
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Which HTTP verb a mounted handler answers, read from what the code declares.
|
|
2
|
+
|
|
3
|
+
A URLConf can mount any callable — ``path("planet/fetch", Planet.fetch)`` —
|
|
4
|
+
and then the route table proves the path but says nothing about the verb.
|
|
5
|
+
The verb is still written down somewhere in the handler, in one of the ways
|
|
6
|
+
Django and DRF offer for it, and this module reads them in the order a
|
|
7
|
+
reviewer would trust them:
|
|
8
|
+
|
|
9
|
+
1. a decorator on the handler: DRF's ``@action(methods=…)`` and ``@api_view``,
|
|
10
|
+
Django's ``@require_http_methods([…])``, ``@require_GET``, ``@require_POST``
|
|
11
|
+
and ``@require_safe``, also through ``method_decorator(…)``;
|
|
12
|
+
2. the same decorators on the class, ``@method_decorator(…, name="dispatch")``;
|
|
13
|
+
3. the class's ``http_method_names``;
|
|
14
|
+
4. a branch on ``request.method`` in the handler body;
|
|
15
|
+
5. a project wrapper — a decorator or a function the handler hands ``request``
|
|
16
|
+
to — whose own body does one of the above, followed a bounded number of
|
|
17
|
+
levels deep.
|
|
18
|
+
|
|
19
|
+
The first tier that speaks decides. Nothing here is guessed: a handler none
|
|
20
|
+
of the tiers describes has no verb, and the caller says so rather than
|
|
21
|
+
inventing one.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import ast
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from typing import List, Optional, Set, Tuple
|
|
29
|
+
|
|
30
|
+
from source import Module, Project, const_str, dotted, keyword, methods, node_name
|
|
31
|
+
|
|
32
|
+
HTTP_VERBS = ("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "TRACE")
|
|
33
|
+
|
|
34
|
+
# What a framework answers for every route regardless of the handler; listing
|
|
35
|
+
# them never says what the handler is for, so they are dropped when any other
|
|
36
|
+
# verb is named beside them.
|
|
37
|
+
IMPLICIT = {"HEAD", "OPTIONS", "TRACE"}
|
|
38
|
+
|
|
39
|
+
SHORTHAND = {"require_GET": ("GET",), "require_POST": ("POST",), "require_safe": ("GET",)}
|
|
40
|
+
LISTED = ("require_http_methods", "action", "api_view")
|
|
41
|
+
|
|
42
|
+
# How far a project wrapper is followed before the reader stops. Two levels
|
|
43
|
+
# cover a decorator that wraps a decorator; anything deeper is a project that
|
|
44
|
+
# should declare its verb where the handler is.
|
|
45
|
+
DEPTH = 3
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class Evidence:
|
|
50
|
+
verbs: Tuple[str, ...]
|
|
51
|
+
rule: str # "decorator", "class decorator", "http_method_names", "request.method", "wrapper <name>"
|
|
52
|
+
source: str # file:line of what was read
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Reader:
|
|
56
|
+
def __init__(self, project: Optional[Project]):
|
|
57
|
+
self.project = project
|
|
58
|
+
|
|
59
|
+
# --- the tiers -----------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
def for_handler(self, module: Module, handler: ast.AST, owner: Optional[ast.ClassDef] = None) -> Optional[Evidence]:
|
|
62
|
+
found = self.decorators(module, handler, handler_name=getattr(handler, "name", ""))
|
|
63
|
+
if found is None and owner is not None:
|
|
64
|
+
found = self.decorators(module, owner, handler_name=getattr(handler, "name", ""), rule="class decorator")
|
|
65
|
+
if found is None and owner is not None:
|
|
66
|
+
found = self.method_names(module, owner)
|
|
67
|
+
if found is None:
|
|
68
|
+
found = self.branches(module, handler)
|
|
69
|
+
if found is None:
|
|
70
|
+
found = self.wrappers(module, handler, owner, DEPTH, set())
|
|
71
|
+
return found
|
|
72
|
+
|
|
73
|
+
def decorators(self, module: Module, node: ast.AST, handler_name: str = "", rule: str = "decorator") -> Optional[Evidence]:
|
|
74
|
+
for dec in getattr(node, "decorator_list", []) or []:
|
|
75
|
+
inner = dec
|
|
76
|
+
# ``method_decorator(require_POST)`` on a method, or on the class
|
|
77
|
+
# with ``name=`` saying which method; ``dispatch`` means every one.
|
|
78
|
+
if isinstance(inner, ast.Call) and dotted(inner.func).split(".")[-1] == "method_decorator" and inner.args:
|
|
79
|
+
applies_to = const_str(keyword(inner, "name"))
|
|
80
|
+
if rule == "class decorator" and applies_to not in ("", "dispatch", handler_name):
|
|
81
|
+
continue
|
|
82
|
+
inner = inner.args[0]
|
|
83
|
+
verbs = listed_verbs(inner)
|
|
84
|
+
if verbs:
|
|
85
|
+
return Evidence(verbs, rule, module.where(dec))
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
def method_names(self, module: Module, owner: ast.ClassDef) -> Optional[Evidence]:
|
|
89
|
+
for stmt in owner.body:
|
|
90
|
+
if not isinstance(stmt, (ast.Assign, ast.AnnAssign)):
|
|
91
|
+
continue
|
|
92
|
+
targets = stmt.targets if isinstance(stmt, ast.Assign) else [stmt.target]
|
|
93
|
+
if not any(isinstance(target, ast.Name) and target.id == "http_method_names" for target in targets):
|
|
94
|
+
continue
|
|
95
|
+
verbs = verbs_in(stmt.value)
|
|
96
|
+
if verbs:
|
|
97
|
+
return Evidence(verbs, "http_method_names", module.where(stmt))
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
def branches(self, module: Module, node: ast.AST) -> Optional[Evidence]:
|
|
101
|
+
found: List[str] = []
|
|
102
|
+
where = ""
|
|
103
|
+
for compare in ast.walk(node):
|
|
104
|
+
if not isinstance(compare, ast.Compare):
|
|
105
|
+
continue
|
|
106
|
+
sides = [compare.left] + list(compare.comparators)
|
|
107
|
+
if not any(is_request_method(side) for side in sides):
|
|
108
|
+
continue
|
|
109
|
+
for side in sides:
|
|
110
|
+
for verb in verbs_in(side, filter_implicit=False):
|
|
111
|
+
if verb not in found:
|
|
112
|
+
found.append(verb)
|
|
113
|
+
where = where or module.where(compare)
|
|
114
|
+
verbs = normalise(found)
|
|
115
|
+
return Evidence(verbs, "request.method", where) if verbs else None
|
|
116
|
+
|
|
117
|
+
def wrappers(self, module: Module, handler: ast.AST, owner: Optional[ast.ClassDef], depth: int, seen: Set[Tuple[str, str]]) -> Optional[Evidence]:
|
|
118
|
+
if depth <= 0 or self.project is None:
|
|
119
|
+
return None
|
|
120
|
+
candidates: List[Tuple[Module, ast.AST, Optional[ast.ClassDef], str]] = []
|
|
121
|
+
for dec in getattr(handler, "decorator_list", []) or []:
|
|
122
|
+
target = dec.func if isinstance(dec, ast.Call) else dec
|
|
123
|
+
resolved = self.callable_target(module, owner, target)
|
|
124
|
+
if resolved is not None:
|
|
125
|
+
candidates.append(resolved + (dotted(target),))
|
|
126
|
+
for call in ast.walk(handler):
|
|
127
|
+
if not isinstance(call, ast.Call) or not passes_request(call):
|
|
128
|
+
continue
|
|
129
|
+
resolved = self.callable_target(module, owner, call.func)
|
|
130
|
+
if resolved is not None:
|
|
131
|
+
candidates.append(resolved + (dotted(call.func),))
|
|
132
|
+
for target_module, target, target_owner, name in candidates:
|
|
133
|
+
key = (target_module.dotted, node_name(target) if not target_owner else target_owner.name + "." + node_name(target))
|
|
134
|
+
if key in seen:
|
|
135
|
+
continue
|
|
136
|
+
seen.add(key)
|
|
137
|
+
found = self.decorators(target_module, target) or self.calls(target_module, target) or self.branches(target_module, target)
|
|
138
|
+
if found is None:
|
|
139
|
+
found = self.wrappers(target_module, target, target_owner, depth - 1, seen)
|
|
140
|
+
if found is not None:
|
|
141
|
+
return Evidence(found.verbs, "wrapper %s" % name, found.source)
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
def calls(self, module: Module, node: ast.AST) -> Optional[Evidence]:
|
|
145
|
+
"""``return require_POST(view)`` inside a project decorator: the
|
|
146
|
+
framework decorator applied by hand rather than with ``@``."""
|
|
147
|
+
for call in ast.walk(node):
|
|
148
|
+
if not isinstance(call, ast.Call):
|
|
149
|
+
continue
|
|
150
|
+
for candidate in (call, call.func):
|
|
151
|
+
verbs = listed_verbs(candidate)
|
|
152
|
+
if verbs:
|
|
153
|
+
return Evidence(verbs, "decorator", module.where(call))
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
# --- names ---------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
def callable_target(self, module: Module, owner: Optional[ast.ClassDef], node: ast.AST) -> Optional[Tuple[Module, ast.AST, Optional[ast.ClassDef]]]:
|
|
159
|
+
"""A project function or method this name refers to; nothing for a
|
|
160
|
+
framework name, a builtin or anything the imports do not explain."""
|
|
161
|
+
name = dotted(node)
|
|
162
|
+
if not name or self.project is None:
|
|
163
|
+
return None
|
|
164
|
+
parts = name.split(".")
|
|
165
|
+
if len(parts) == 2 and parts[0] in ("self", "cls") and owner is not None:
|
|
166
|
+
for item in methods(owner):
|
|
167
|
+
if item.name == parts[1]:
|
|
168
|
+
return module, item, owner
|
|
169
|
+
return None
|
|
170
|
+
if len(parts) == 1:
|
|
171
|
+
hit = self.project.resolve(module, name)
|
|
172
|
+
if hit is None:
|
|
173
|
+
return None
|
|
174
|
+
target = function_in(hit[0], hit[1])
|
|
175
|
+
return (hit[0], target, None) if target is not None else None
|
|
176
|
+
imported = module.imports.get(parts[0])
|
|
177
|
+
if imported is None:
|
|
178
|
+
return None
|
|
179
|
+
target_module = self.project.module(".".join([imported.module] + ([] if imported.name == "*" else [imported.name]) + parts[1:-1]))
|
|
180
|
+
if target_module is None:
|
|
181
|
+
return None
|
|
182
|
+
target = function_in(target_module, parts[-1])
|
|
183
|
+
return (target_module, target, None) if target is not None else None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def function_in(module: Module, name: str) -> Optional[ast.AST]:
|
|
187
|
+
for node in module.tree.body:
|
|
188
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == name:
|
|
189
|
+
return node
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def listed_verbs(node: ast.AST) -> Tuple[str, ...]:
|
|
194
|
+
"""The verbs a framework decorator names, however it was imported:
|
|
195
|
+
``@require_http_methods(["GET", "POST"])``, ``@action(methods=["post"])``,
|
|
196
|
+
``@api_view(["GET"])``, or the ``require_POST`` shorthand."""
|
|
197
|
+
name = dotted(node.func if isinstance(node, ast.Call) else node).split(".")[-1]
|
|
198
|
+
if name in SHORTHAND:
|
|
199
|
+
return SHORTHAND[name]
|
|
200
|
+
if not isinstance(node, ast.Call) or name not in LISTED:
|
|
201
|
+
return ()
|
|
202
|
+
listed = keyword(node, "methods") or keyword(node, "request_method_list") or keyword(node, "http_method_names")
|
|
203
|
+
if listed is None and node.args:
|
|
204
|
+
listed = node.args[0]
|
|
205
|
+
if listed is None and name in ("action", "api_view"):
|
|
206
|
+
# DRF's documented default when the decorator names no methods.
|
|
207
|
+
return ("GET",)
|
|
208
|
+
return verbs_in(listed)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def verbs_in(node: Optional[ast.AST], filter_implicit: bool = True) -> Tuple[str, ...]:
|
|
212
|
+
"""The HTTP verbs a literal spells, upper-cased; anything that is not one
|
|
213
|
+
is left out, so ``["get", "frobnicate"]`` reads as GET alone."""
|
|
214
|
+
if node is None:
|
|
215
|
+
return ()
|
|
216
|
+
items = node.elts if isinstance(node, (ast.List, ast.Tuple, ast.Set)) else [node]
|
|
217
|
+
found = [const_str(item).upper() for item in items if const_str(item)]
|
|
218
|
+
return normalise(found) if filter_implicit else tuple(verb for verb in found if verb in HTTP_VERBS)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def normalise(found: List[str]) -> Tuple[str, ...]:
|
|
222
|
+
verbs = [verb for verb in found if verb in HTTP_VERBS]
|
|
223
|
+
explicit = [verb for verb in verbs if verb not in IMPLICIT]
|
|
224
|
+
out: List[str] = []
|
|
225
|
+
for verb in explicit or verbs:
|
|
226
|
+
if verb not in out:
|
|
227
|
+
out.append(verb)
|
|
228
|
+
return tuple(out)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def is_request_method(node: ast.AST) -> bool:
|
|
232
|
+
"""``request.method``, ``self.request.method``, or either through
|
|
233
|
+
``.upper()``/``.lower()``."""
|
|
234
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr in ("upper", "lower") and not node.args:
|
|
235
|
+
node = node.func.value
|
|
236
|
+
return dotted(node).endswith("request.method")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def passes_request(call: ast.Call) -> bool:
|
|
240
|
+
values = list(call.args) + [kw.value for kw in call.keywords]
|
|
241
|
+
return any(dotted(value) in ("request", "self.request") for value in values)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Go domain and execution-flow extraction
|
|
2
|
+
|
|
3
|
+
The syntax extractor reads aggregates, events and use cases in the supported
|
|
4
|
+
DDD layouts. It also discovers registered HTTP execution roots without requiring
|
|
5
|
+
an aggregate, a `UseCase.Handle` method, an `internal` directory or a `scope`.
|
|
6
|
+
|
|
7
|
+
For conventional services, supported registrations include `net/http.HandleFunc`,
|
|
8
|
+
`ServeMux.HandleFunc`, and `ServeMux.Handle` with a `http.HandlerFunc` conversion.
|
|
9
|
+
The handler can be a function or a same-package method on an explicitly typed
|
|
10
|
+
parameter, receiver or local composite literal. Router verb registrations use
|
|
11
|
+
recognized router imports/types; the existing `http` package convention remains
|
|
12
|
+
available for syntax-only legacy fixtures. Concrete service methods and their
|
|
13
|
+
repository calls are followed by the existing flow walker. An unregistered
|
|
14
|
+
method does not become an HTTP root just because it resembles a handler.
|
|
15
|
+
|
|
16
|
+
`scope` still limits ownership to `internal/<scope>` in a shared module. Imported
|
|
17
|
+
local application methods can be followed across that boundary. Nested modules,
|
|
18
|
+
vendor, node_modules and testdata are excluded from package discovery. When a
|
|
19
|
+
DDD flow already covers the same handler, its richer domain flow is retained.
|
|
20
|
+
|
|
21
|
+
Assembly must not select the first of several use cases. Competing providers,
|
|
22
|
+
multiple adapter targets and unreadable multi-use-case providers leave the call
|
|
23
|
+
`unresolved`, with an `ambiguous-binding` warning and the candidate use cases.
|
|
24
|
+
A per-method adapter binding is retained only when all candidate providers agree.
|
|
25
|
+
|
|
26
|
+
This extractor remains syntax-only and runs in WASI. The shared native typed
|
|
27
|
+
analysis API lives in `internal/gocall`; its first consumer is the HTTP client
|
|
28
|
+
sidecar. Adopting that API in additional extractors is a separate migration,
|
|
29
|
+
without imposing a Go toolchain on this syntax path.
|
|
30
|
+
|
|
31
|
+
Go-domain, HTTP analysis and Go SQL readers now share `internal/goscan` for
|
|
32
|
+
source selection, imports, constants and package lookup. Each invocation reuses
|
|
33
|
+
its parsed AST across passes. Generated declarations remain available for
|
|
34
|
+
contract lookup but are marked separately. The index respects GOOS/GOARCH,
|
|
35
|
+
build constraints and nested module boundaries; it is not a cache shared across
|
|
36
|
+
plugin processes. Native type analysis still uses the Go package loader.
|
|
37
|
+
|
|
38
|
+
Flow steps carry structured `evidence`: source expressions, enclosing functions,
|
|
39
|
+
provider signatures and unresolved candidates. Provider signatures are marked as
|
|
40
|
+
inference, and static paths do not claim that a call ran. The detail panel also
|
|
41
|
+
explains older fragments from their recorded source and contract references.
|
|
42
|
+
|
|
43
|
+
For a service with plain SQL migrations, combine `go-domain` and `sql` with the
|
|
44
|
+
same context, service and store. SQL discovers `migrations`/`migration` directories
|
|
45
|
+
outside `internal` too. Without a matching domain root or an explicit
|
|
46
|
+
`-- aggregate: <id>` annotation, tables and views remain independent of aggregates;
|
|
47
|
+
their schema, foreign keys and SQL accesses are retained.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
package extracthttpclients
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
_ "embed"
|
|
5
|
+
|
|
6
|
+
"github.com/shortlink-org/portolan/plugin"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
//go:embed options.schema.json
|
|
10
|
+
var optionsSchema []byte
|
|
11
|
+
|
|
12
|
+
func descriptor() plugin.Descriptor {
|
|
13
|
+
return plugin.Descriptor{
|
|
14
|
+
Name: "extract-http-clients",
|
|
15
|
+
Summary: "Reads outbound HTTP/SOAP calls and joins Go routes, handlers, factories, and providers into source-backed flows.",
|
|
16
|
+
Phases: []string{plugin.PhaseExtract},
|
|
17
|
+
Options: optionsSchema,
|
|
18
|
+
}
|
|
19
|
+
}
|