Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,20 @@ export default defineConfig({
{ label: "OpenTelemetry", slug: "docs/integrations/opentelemetry" },
],
},
{
label: "Go CLI",
collapsed: true,
items: [
{ label: "Overview", slug: "docs/go" },
{ label: "Quickstart", slug: "docs/go/quickstart" },
{ label: "scan", slug: "docs/go/cli/scan" },
{ label: "audit", slug: "docs/go/cli/audit" },
{ label: "validate", slug: "docs/go/cli/validate" },
{ label: "Identity Model", slug: "docs/go/concepts/identity-model" },
{ label: "Supported Scope", slug: "docs/go/reference/supported-scope" },
{ label: "Limitations", slug: "docs/go/reference/limitations" },
],
},
{
label: "Trust",
collapsed: true,
Expand Down
7 changes: 5 additions & 2 deletions src/content/docs/docs/concepts/safety-model.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
---
title: Safety Model
description: What FlagLint auto-rewrites, what it skips, and why the safety-over-coverage principle guides every pattern.
lastUpdated: 2026-06-22
lastUpdated: 2026-07-06
---

This page covers **flaglint-js**'s `migrate --apply` rewrite safety model, which is specific to the JavaScript/TypeScript CLI — flaglint-go does not have an automated migration/rewrite command (audit, scan, and validate only). See [flaglint-go's concepts docs](/docs/go/concepts/identity-model/) for the Go CLI's own safety guarantees.

FlagLint is conservative by design. It separates inventory, review, and source edits so teams can inspect every migration change before it lands in production.

## Safety Model Diagram
Expand Down Expand Up @@ -45,7 +47,8 @@ When every condition is met, FlagLint produces an idempotent, argument-order-cor
| **Wrappers not configured in `.flaglintrc`** | FlagLint cannot distinguish a custom wrapper from an unrelated function unless it is explicitly declared | Add wrapper declarations to `wrappers: []` in `.flaglintrc`, then re-run |
| **Ambiguous fallback types** — e.g. `null`, a variable, or a union-typed expression | FlagLint cannot determine which `get*Value` method to use without a concrete literal type | Replace the fallback with an explicit literal (`false`, `""`, `0`, `{}`) at the call site |
| **Non-LaunchDarkly providers** | FlagLint is a LaunchDarkly-specific static analysis tool | No action; these calls are reported in the inventory but never touched |
| **Non-Node.js SDKs** — browser SDK, Go SDK, Java SDK | Only the Node.js server-side SDK import provenance is verified | Migrate browser and other-language calls manually |
| **Non-Node.js SDKs** — browser SDK, Java SDK | Only the Node.js server-side SDK import provenance is verified | Migrate browser and other-language calls manually |
| **Go SDK** | flaglint-js only verifies the Node.js server-side SDK import — Go usage is out of scope for this tool entirely | Use [flaglint-go](/docs/go/) to audit Go usage; it has no `--apply`-style rewrite yet, only `scan`/`audit`/`validate` |

## Why Fallback Value Does Not Equal Production Value

Expand Down
84 changes: 84 additions & 0 deletions src/content/docs/docs/go/cli/audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
title: flaglint-go audit
description: Risk-ranked LaunchDarkly Go SDK usage report with a migration-readiness score.
lastUpdated: 2026-07-06
---

`flaglint-go audit` scans your source and classifies every detected LaunchDarkly Go SDK call by risk level, producing a migration-readiness score. It never modifies files.

## Command

```bash
flaglint-go audit [dir] [flags]
```

## Options

| Flag | Description |
| --- | --- |
| `-f, --format string` | Output format: `json` \| `markdown` (default `markdown`) |
| `-o, --output string` | Write report to a file instead of stdout |
| `--config string` | Path to config file |
| `--write-baseline string` | Write current finding fingerprints to a baseline file for use with `validate --baseline --fail-on-new` |

## Risk Levels

| Method | Risk | Notes |
|---|---|---|
| `BoolVariation` / `StringVariation` / `IntVariation` / `Float64Variation` (and their `*Ctx` forms) | Low | Static key only |
| `JSONVariation` (and `*Ctx`) | Medium | Pointer output, manual review |
| `*VariationDetail(Ctx)` methods | High | Returns `EvaluationDetail` — no direct OpenFeature equivalent |
| `AllFlagsState` | High | Bulk call, no single-flag equivalent |
| Dynamic flag key (identifier, `fmt.Sprintf`, string concatenation) | High | Cannot resolve statically — overrides the method's own risk level |

## Example Output

```text
Scan complete — 2 unique flag(s) across 2 call site(s) (0ms, 2 file(s))
Migration readiness: 100/100 · ready
2 low risk · 0 medium risk · 0 high risk
```

A codebase with genuine high-risk usage:

```text
Scan complete — 0 unique flag(s) across 4 call site(s) (2.3s, 4512 file(s))
Migration readiness: 0/100 · complex
0 low risk · 0 medium risk · 4 high risk
```

(All four call sites use a dynamic, per-instance flag key — correctly classified high risk regardless of the underlying method, per the dynamic-key rule above.)

## Migration Readiness

The score is the ratio of low/medium-risk (safely reviewable) call sites to total detected call sites, expressed 0–100. `N/A` is reported when zero direct calls are found — there's nothing to score.

## Baseline Mode (`--write-baseline`)

```bash
flaglint-go audit ./services --write-baseline .flaglint-baseline.json
```

```json
{
"version": "1",
"createdAt": "2026-07-06T19:23:09Z",
"flaglintVersion": "dev",
"fingerprints": [
"launchdarkly:BoolVariation:checkout-v2:checkout.go",
"launchdarkly:IntVariation:discount-percentage:checkout.go"
]
}
```

Commit this file to source control, then use it with `flaglint-go validate --baseline <file> --fail-on-new` to adopt CI enforcement without requiring all existing debt to be fixed first. See [Quickstart: Adopt Gradually With a Baseline](/docs/go/quickstart/#4-adopt-gradually-with-a-baseline).

## Exit Behavior

`audit` always exits `0` unless the tool itself errors. It is informational — use `flaglint-go validate --no-direct-launchdarkly` to fail a build.

## Feedback

- [Edit this page on GitHub](https://github.com/flaglint/flaglint.dev/edit/main/src/content/docs/docs/go/cli/audit.md)
- [Report an issue](https://github.com/flaglint/flaglint-go/issues/new)
- Next: [validate](/docs/go/cli/validate/)
74 changes: 74 additions & 0 deletions src/content/docs/docs/go/cli/scan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
title: flaglint-go scan
description: Structured, file-level LaunchDarkly Go SDK usage inventory. Always exits 0.
lastUpdated: 2026-07-06
---

`flaglint-go scan` walks a directory and reports every detected LaunchDarkly Go SDK call site as a structured inventory — no risk scoring, no readiness score, just the raw findings. Use `audit` instead when you want a risk-ranked summary.

## Command

```bash
flaglint-go scan [dir] [flags]
```

## Options

| Flag | Description |
| --- | --- |
| `-f, --format string` | Output format: `json` \| `markdown` (default `markdown`) |
| `-o, --output string` | Write report to a file instead of stdout |
| `--config string` | Path to config file |

## Example Output (Markdown)

```text
# FlagLint Go Scan Report

Scanned 2 file(s) in 0ms — 2 unique flag(s) across 2 call site(s).

## Flags

| Flag | Call Type | Risk | File |
|---|---|---|---|
| `checkout-v2` | BoolVariation | low | checkout.go:13 |
| `discount-percentage` | IntVariation | low | checkout.go:20 |
```

## Example Output (JSON)

```json
{
"scannedFiles": 2,
"totalUsages": 2,
"uniqueFlags": ["checkout-v2", "discount-percentage"],
"usages": [
{
"flagKey": "checkout-v2",
"isDynamic": false,
"file": "checkout.go",
"line": 13,
"column": 16,
"callType": "BoolVariation",
"fingerprint": "launchdarkly:BoolVariation:checkout-v2:checkout.go",
"stalenessSignals": [],
"language": "go",
"sdk": "go-server-sdk-v7",
"risk": "low"
}
],
"warnings": []
}
```

Fingerprints (`launchdarkly:callType:flagKey:normalizedPath[:dynamicIndex]`) are stable across line-number changes and match flaglint-js's format byte-for-byte — see the [cross-tool contract](https://github.com/flaglint/flaglint-go/blob/main/docs/adr/003-cross-tool-contract.md).

## Exit Behavior

`scan` always exits `0` unless the tool itself errors (invalid directory, bad `--format`, unreadable config). It never fails based on what it finds — use `flaglint-go validate` for a CI gate.

## Feedback

- [Edit this page on GitHub](https://github.com/flaglint/flaglint.dev/edit/main/src/content/docs/docs/go/cli/scan.md)
- [Report an issue](https://github.com/flaglint/flaglint-go/issues/new)
- Next: [audit](/docs/go/cli/audit/)
99 changes: 99 additions & 0 deletions src/content/docs/docs/go/cli/validate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
title: flaglint-go validate
description: Enforce a no-direct-LaunchDarkly policy, baseline-aware CI enforcement, and SARIF findings for Go.
lastUpdated: 2026-07-06
---

`flaglint-go validate` checks whether a Go codebase complies with a flag-usage policy. It's the only flaglint-go command that ever exits `1`.

## Options

| Flag | Description |
| --- | --- |
| `--no-direct-launchdarkly` | Fail if any direct LaunchDarkly Go SDK evaluation calls are found |
| `--bootstrap-exclude stringArray` | Glob pattern for files allowed to use the LaunchDarkly SDK directly (repeatable) |
| `-f, --format string` | Output format: `text` \| `sarif` (default `text`) |
| `-o, --output string` | Write report to a file instead of stdout |
| `--config string` | Path to config file |
| `--baseline string` | Baseline file for comparing against known debt |
| `--fail-on-new` | Exit `1` if any findings are not in the baseline |

## Blocking Policy Command

```bash
flaglint-go validate ./services --no-direct-launchdarkly
```

```text
✗ validate --no-direct-launchdarkly: 2 direct LaunchDarkly Go SDK call(s) found.

checkout.go:13:16 — BoolVariation("checkout-v2")
checkout.go:20:12 — IntVariation("discount-percentage")

These call sites must migrate to OpenFeature before this rule passes.
```

Pass output once no direct calls remain:

```text
✓ validate --no-direct-launchdarkly: no direct LaunchDarkly Go SDK calls found.
Scanned 2 file(s).
```

## Baseline Mode

`--baseline`/`--fail-on-new` runs **independently** of `--no-direct-launchdarkly` — use it alone to adopt CI enforcement before existing debt is resolved:

```bash
flaglint-go audit ./services --write-baseline .flaglint-baseline.json
flaglint-go validate ./services --baseline .flaglint-baseline.json --fail-on-new
```

```text
Scanned 2 file(s). Found 2 LaunchDarkly Go SDK usage(s).
✓ No new findings beyond baseline
```

Once new debt is introduced:

```text
Scanned 2 file(s). Found 3 LaunchDarkly Go SDK usage(s).
Error: 1 new finding(s) not in baseline:
- launchdarkly:BoolVariation:new-feature-flag:checkout.go
```

Re-run `--write-baseline` whenever you accept new debt on purpose.

## SARIF

```bash
flaglint-go validate ./services --no-direct-launchdarkly --format sarif --output flaglint.sarif
```

SARIF findings use rule ID `flaglint.go.direct-launchdarkly` (Go-namespaced — distinct from flaglint-js's `flaglint.direct-launchdarkly`, per the [cross-tool contract](https://github.com/flaglint/flaglint-go/blob/main/docs/adr/003-cross-tool-contract.md)). Note: unlike flaglint-js's 0-based ESTree columns, Go's `token.Position.Column` is already 1-based, so flaglint-go does not add 1 to column numbers.

## Bootstrap Exclusions

Use `--bootstrap-exclude` for files allowed to use the SDK directly (a provider-wiring package, for example):

```bash
flaglint-go validate ./services --no-direct-launchdarkly --bootstrap-exclude "internal/openfeature-bootstrap/**"
```

## Exit Codes

| Code | Meaning |
| --- | --- |
| 0 | Success — no policy violations |
| 1 | Policy failure — direct calls found (`--no-direct-launchdarkly`) or new findings beyond baseline (`--fail-on-new`) |
| 2 | Invalid input — bad directory, bad `--format`, malformed config/baseline |
| 3 | Internal error |
| 130 | Interrupted (Ctrl-C) |

`scan` and `audit` never exit anything but `0` (barring a tool error) — `validate` is the only policy gate.

## Feedback

- [Edit this page on GitHub](https://github.com/flaglint/flaglint.dev/edit/main/src/content/docs/docs/go/cli/validate.md)
- [Report an issue](https://github.com/flaglint/flaglint-go/issues/new)
- Next: [Identity Model](/docs/go/concepts/identity-model/)
83 changes: 83 additions & 0 deletions src/content/docs/docs/go/concepts/identity-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
title: flaglint-go Identity Model
description: How flaglint-go proves a variable is a LaunchDarkly client — never by name, and across the whole scan, not just one file.
lastUpdated: 2026-07-06
---

flaglint-go's core rule, inherited from flaglint-js: a variable is only treated as a LaunchDarkly client when its identity can be **proven** — never by matching a method or variable name in isolation.

## Why not just search for method names?

Most flag scanners look for calls like `.BoolVariation(...)` or `.variation(...)` anywhere in the source. This produces false positives the moment an unrelated type happens to define a same-named method:

```go
// ❌ a name-only scanner would match this
type FakeClient struct{}
func (c *FakeClient) BoolVariation(key string, ctx interface{}, def bool) (bool, error) {
return def, nil
}

// ✅ flaglint-go correctly ignores it — FakeClient is never traced back
// to a github.com/launchdarkly/go-server-sdk import
```

flaglint-go proves identity through **import-alias tracing plus constructor-call binding**: a variable is only a client if it traces back, through the AST, to `ld.MakeClient(...)`/`ld.MakeCustomClient(...)` for a package actually imported from `github.com/launchdarkly/go-server-sdk` (v6 or v7) — whatever local alias is used, including dot-imports.

## Whole-scan resolution

Real-world Go code rarely constructs and uses the client in the same place. flaglint-go parses every file in a scan up front and resolves several indirection patterns *across* files — not just within one:

**Struct fields and composite literals** — the client stored into a wrapper struct:
```go
type Integration struct {
ldClient *ld.LDClient
}

func setup() *Integration {
client, _ := ld.MakeClient("sdk-key", 5*time.Second)
return &Integration{ldClient: client}
}
```

**Multi-level field chains**, including through generics — a struct's fields can be declared in a different file (even a different file than where they're used):
```go
type FeatureFlag[T comparable] struct {
integ *Integration
}

func (f *FeatureFlag[T]) Evaluate() bool {
v, _ := f.integ.ldClient.BoolVariation("my-flag", nil, false)
return v
}
```

**Cross-package factory/getter functions** — a package-level singleton getter called from a different package, resolved via real `go.mod`-derived import paths (never a name-based guess):
```go
// package flags
func GetLdClient() *ld.LDClient { /* ... */ }

// package main
client := flags.GetLdClient()
client.BoolVariation("my-flag", nil, false)
```

**Parameter-typed bindings** — a client passed in as a plain parameter, with no assignment to trace at all:
```go
func useClient(client *ld.LDClient) {
client.BoolVariation("my-flag", nil, false) // bound from the declared type alone
}
```

## What stays out of scope

Identity resolution is deliberately conservative — flaglint-go prefers a missed detection (false negative) over a false positive. Patterns not resolved without real type information (`go/types`) are documented, not silently guessed at. See [Limitations](/docs/go/reference/limitations/) for the current list.

## Dynamic Key Detection

A flag-key argument is `isDynamic: true` whenever it isn't a string literal — an identifier, a `fmt.Sprintf(...)` call, or string concatenation. Dynamic call sites are always classified high risk, regardless of the underlying method's own risk level, since the key "cannot resolve statically" no matter how simple the method otherwise is.

## Feedback

- [Edit this page on GitHub](https://github.com/flaglint/flaglint.dev/edit/main/src/content/docs/docs/go/concepts/identity-model.md)
- [Report an issue](https://github.com/flaglint/flaglint-go/issues/new)
- Next: [Supported Scope](/docs/go/reference/supported-scope/)
Loading
Loading