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
144 changes: 144 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

positionless is a Go static analyzer that detects positional struct literal initialization and suggests converting them to named field initialization. It's built using the `golang.org/x/tools/go/analysis` framework.

## Commands

<build_and_test>
```bash
# Build
go build -v ./...

# Run tests
go test -v ./...

# Install locally
go install .

# Use as standalone tool
positionless ./...
positionless -fix ./... # Auto-fix issues
positionless -generated ./... # Include generated files
positionless -unexported ./... # Include structs with unexported fields
positionless -internal ./... # Auto-allow unexported in internal/ packages
positionless -ignore=Pattern ./... # Skip matching struct types
positionless -output=json ./... # JSON output (golangci-lint format, to stderr)

# Use with go vet
go vet -vettool=$(which positionless) ./...
go vet -vettool=$(which positionless) -fix ./...
```
</build_and_test>

## Architecture

<core_components>
**Single-file analyzer** (`main.go`) - The entire analyzer logic is in one file:

1. **Entry point**: `singlechecker.Main(Analyzer)` - standard analysis framework pattern
2. **Analyzer instance**: Defines name, doc, run function, and `-generated` flag
3. **Analysis flow**:
- `run()` → iterates AST files, skips generated files by default
- `isGeneratedFile()` → checks comments for "code generated", "do not edit", "autogenerated"
- `analyzeFile()` → uses `ast.Inspect` to find all `*ast.CompositeLit` nodes
- `checkCompositeLit()` → validates struct, checks ignore patterns, reports diagnostics
- `isPositionalStruct()` → returns true if no `*ast.KeyValueExpr` elements exist
- `getStructType()` → extracts `*types.Struct` from type info, handles pointers
- `createNamedFieldsFix()` → builds replacement text with named fields
- `isInternalPackage()` → checks if file path contains `/internal/`
- `shouldAllowUnexported()` → determines if unexported fields should be processed
- `matchesIgnorePattern()` → checks struct type against ignore patterns
</core_components>

<key_code_points>
**Detection logic** (`isPositionalStruct`): A composite literal is positional if it has elements but none are `KeyValueExpr`.

**Fix generation** (`createNamedFieldsFix`): Reads source file once, extracts original element text by offset, reconstructs with field names. Returns both fix and unexported field status.

**Type resolution** (`getStructType`): Uses `pass.TypesInfo.Types` to get struct type, unwraps pointers via `typ.(*types.Pointer).Elem()`.

**Internal detection** (`isInternalPackage`): Checks if file path contains `/internal/` for auto-allowing unexported fields.

**Ignore patterns** (`matchesIgnorePattern`): Supports glob patterns and substring matching for struct type names.
</key_code_points>

## Test Structure

<testing>
Tests use `golang.org/x/tools/go/analysis/analysistest` framework:

```bash
testdata/src/
├── basic/ # Simple positional structs
├── nested/ # Nested struct initialization
├── pointer/ # Pointer receivers, recursive structs
├── edge/ # Empty structs, unexported fields, embedded types
├── generated/ # Generated file detection
├── internal/config # Internal package detection tests
├── unexported/ # Tests for -unexported flag
└── ignore/ # Tests for -ignore flag
```

Test annotations use `// want "message"` comments to assert expected diagnostics:
```go
p := Person{"John", 30} // want "positional struct literal initialization is fragile"
```

Run specific test packages by editing `analysistest.Run()` call in `main_test.go`.
</testing>

## Limitations

<limitations>
- **Unexported fields**: By default, reports but can't fix structs with unexported fields. Use `-unexported` or `-internal` to enable fixes.
- **Cross-file elements**: Skips if element spans multiple files
- **Field count mismatch**: Aborts if element count exceeds struct field count
- **Generated file patterns**: Only detects specific keywords in comments - custom patterns may be missed
- **Exit code 3**: Used for "findings exist" status in CI - not a failure code
- **JSON output**: Use `-output=json` for JSON lines to stderr. Normal text output still goes to stdout.
</limitations>

## Verification

<verification>
To verify implementation changes:

```bash
# 1. Run full test suite
go test -v ./...

# 2. Test on real code
positionless ./... # Check current project
positionless -fix /tmp/testdir/... # Test fix generation

# 3. Verify generated file handling
positionless testdata/src/generated/ # Should find nothing
positionless -generated testdata/src/generated/ # Should find issues

# 4. Test unexported field handling
positionless testdata/src/edge/ # Reports MixedExport without fix
positionless -unexported testdata/src/edge/ # Reports MixedExport WITH fix

# 5. Test internal package detection
positionless -internal testdata/src/internal/... # Should fix unexported in internal/

# 6. Test ignore patterns
positionless -ignore="MixedExport" testdata/src/edge/ # Should skip MixedExport

# 7. Test JSON output
positionless -output=json testdata/src/basic/ 2>&1 # JSON to stderr

# 8. Check fix output manually
positionless -fix ./testdata/src/basic/ 2>&1 # Inspect applied fixes
git diff # Review changes
git checkout -- testdata/ # Reset test files
```
</verification>

## GitHub Action

The project provides a GitHub Action (`action.yml`) that downloads the binary and runs analysis. Exit code 3 signals findings to fail CI pipelines.
68 changes: 66 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,57 @@ fieldalignment -fix ./...

Most Go editors support running custom analyzers. Configure your editor to run this analyzer for real-time feedback.

### With golangci-lint v2

`positionless` supports [golangci-lint v2 module plugins](https://golangci-lint.run/docs/plugins/module-plugins/).

**Step 1:** Create `.custom-gcl.yml` in your project root:

```yaml
version: v2.1.2
plugins:
- module: 'github.com/flaticols/positionless'
import: 'github.com/flaticols/positionless'
version: v2.0.0
```

**Step 2:** Build custom golangci-lint:

```bash
# Build custom binary with positionless (requires golangci-lint v2 installed)
golangci-lint custom
```

**Step 3:** Configure `.golangci.yml`:

```yaml
version: "2"
linters:
enable:
- positionless
settings:
custom:
positionless:
type: "module"
description: Detect positional struct literals
# Pass flags via settings (optional)
settings:
generated: false
unexported: false
internal: true
ignore: ""
output: "text"
```

**Step 4:** Run:

```bash
./custom-gcl run ./...
```

> [!NOTE]
> Module plugins are the recommended approach for golangci-lint v2. No toolchain version matching required.

### As a GitHub Action

You can use `positionless` in your GitHub workflows to automatically check for positional struct literals:
Expand Down Expand Up @@ -236,8 +287,21 @@ The analyzer:
3. Suggests fixes that convert to named field initialization
4. Can automatically apply fixes with the `-fix` flag
5. Preserves your original values and formatting
6. Only processes exported fields (respects Go's visibility rules)
7. Skips generated files by default (use `-generated` to include them)
6. Skips generated files by default (use `-generated` to include them)

### Flags

| Flag | Description |
|------|-------------|
| `-fix` | Apply suggested fixes automatically |
| `-generated` | Include generated files in analysis |
| `-unexported` | Include structs with unexported fields in fixes |
| `-internal` | Auto-allow unexported fields in `internal/` packages |
| `-ignore=PATTERN` | Skip structs matching pattern (comma-separated) |
| `-output=FORMAT` | Output format: `text` (default) or `json` |

> [!TIP]
> Use `-internal` when analyzing your own internal packages where you control all the code

## Example

Expand Down
Loading