Skip to content

Commit 341a3f8

Browse files
ryanyuansean-
andauthored
feat: add osapifix, a version-hop migration tool for opensearch-go v4->v5 (#933)
* chore: baseline osupgrade-v4-to-v5 tool before generic refactor Snapshot the working v4->v5 migration tool (rewrite + vet passes, surface diffing, hand-authored type/call tables) as a diffable baseline. Subsequent commits generalize this into a multi-version 'osupgrade' with a transition registry, source auto-detection, and cross-hop composition. Excludes the built binary (now gitignored). Signed-off-by: Ryan Yuan <ryan.yuan@crowdstrike.com> * feat: add osupgrade, a multi-version opensearch-go migration tool osupgrade migrates a Go module across opensearch-go major versions. It is a generic frontend over a registry of per-adjacent-hop migration tables: it auto-detects the source major from the consumer's imports, defaults the target to the newest known version, and composes the tables across the src->dst chain. v4->v5 ships as the one fully worked hop; adding v3->v4 / v2->v3 is additive (a surface file, a hop file, and a registry entry) with no engine changes. Why a registry instead of one binary per transition: the rewrite engine, surface diffing, and CLI are all version-agnostic, so only the hand-authored hop DATA is migration-specific. A single tool that chains hops covers multi-version jumps without duplicating the machinery or accreting version flags. Two passes, because a major bump has two distinct kinds of change: - rewrite (syntactic, pre-compile): source-shaped code does not compile against the target, so a type-checking pass cannot load it. rewrite edits the AST from a composed, type-aware delta to get the module compiling against the into any-typed testify sinks and fail at run time; go/analysis analyzers catch these, and -fix rewrites the safe cases. Runs after build; targets one version. Design: - transitions.go: version-neutral types (Major, Hop, methodRegroup) plus the surfaces and hops registries. - hop_v4_to_v5.go: the worked hop (type renames, method regroups, removed helpers, semantic followups). - compose.go: resolve(src,dst) chains hops in [src,dst). The field delta is an endpoint diff of the source/target surfaces (intermediate versions never matter to fields); only type renames and call-site rules are folded across hops as ORDERED composition, with a cross-talk guard against a later hop reusing a source-version type name. - detect.go: source major read from .go import paths, robust to go.mod listing both majors mid-migration or already naming the target. - applydelta.go / internal/surface: the version-agnostic type-aware rewriter and surface model; engine consumes only composed inputs, no version-specific globals. Identifiers are From/To throughout. Usage (v4 -> v5): osupgrade rewrite -w ./... go get github.com/opensearch-project/opensearch-go/v5 && go build ./... osupgrade vet -fix ./... Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com> Signed-off-by: Ryan Yuan <ryan.yuan@crowdstrike.com> * feat: add cpu/dfs to osgen acronym table Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com> Signed-off-by: Ryan Yuan <ryan.yuan@crowdstrike.com> * chore: regenerate opensearchapi for CPU/DFS casing Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com> Signed-off-by: Ryan Yuan <ryan.yuan@crowdstrike.com> * build: run osapifix module in lint.local and test-unit Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com> Signed-off-by: Ryan Yuan <ryan.yuan@crowdstrike.com> * docs: document osapifix upgrade tool Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com> Signed-off-by: Ryan Yuan <ryan.yuan@crowdstrike.com> * docs: add osapifix CHANGELOG entry and format README Signed-off-by: Ryan Yuan <ryan.yuan@crowdstrike.com> * test(osapifix): rename typedassert testdata to src/v4/test1.go Signed-off-by: Ryan Yuan <ryan.yuan@crowdstrike.com> --------- Signed-off-by: Ryan Yuan <ryan.yuan@crowdstrike.com> Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com> Co-authored-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
1 parent 5bfd1d9 commit 341a3f8

39 files changed

Lines changed: 97813 additions & 57 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
66

77
### Added
88

9+
- Add `cmd/osapifix`, a tool that migrates a Go module across opensearch-go major versions. `osapifix rewrite -w ./...` detects the source major from the module's imports and rewrites old-major API shapes (type renames, field dispositions, method regroups) into the target's; the pass is purely syntactic (go/parser + astutil + go/printer), so it runs before the code compiles against the target, and writes are sandboxed to the target module directory via `os.Root`. `osapifix vet -fix ./...` then runs go/analysis analyzers that catch runtime type-assertion hazards -- the target's precise `*int64`/`*string` types flowing into `any` sinks such as testify's `Equal`, which compile cleanly but panic at run time. Each adjacent version transition is a hand-authored `Hop` keyed against two committed API surfaces (`surface_vN.json`); a migration resolves to the ordered list of hops between source and target, and new hops (v3 -> v4, v2 -> v3) are added as data without engine changes. Ships the v4 -> v5 hop. See `cmd/osapifix/README.md` ([#933](https://github.com/opensearch-project/opensearch-go/pull/933))
910
- Add `Close()` to `opensearch.Client` and `opensearchapi.Client` for explicit teardown of background goroutines (node discovery, health/stats pollers, DNS refresh) and idle connections, without type-asserting the transport. Cache implicitly-constructed default clients (`opensearch.NewDefaultClient`, `opensearchapi.NewDefaultClient`, and the client `opensearchutil.NewBulkIndexer` builds when none is supplied) in a process-wide, refcounted, idle-TTL cache keyed by config hash, so identical default clients share one transport instead of leaking one set of goroutines and its connection pool per construction. User-built `opensearch.NewClient`/`opensearchapi.NewClient` clients never enter the cache. `opensearchutil.NewBulkIndexer` now closes the client it implicitly creates when the indexer is closed. Tune the idle eviction window with `OPENSEARCH_GO_DEFAULT_CLIENT_TTL` (default `16m`; `0` = never evict; a negative value disables caching so every call builds a fresh client) ([#893](https://github.com/opensearch-project/opensearch-go/issues/893))
1011
- Add client-side DNS caching, enabled by default on the built-in transport. Resolved addresses are cached and re-resolved on an interval (default 60s, mirroring the TTL AWS publishes for managed OpenSearch Service endpoints). When the resolver becomes briefly unreachable, the last-known-good address continues to be served until the resolver recovers, so transient resolver outages (e.g. a node-local DNS blip producing `dial tcp: lookup ...: i/o timeout`) no longer fail requests for already-resolved hosts. Tune or disable via the `DNSCacheRefresh`, `DNSDialTimeout`, `DNSKeepAlive`, and `DNSTimeout` fields on `opensearch.Config` (or `OPENSEARCH_GO_DNS_CACHE_REFRESH`, `OPENSEARCH_GO_DNS_DIAL_TIMEOUT`, `OPENSEARCH_GO_DNS_KEEP_ALIVE`, `OPENSEARCH_GO_DNS_TIMEOUT`); each follows the 0 = default, <0 = disable, >0 = explicit convention. Caching is installed only when no custom `Transport` is supplied; a caller-provided `Transport` is never modified. A host that resolves to multiple addresses races up to three of them concurrently (random start offset per connection) and takes the first to connect, spreading load and tolerating a dead address. Refresh re-resolves cached hosts sequentially, so `DNSTimeout` (default 10s) bounds each lookup to keep one hung resolution from stalling a refresh tick. The refresh goroutine is bound to the client's root context, so it is reclaimed both when `Close` is called and when `New` returns an error after the context is created. Because Go's resolver does not expose record TTLs, the refresh interval is a re-resolution cadence, not a per-record TTL. Exposes `DNSLookups`, `DNSCacheMisses`, and `DNSLookupErrors` counters via `Transport.Metrics()`
1112
- `cmd/osgen`: guard `json.RawMessage` in generated request/response types behind a checked-in allowlist (`cmd/osgen/rawmessage_allowlist.txt`). Because a `json.RawMessage` is the symptom of a type the generator could not resolve, a generator bug can silently widen the raw-JSON surface of the public API; generation now fails (non-zero exit) when any `json.RawMessage` use is not listed, including nested forms such as `[]json.RawMessage`, `map[string]json.RawMessage`, and `[][]json.RawMessage` (the leaf is detected at any wrapper depth). Entries are keyed `GoTypeName/jsonFieldName` (whole-response raw bodies use `<Prefix>Resp/-`, and map/array responses whose element type is unresolved use `<Prefix>Resp/[entries]` and `<Prefix>Resp/[records]`). Add `-update-raw-message-allowlist` to regenerate the allowlist from current output (sorted and grouped for minimal diffs), and `-allow-unlisted-raw-message` to downgrade the check to a warning ([#890](https://github.com/opensearch-project/opensearch-go/pull/890))

Makefile

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -55,26 +55,33 @@ format: ## Format all Go files with goimports
5555
goimports -w .;
5656

5757
##@ Testing
58-
test-unit: ## Run unit tests across all modules (root + cmd/osgen)
58+
test-unit: ## Run unit tests across all modules (root + cmd/osgen + cmd/osapifix)
5959
@printf "\033[2m-> Running unit tests...\033[0m\n"
6060
ifdef race
6161
$(eval testunitargs += "-race")
6262
$(eval testosgenargs += "-race")
63+
$(eval testosapifixargs += "-race")
6364
endif
6465
$(eval testunitargs += "-cover" "./..." "-args" "-test.gocoverdir=$(PWD)/tmp/unit")
6566
$(eval testosgenargs += "-cover" "./..." "-args" "-test.gocoverdir=$(PWD)/tmp/osgen")
66-
@rm -rf $(PWD)/tmp/unit $(PWD)/tmp/osgen
67-
@mkdir -p $(PWD)/tmp/unit $(PWD)/tmp/osgen
67+
$(eval testosapifixargs += "-cover" "./..." "-args" "-test.gocoverdir=$(PWD)/tmp/osapifix")
68+
@rm -rf $(PWD)/tmp/unit $(PWD)/tmp/osgen $(PWD)/tmp/osapifix
69+
@mkdir -p $(PWD)/tmp/unit $(PWD)/tmp/osgen $(PWD)/tmp/osapifix
6870
@echo "go test -v" $(testunitargs); \
6971
go test -v $(testunitargs) 2>&1 | tee test-unit.log; \
7072
exit $${PIPESTATUS[0]};
7173
@printf "\033[2m-> Running cmd/osgen unit tests (separate module)...\033[0m\n"
7274
@echo "(cd cmd/osgen && go test -v" $(testosgenargs) ")"; \
7375
cd cmd/osgen && go test -v $(testosgenargs) 2>&1 | tee $(PWD)/test-osgen.log; \
7476
exit $${PIPESTATUS[0]};
77+
@printf "\033[2m-> Running cmd/osapifix unit tests (separate module)...\033[0m\n"
78+
@echo "(cd cmd/osapifix && go test -v" $(testosapifixargs) ")"; \
79+
cd cmd/osapifix && go test -v $(testosapifixargs) 2>&1 | tee $(PWD)/test-osapifix.log; \
80+
exit $${PIPESTATUS[0]};
7581
ifdef coverage
7682
@go tool covdata textfmt -i=$(PWD)/tmp/unit -o $(PWD)/tmp/unit.cov
7783
@go tool covdata textfmt -i=$(PWD)/tmp/osgen -o $(PWD)/tmp/osgen.cov
84+
@go tool covdata textfmt -i=$(PWD)/tmp/osapifix -o $(PWD)/tmp/osapifix.cov
7885
endif
7986
test: test-unit
8087

@@ -259,6 +266,8 @@ lint.local: ## Run lint locally (not in Docker) across all build-tag combinatio
259266
done
260267
@printf "\033[2m-> Running golangci-lint in cmd/osgen (separate Go module)...\033[0m\n"
261268
cd cmd/osgen && golangci-lint run --fix --build-tags $(GOLANGCI_LINT_BUILD_TAGS) --timeout=5m -v ./...
269+
@printf "\033[2m-> Running golangci-lint in cmd/osapifix (separate Go module)...\033[0m\n"
270+
cd cmd/osapifix && golangci-lint run --fix --build-tags $(GOLANGCI_LINT_BUILD_TAGS) --timeout=5m -v ./...
262271

263272
package := "prettier"
264273
PRETTIER_ARGS := --prose-wrap never --print-width 300 "**/*.md"
@@ -373,27 +382,23 @@ cluster.runtime: ## Show detected container runtime
373382
@$(CTR) --version
374383
@$(CTR) compose version
375384

376-
cluster.sysctl: ## Ensure vm.max_map_count is set for OpenSearch
377-
@current=$$( \
378-
if [ "$$(uname)" = "Darwin" ]; then \
379-
if command -v rdctl >/dev/null 2>&1; then \
380-
rdctl shell cat /proc/sys/vm/max_map_count 2>/dev/null || echo 0; \
381-
else \
382-
echo 0; \
383-
fi; \
384-
else \
385-
cat /proc/sys/vm/max_map_count 2>/dev/null || echo 0; \
386-
fi \
387-
); \
385+
cluster.sysctl: ## Ensure vm.max_map_count is set for OpenSearch (Linux, or macOS via Colima/Rancher/Docker)
386+
@if [ "$$(uname)" != "Darwin" ]; then \
387+
vmexec=""; setter="sudo sysctl -w vm.max_map_count=262144"; \
388+
elif command -v colima >/dev/null 2>&1 && colima status >/dev/null 2>&1; then \
389+
vmexec="colima ssh --"; setter="colima ssh -- sudo sysctl -w vm.max_map_count=262144"; \
390+
elif command -v rdctl >/dev/null 2>&1 && rdctl shell true >/dev/null 2>&1; then \
391+
vmexec="rdctl shell"; setter="rdctl shell sudo sysctl -w vm.max_map_count=262144"; \
392+
else \
393+
vmexec="$(CTR) run --rm --privileged --net=host busybox"; \
394+
setter="$(CTR) run --rm --privileged --net=host busybox sysctl -w vm.max_map_count=262144"; \
395+
fi; \
396+
current=$$($$vmexec cat /proc/sys/vm/max_map_count 2>/dev/null || echo 0); \
388397
if [ "$$current" -ge 262144 ]; then \
389398
printf "\033[2m-> vm.max_map_count already $$current (>= 262144)\033[0m\n"; \
390399
else \
391400
printf "\033[2m-> Setting vm.max_map_count=262144 (was $$current)...\033[0m\n"; \
392-
if [ "$$(uname)" = "Darwin" ]; then \
393-
rdctl shell sudo sysctl -w vm.max_map_count=262144; \
394-
else \
395-
sudo sysctl -w vm.max_map_count=262144; \
396-
fi; \
401+
$$setter; \
397402
fi
398403

399404
cluster.build: ## Build OpenSearch Docker images (version-aware)

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@ OpenSearch Go Client
1616

1717
The client supports automatic node discovery, request-based connection routing, and role-aware node selection. See the [User Guide](USER_GUIDE.md) and [guides](guides/) for usage examples and configuration options.
1818

19+
Upgrading across a major version? The [`osapifix`](cmd/osapifix/README.md) tool automates most of the API-shape changes (type, method, and field renames) - see [UPGRADING_V4_TO_V5.md](opensearchapi/UPGRADING_V4_TO_V5.md).
20+
1921
## Project Resources
2022

2123
- [Project Website](https://opensearch.org/)
2224
- [Developer Guide](DEVELOPER_GUIDE.md)
2325
- [User Guide](USER_GUIDE.md)
26+
- [Upgrade Tool (`osapifix`)](cmd/osapifix/README.md)
2427
- [Documentation](https://opensearch.org/docs/latest/clients/go/)
2528
- [API Documentation](https://pkg.go.dev/github.com/opensearch-project/opensearch-go/v5)
2629
- Need help? Try [Forums](https://discuss.opendistrocommunity.dev/c/clients/)

UPGRADING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ This is the version-history index for the OpenSearch Go client. Each entry below
1313

1414
Upgrading from the hand-written v4 `opensearchapi/` package to the code-generated v5 surface is the largest single change in v5. [`UPGRADING_V5.md`](UPGRADING_V5.md) summarizes it; for the field-level delta (every rename, the `*Params` change, embedded `TimeoutParams`/`DebugParams`, `BulkResp.Items` becoming `[]BulkItem`) and the optional forward-compatible `replace` directive, see the deep-dive at [`opensearchapi/UPGRADING_V4_TO_V5.md`](opensearchapi/UPGRADING_V4_TO_V5.md).
1515

16+
The [`osapifix`](cmd/osapifix/README.md) tool automates most of this delta (import bump, type/method/field renames, value-to-pointer adjustments); see the [Automated migration](opensearchapi/UPGRADING_V4_TO_V5.md#automated-migration) section.
17+
1618
## Related references
1719

1820
- [`COMPATIBILITY.md`](COMPATIBILITY.md) - client/server version support matrix.

UPGRADING_V5.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ The recommended call-site pattern is a `for`/`switch` over `opensearchapi.Errors
3535
- [`opensearchapi/README.md`](opensearchapi/README.md) - full v5 usage guide for these errors, including the type-switch pattern and the rationale for preferring it over `errors.As`/`Has`.
3636
- [`guides/usage-error_handling.md`](guides/usage-error_handling.md) - cross-version best-practices guide with v4 and v5 examples side-by-side.
3737
- [`opensearchapi/UPGRADING_V4_TO_V5.md`](opensearchapi/UPGRADING_V4_TO_V5.md) - v4 -> v5 surface delta.
38+
- [`cmd/osapifix/README.md`](cmd/osapifix/README.md) - the tool that automates most of the v4 -> v5 surface delta.
3839

3940
**Error types in v4 `opensearchapi/`** (the upgrade source):
4041

cmd/osapifix/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/osupgrade

cmd/osapifix/README.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# osapifix
2+
3+
`osapifix` migrates a Go module across opensearch-go major versions. The source major is detected from the module's imports and the target defaults to the newest supported version, so the common invocation is:
4+
5+
```sh
6+
osapifix rewrite -w ./...
7+
```
8+
9+
Today it supports the v4 -> v5 hop. Additional hops (v3 -> v4, v2 -> v3) are added as data, without engine changes.
10+
11+
## Install
12+
13+
`osapifix` is a separate Go module. Build the binary from an `opensearch-go` checkout:
14+
15+
```sh
16+
git clone https://github.com/opensearch-project/opensearch-go
17+
(cd opensearch-go/cmd/osapifix && go build -o "$(go env GOPATH)/bin/osapifix" .)
18+
```
19+
20+
The examples below assume the resulting `osapifix` binary is on your `PATH`.
21+
22+
## Subcommands
23+
24+
### `rewrite` - API-shape migration (pre-compile)
25+
26+
Rewrites source that uses the old major's API shapes (type names, method paths, field spellings) into the target's. It is purely syntactic (go/parser + astutil + go/printer) so it runs before the code compiles against the target.
27+
28+
```sh
29+
osapifix rewrite [-src=auto] [-dst=vN] [-w] [dir]
30+
```
31+
32+
- `-src` - source major (`v4`), or `auto` (default) to detect from imports.
33+
- `-dst` - target major (`v5`), defaults to the newest supported version.
34+
- `-w` - apply changes. Omitted, `rewrite` is a dry run that prints the edits.
35+
- `dir` - module directory (default `.`). A `./...` pattern is accepted and resolved to its base directory.
36+
37+
Writes are sandboxed to the target module directory via `os.Root`.
38+
39+
### `vet` - runtime-hazard cleanup (post-compile)
40+
41+
The target's precise types (`*int64`, `*string`, ...) flow into `any` sinks such as testify's `Equal`/`Greater`, compiling cleanly but failing at run time with "Elements should be the same type". `vet` runs go/analysis analyzers (`typedassert.go`) that catch these; `-fix` applies the safe rewrites. Run it after `rewrite` and a successful build.
42+
43+
```sh
44+
osapifix vet [-fix] ./...
45+
```
46+
47+
## Typical flow (v4 -> v5)
48+
49+
```sh
50+
osapifix rewrite -w ./...
51+
go get github.com/opensearch-project/opensearch-go/v5 && go build ./...
52+
osapifix vet -fix ./...
53+
```
54+
55+
## How it works
56+
57+
Each adjacent transition (vN -> vN+1) is a `Hop`: hand-authored tables of type renames, field dispositions, method regroups, removed helpers, and semantic followups, keyed against two committed API surfaces (`surface_vN.json`). A migration request resolves to the ordered list of hops between source and target, applied one at a time - rewrite, rebuild against the intermediate version so the type-aware pass can load, then the next hop. Intermediate versions are not surfaced to the operator.
58+
59+
| File | Responsibility |
60+
| ------------------ | ----------------------------------------------------------------------------- |
61+
| `transitions.go` | Version-neutral types (`Major`, `Hop`) and the `surfaces` / `hops` registries |
62+
| `hop_vN_to_vN1.go` | Hand-authored migration data for one hop |
63+
| `plan.go` | `planChain(src, dst)` -> the ordered per-hop plans |
64+
| `detect.go` | Source major from the module's imports |
65+
| `applydelta.go` | Type-aware AST rewriter |
66+
| `internal/surface` | Surface model and `DeriveDelta` between two surfaces |
67+
| `cmd/gensurface` | Generates a version's committed surface JSON |
68+
69+
### Field dispositions
70+
71+
A field that vanishes on the target is governed by an explicit `FieldDisposition`, matched by (source pkg + type + field):
72+
73+
- **rename** - rewrite to the target field. The target type is stated explicitly, so a field may move across a type rename (e.g. `DocumentGetReq#DocumentID` -> `GetReq#ID`).
74+
- **remove** - drop the composite-literal key.
75+
- **manual** - the field's data relocated (e.g. a response collapsed to a raw `Body`); flagged for a human.
76+
77+
A vanished field with no disposition fails the run with an `osapifix bug` error; the tool does not infer rename-versus-remove. Dispositions are verified against the surfaces by `TestHopFieldDispositionsAgainstSurfaces` and are established from source: response-field renames by a shared JSON wire tag, request-field renames by the v4 code that assembles the field into the spec-named element.
78+
79+
### Source detection
80+
81+
The major version is read from import paths (`.../opensearch-go/v4/...`), not `go.mod`: a partially migrated module may `require` both majors, and `go.mod` may name the target while call sites are still source-shaped. A module importing multiple majors migrates from the lowest; the rest are reported.
82+
83+
## Adding a hop (e.g. v3 -> v4)
84+
85+
1. Generate both endpoint surfaces with `cmd/gensurface`:
86+
87+
```sh
88+
go run ./cmd/gensurface -dir <v3-module-dir> -version v3 \
89+
-patterns ./opensearchapi,.,./opensearchtransport -out surface_v3.json
90+
```
91+
92+
2. Embed each surface (`//go:embed`) in `main.go` and register it in the `surfaces` map (`transitions.go`).
93+
94+
3. Author `hop_v3_to_v4.go`: diff the surfaces and rule on every changed type, field, and method. Follow `hop_v4_to_v5.go`.
95+
96+
4. Register the hop in the `hops` map (`transitions.go`).
97+
98+
5. Add `hop_v3_to_v4_test.go` for version-specific facts. The drift guards validate the tables against the surfaces automatically; a `rewrite` against real v3 code fails loudly on any unruled field.
99+
100+
## Testing
101+
102+
```sh
103+
go test ./...
104+
```
105+
106+
| File | Covers |
107+
| ----------------------------------------- | --------------------------------------------------------------------------------- |
108+
| `plan_test.go` | `planChain` and `DeriveDelta` field dispositions, via synthetic v7/v8/v9 surfaces |
109+
| `delta_test.go` | Drift guards over every hop's type renames and field dispositions |
110+
| `hop_v4_to_v5_test.go` | v4 -> v5 version-specific facts |
111+
| `detect_test.go` | Source detection, version parsing, directory resolution |
112+
| `internal/surface/delta_internal_test.go` | Surface diffing internals |
113+
114+
## Limitations
115+
116+
- `vet` analyzers are v5-specific (`TypedAssertAnalyzer`) and target a single version; they do not chain across hops.
117+
- A module importing multiple majors migrates from the lowest; per-import-site source selection is not implemented.

0 commit comments

Comments
 (0)