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
53 changes: 53 additions & 0 deletions .bestpractices.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"description_good_status": "Met",
"description_good_justification": "The project has a clear, comprehensive description and summary of its functionality in README.md.",

"interact_status": "Met",
"interact_justification": "Users can interact with the project and maintainers via GitHub issues, discussions, and pull requests.",

"contribution_status": "Met",
"contribution_justification": "Contributing guidelines are clearly documented in CONTRIBUTING.md.",

"contribution_quickstart_status": "Met",
"contribution_quickstart_justification": "The README.md and CONTRIBUTING.md files include a quick-start guide for new contributors.",

"floss_license_status": "Met",
"floss_license_justification": "The project is licensed under the MIT license, which is a recognized FLOSS license.",

"floss_license_osi_status": "Met",
"floss_license_osi_justification": "The MIT license is officially approved by the OSI (Open Source Initiative).",

"build_status": "Met",
"build_justification": "The project uses a standard Go toolchain and Taskfile.yml/Makefile for automated builds.",

"test_status": "Met",
"test_justification": "The project includes a robust test suite covering over 190 tests (unit, integration, coverage, and fuzz tests).",

"test_suite_status": "Met",
"test_suite_justification": "All tests are run automatically via a make/task runner on the command line.",

"test_ci_status": "Met",
"test_ci_test_status": "Met",
"test_ci_justification": "GitHub Actions runs all tests on every push and pull request (defined in ci.yml).",

"coding_standards_status": "Met",
"coding_standards_justification": "We enforce strict coding standards using golangci-lint, go-consistent, and gofumpt formatting in CI.",

"coding_standards_tools_status": "Met",
"coding_standards_tools_justification": "All styling, consistency, and syntax audits are fully automated using standard static analysis tools.",

"report_tracker_status": "Met",
"report_tracker_justification": "GitHub Issues is used as the authoritative bug tracking system.",

"vulnerability_report_status": "Met",
"vulnerability_report_justification": "The SECURITY.md file outlines the clear process for private/responsible disclosure of vulnerabilities.",

"static_analysis_status": "Met",
"static_analysis_justification": "Static application security testing (SAST) is fully automated using GitHub CodeQL.",

"dynamic_analysis_status": "N/A",
"dynamic_analysis_justification": "As a static Go tool analyzing code structures locally, there is no long-running server or dynamic state to fuzz dynamically in runtime, making DAST not applicable. We compensate by running extensive native Go fuzzing on all parsing inputs.",

"osps_ac_01_01_status": "Met",
"osps_ac_01_01_justification": "GitHub strictly mandates and enforces multi-factor authentication (MFA) for all repository write collaborators."
}
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,14 @@ to swap it back for the internal package — it won't compile from this module.
- **`align` and `layout` return data, `ui` renders it.** Keep that split: no
printing in the logic packages, no analysis in `ui`. New output formatting goes
in `ui`; new analysis/derived fields go on the `common` types.
- **`-format=json` (`ui.RenderJSON`) is the machine renderer**, parallel to
`RenderFindings`/`RenderLayouts`. Two deliberate divergences from the text path:
the diff document **always** carries the `summary` block (so consumers always
get totals — `-summary` governs only the text trailing line), and the text-only
presentation flags (`-diff`, `-summary`, `-verbose`, `-color`, `-width`) are
ignored in JSON. `-tags` still gates the inspect field's `tag`. Any encode
error is reported on the printer's `Err` stream (`p.err()`, set to
`App.Stderr`), not the real `os.Stderr`.
- **Scan options travel in `common.Options`** (`Patterns`, `KeepTags`,
`IncludeGenerated`, `SkipCachePadded`, `RespectNolint`, `NolintLinters`), passed
to `Aligner.Findings` / `Inspector.Layouts`. `align`/`layout` apply the filters
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/peczenyj/structalign/blob/main/CONTRIBUTING.md#pull-request-process)
[![SLSA Build Level 2](https://img.shields.io/badge/SLSA-Build_L2-green.svg)](https://github.com/peczenyj/structalign/attestations)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/peczenyj/structalign/badge)](https://scorecard.dev/viewer/?uri=github.com/peczenyj/structalign)
[![OpenSSF Best Practices](https://bestpractices.coreinfrastructure.org/projects/13027/badge)](https://bestpractices.coreinfrastructure.org/projects/13027)
[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge-flat.svg)](https://github.com/avelino/awesome-go#code-analysis)

> See how reordering a Go struct's fields could save memory — as a **diff**, not a
Expand Down Expand Up @@ -375,6 +376,36 @@ inspect prints a *struct field layout*, and scalars have no fields. (The
see a scalar's size, inspect a struct that contains it — a `string` field shows
`size: 16` on a 64-bit target.

### JSON output

`-format=json` (or `STRUCTALIGN_FORMAT=json`, or `format = json` in
`.structalignrc`) emits a single structured document instead of the rendered
text, for both diff and inspect modes. It carries the same data the text
renderers show — findings include `original` / `proposed`, `oldSize` /
`newSize` / `bytesSaved`; inspect layouts include per-field
`offset` / `size` / `align` / `padding` and the generic `assume` notes.

```
$ structalign -format=json -type=Mixed ./_example
{
"version": "...",
"mode": "diff",
"findings": [ ... ],
"summary": { "structsAffected": 1, "bytesSaved": 8 }
}
```

Two things differ from text mode by design:

- **The diff document always includes the `summary` block** (so a machine
consumer always gets the totals). `-summary` only governs the text renderer's
trailing summary line.
- **The presentation flags don't apply.** `-diff`, `-summary`, `-verbose`,
`-color`, and `-width` shape the *text* output only; in JSON mode they are
ignored, since the consumer renders from the structured fields itself.
`-tags` still applies — it gates whether the inspect document's per-field
`tag` field is emitted (see [Field tags](#field-tags)).

### Filtering by type name

`-type` takes a comma-separated list of glob patterns (`path.Match` syntax: `*`,
Expand Down
8 changes: 4 additions & 4 deletions internal/align/internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ func TestReadSourceErrors(t *testing.T) {
// os.ReadFile error
assert.Empty(t, readSource(fset, f.Pos(0), f.Pos(5)))

// Bounds check: stop > len(data)
// We need a file that exists but we use offsets past its length.
// But readSource reads the file from disk using pf.Name().
// It's hard to trigger start < 0 or start > stop with valid token.Pos.
// The remaining bounds guards (start < 0, start > stop, stop > len(data))
// are intentionally not covered here: readSource derives both offsets from
// valid token.Pos values off the same file, so they can't be provoked
// without a corrupt FileSet. The guards stay as defense in depth.
}
7 changes: 7 additions & 0 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,13 @@ func (a *App) Run(args []string) int {
home, _ := os.UserHomeDir()
cwd, _ := os.Getwd()
for k, v := range config.Load(home, cwd) {
// Silently ignore keys that don't map to a flag: this covers the
// documented "theme is not an RC key" exclusion as well as typos,
// so a stray key never surfaces a "no such flag" warning. A key
// that *is* a flag but gets a bad value still warns below.
if fs.Lookup(k) == nil {
continue
}
if err := fs.Set(k, v); err != nil {
fmt.Fprintf(a.Stderr, "structalign: config: %s: %v\n", k, err)
}
Expand Down
46 changes: 46 additions & 0 deletions internal/app/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,49 @@ func TestRunLayeredConfig(t *testing.T) {
errb.Reset()
})
}

// TestRunRCUnknownKeys verifies that RC keys which don't map to a flag — the
// documented "theme is not an RC key" exclusion, and outright typos — are
// skipped silently, while a real flag given a bad value still warns.
func TestRunRCUnknownKeys(t *testing.T) {
tmp := t.TempDir()
t.Setenv("HOME", tmp) // empty home rc

cwd := filepath.Join(tmp, "cwd")
require.NoError(t, os.Mkdir(cwd, 0o755))
oldCWD, err := os.Getwd()
require.NoError(t, err)
require.NoError(t, os.Chdir(cwd))
t.Cleanup(func() { _ = os.Chdir(oldCWD) })

run := func(t *testing.T, rc string) string {
t.Helper()
require.NoError(t, os.WriteFile(filepath.Join(cwd, ".structalignrc"), []byte(rc), 0o644))

var out, errb bytes.Buffer
ml := &mocks.Loader{}
ma := &mocks.Aligner{}
ml.On("Load", "pkg").Return([]common.Target{{PkgPath: "pkg"}}, nil)
ma.On("Findings", mock.Anything, mock.Anything).Return(nil, nil)
a := &app.App{Loader: ml, Aligner: ma, Stdout: &out, Stderr: &errb}
a.Run([]string{"pkg"})
return errb.String()
}

t.Run("ThemeKeyIsSilent", func(t *testing.T) {
out := run(t, "theme = cga\n")
assert.NotContains(t, out, "no such flag")
assert.NotContains(t, out, "theme")
})

t.Run("TypoIsSilent", func(t *testing.T) {
out := run(t, "totally-bogus-key = 1\n")
assert.NotContains(t, out, "no such flag")
assert.NotContains(t, out, "totally-bogus-key")
})

t.Run("BadValueForRealFlagWarns", func(t *testing.T) {
out := run(t, "threshold = garbage\n")
assert.Contains(t, out, "structalign: config: threshold:")
})
}
Loading