Skip to content

Commit 654fbc1

Browse files
authored
Add hardcodedfilepath linter to detect hard-coded file path string literals (#38742)
1 parent cc312df commit 654fbc1

9 files changed

Lines changed: 465 additions & 2 deletions

File tree

cmd/linters/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/github/gh-aw/pkg/linters/fileclosenotdeferred"
2626
"github.com/github/gh-aw/pkg/linters/fmterrorfnoverbs"
2727
"github.com/github/gh-aw/pkg/linters/fprintlnsprintf"
28+
"github.com/github/gh-aw/pkg/linters/hardcodedfilepath"
2829
"github.com/github/gh-aw/pkg/linters/jsonmarshalignoredeerror"
2930
"github.com/github/gh-aw/pkg/linters/largefunc"
3031
"github.com/github/gh-aw/pkg/linters/lenstringzero"
@@ -54,6 +55,7 @@ func main() {
5455
excessivefuncparams.Analyzer,
5556
fileclosenotdeferred.Analyzer,
5657
fmterrorfnoverbs.Analyzer,
58+
hardcodedfilepath.Analyzer,
5759
largefunc.Analyzer,
5860
manualmutexunlock.Analyzer,
5961
osexitinlibrary.Analyzer,
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# ADR-38742: Enforce File-Path Constant Usage via a Custom Linter
2+
3+
**Date**: 2026-06-12
4+
**Status**: Draft
5+
6+
## Context
7+
8+
The codebase repeatedly embeds filesystem path string literals (e.g. `/tmp/gh-aw/awf-config.json`, `${RUNNER_TEMP}/...`, `.github/...`) inline across many files. When the same path is duplicated as a literal in several places, a change to that path requires hunting down every copy, and drift between copies causes subtle, hard-to-diagnose bugs — especially when paths appear in log/print output. The project already maintains a suite of custom `go/analysis` analyzers under `pkg/linters/` (24 existing analyzers wired into a multichecker binary), and has previously chosen the custom-linter approach to enforce conventions mechanically rather than by code-review convention (see [ADR-38704](38704-enforce-context-aware-sleep-via-custom-linter.md)). This PR addresses the path-literal duplication problem within that established pattern.
9+
10+
## Decision
11+
12+
We will enforce file-path constant usage with a new custom `go/analysis` analyzer, `hardcodedfilepath`, added to `pkg/linters/` and wired into the multichecker binary (`cmd/linters/main.go`). The analyzer flags string literals that look like filesystem paths (recognized prefix + minimum length), and either suggests an existing named path constant when the literal matches one (scanning the current package and imported `*constants*` packages), or recommends extracting a new named constant when no match exists. It additionally annotates findings that appear inside log/print calls, skips const-declaration sites and format-template strings, and honours `//nolint:hardcodedfilepath` suppression. We chose this approach because it reuses the existing linter infrastructure and CI enforcement, making the convention self-checking rather than dependent on reviewer vigilance.
13+
14+
## Alternatives Considered
15+
16+
### Alternative 1: Rely on code review and documentation
17+
Document the "use a named path constant" convention and enforce it during PR review. Rejected because it does not scale — reviewers miss inline literals, and the existing duplication shows convention alone has not prevented the problem. It also provides no actionable, located diagnostics.
18+
19+
### Alternative 2: Use an existing general-purpose linter (e.g. goconst, golangci-lint rules)
20+
`goconst` detects repeated string literals generally. Rejected because it cannot reason about path-specific heuristics (path prefixes, `${RUNNER_TEMP}` templating, log/print correlation) or cross-reference values against exported constants in dedicated `*constants*` packages, and it would produce noise on non-path strings. The project's custom-analyzer pattern allows precise, domain-specific diagnostics.
21+
22+
## Consequences
23+
24+
### Positive
25+
- Path-literal duplication is caught mechanically in CI with located, actionable diagnostics that name the constant to use.
26+
- Reinforces a single source of truth for shared paths, reducing drift-related bugs.
27+
- Consistent with the established `pkg/linters/` pattern, so contributors already know how to read, suppress, and extend it.
28+
29+
### Negative
30+
- Heuristic detection (prefix list + length threshold) will produce false positives/negatives; the prefix list and `minPathRuneLen` must be maintained as path conventions evolve.
31+
- Adds a 25th analyzer to the multichecker, marginally increasing lint runtime and the maintenance surface.
32+
- The constant-matching logic depends on the `*constants*` package-path naming convention; constants defined elsewhere will not be recognized as suggestions.
33+
34+
### Neutral
35+
- Suppression is available via `//nolint:hardcodedfilepath` for intentional inline literals.
36+
- Test files are skipped, so test fixtures may continue to use inline path literals.
37+
38+
---
39+
40+
*This is a DRAFT ADR generated by the [Design Decision Gate](https://github.com/github/gh-aw/actions/runs/27386237174) workflow. The PR author must review, complete, and finalize this document before the PR can merge.*

linters

482 KB
Binary file not shown.

pkg/linters/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ This package currently provides custom Go analyzers in the following subpackages
1515
- `execcommandwithoutcontext` — reports `exec.Command(...)` calls inside functions that already receive `context.Context` and should use `exec.CommandContext(...)`.
1616
- `fmterrorfnoverbs` — reports `fmt.Errorf` calls whose format string contains no verbs, recommending `errors.New` instead.
1717
- `fprintlnsprintf` — reports `fmt.Fprintln(..., fmt.Sprintf(...))` patterns and recommends direct formatting calls.
18+
- `hardcodedfilepath` — reports hard-coded file path string literals that match known path constants or should be extracted into named constants; also annotates paths that appear in log/print calls.
1819
- `jsonmarshalignoredeerror` — reports `json.Marshal` and `json.Unmarshal` calls where the error return is discarded with `_`.
1920
- `largefunc` — reports function bodies that exceed a configurable line-count threshold.
2021
- `lenstringzero` — reports `len(s) == 0` / `len(s) != 0` comparisons on string values that should use `s == ""` / `s != ""`.
@@ -47,6 +48,7 @@ This package currently provides custom Go analyzers in the following subpackages
4748
| `fileclosenotdeferred` | Custom `go/analysis` analyzer that flags file `Close()` calls that are not deferred immediately |
4849
| `fmterrorfnoverbs` | Custom `go/analysis` analyzer that flags `fmt.Errorf` calls with no format verbs, recommending `errors.New` |
4950
| `fprintlnsprintf` | Custom `go/analysis` analyzer that flags `fmt.Fprintln(..., fmt.Sprintf(...))` patterns |
51+
| `hardcodedfilepath` | Custom `go/analysis` analyzer that flags hard-coded file path string literals that match known path constants or should be extracted as named constants; annotates paths in log/print calls |
5052
| `jsonmarshalignoredeerror` | Custom `go/analysis` analyzer that flags `json.Marshal`/`json.Unmarshal` calls where the error return is discarded with `_` |
5153
| `largefunc` | Custom `go/analysis` analyzer that flags large functions with actionable diagnostics |
5254
| `lenstringzero` | Custom `go/analysis` analyzer that flags `len(s) == 0` / `len(s) != 0` on string values that should use `s == ""` / `s != ""` |
@@ -80,6 +82,7 @@ import (
8082
"github.com/github/gh-aw/pkg/linters/errstringmatch"
8183
"github.com/github/gh-aw/pkg/linters/execcommandwithoutcontext"
8284
"github.com/github/gh-aw/pkg/linters/fileclosenotdeferred"
85+
"github.com/github/gh-aw/pkg/linters/hardcodedfilepath"
8386
"github.com/github/gh-aw/pkg/linters/largefunc"
8487
"github.com/github/gh-aw/pkg/linters/lenstringzero"
8588
"github.com/github/gh-aw/pkg/linters/manualmutexunlock"
@@ -98,6 +101,7 @@ _ = errormessage.Analyzer
98101
_ = errstringmatch.Analyzer
99102
_ = execcommandwithoutcontext.Analyzer
100103
_ = fileclosenotdeferred.Analyzer
104+
_ = hardcodedfilepath.Analyzer
101105
_ = largefunc.Analyzer
102106
_ = lenstringzero.Analyzer
103107
_ = manualmutexunlock.Analyzer
@@ -121,6 +125,7 @@ _ = ssljson.Analyzer
121125
- `github.com/github/gh-aw/pkg/linters/fileclosenotdeferred` — file-close-not-deferred analyzer subpackage
122126
- `github.com/github/gh-aw/pkg/linters/fmterrorfnoverbs` — fmt-errorf-no-verbs analyzer subpackage
123127
- `github.com/github/gh-aw/pkg/linters/fprintlnsprintf` — fprintln-sprintf analyzer subpackage
128+
- `github.com/github/gh-aw/pkg/linters/hardcodedfilepath` — hard-coded-file-path analyzer subpackage
124129
- `github.com/github/gh-aw/pkg/linters/jsonmarshalignoredeerror` — json-marshal-ignored-error analyzer subpackage
125130
- `github.com/github/gh-aw/pkg/linters/largefunc` — large-func analyzer subpackage
126131
- `github.com/github/gh-aw/pkg/linters/lenstringzero` — len-string-zero analyzer subpackage

0 commit comments

Comments
 (0)