diff --git a/cmd/linters/main.go b/cmd/linters/main.go index 5a2ee4a948b..5323ce9b31a 100644 --- a/cmd/linters/main.go +++ b/cmd/linters/main.go @@ -25,6 +25,7 @@ import ( "github.com/github/gh-aw/pkg/linters/fileclosenotdeferred" "github.com/github/gh-aw/pkg/linters/fmterrorfnoverbs" "github.com/github/gh-aw/pkg/linters/fprintlnsprintf" + "github.com/github/gh-aw/pkg/linters/hardcodedfilepath" "github.com/github/gh-aw/pkg/linters/jsonmarshalignoredeerror" "github.com/github/gh-aw/pkg/linters/largefunc" "github.com/github/gh-aw/pkg/linters/lenstringzero" @@ -54,6 +55,7 @@ func main() { excessivefuncparams.Analyzer, fileclosenotdeferred.Analyzer, fmterrorfnoverbs.Analyzer, + hardcodedfilepath.Analyzer, largefunc.Analyzer, manualmutexunlock.Analyzer, osexitinlibrary.Analyzer, diff --git a/docs/adr/38742-enforce-path-constants-via-custom-linter.md b/docs/adr/38742-enforce-path-constants-via-custom-linter.md new file mode 100644 index 00000000000..bf26b5e4859 --- /dev/null +++ b/docs/adr/38742-enforce-path-constants-via-custom-linter.md @@ -0,0 +1,40 @@ +# ADR-38742: Enforce File-Path Constant Usage via a Custom Linter + +**Date**: 2026-06-12 +**Status**: Draft + +## Context + +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. + +## Decision + +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. + +## Alternatives Considered + +### Alternative 1: Rely on code review and documentation +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. + +### Alternative 2: Use an existing general-purpose linter (e.g. goconst, golangci-lint rules) +`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. + +## Consequences + +### Positive +- Path-literal duplication is caught mechanically in CI with located, actionable diagnostics that name the constant to use. +- Reinforces a single source of truth for shared paths, reducing drift-related bugs. +- Consistent with the established `pkg/linters/` pattern, so contributors already know how to read, suppress, and extend it. + +### Negative +- Heuristic detection (prefix list + length threshold) will produce false positives/negatives; the prefix list and `minPathRuneLen` must be maintained as path conventions evolve. +- Adds a 25th analyzer to the multichecker, marginally increasing lint runtime and the maintenance surface. +- The constant-matching logic depends on the `*constants*` package-path naming convention; constants defined elsewhere will not be recognized as suggestions. + +### Neutral +- Suppression is available via `//nolint:hardcodedfilepath` for intentional inline literals. +- Test files are skipped, so test fixtures may continue to use inline path literals. + +--- + +*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.* diff --git a/linters b/linters index 8aadbce8f6f..c336ba6b6fa 100755 Binary files a/linters and b/linters differ diff --git a/pkg/linters/README.md b/pkg/linters/README.md index c4e1c9950f5..4a54805ad8d 100644 --- a/pkg/linters/README.md +++ b/pkg/linters/README.md @@ -15,6 +15,7 @@ This package currently provides custom Go analyzers in the following subpackages - `execcommandwithoutcontext` — reports `exec.Command(...)` calls inside functions that already receive `context.Context` and should use `exec.CommandContext(...)`. - `fmterrorfnoverbs` — reports `fmt.Errorf` calls whose format string contains no verbs, recommending `errors.New` instead. - `fprintlnsprintf` — reports `fmt.Fprintln(..., fmt.Sprintf(...))` patterns and recommends direct formatting calls. +- `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. - `jsonmarshalignoredeerror` — reports `json.Marshal` and `json.Unmarshal` calls where the error return is discarded with `_`. - `largefunc` — reports function bodies that exceed a configurable line-count threshold. - `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 | `fileclosenotdeferred` | Custom `go/analysis` analyzer that flags file `Close()` calls that are not deferred immediately | | `fmterrorfnoverbs` | Custom `go/analysis` analyzer that flags `fmt.Errorf` calls with no format verbs, recommending `errors.New` | | `fprintlnsprintf` | Custom `go/analysis` analyzer that flags `fmt.Fprintln(..., fmt.Sprintf(...))` patterns | +| `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 | | `jsonmarshalignoredeerror` | Custom `go/analysis` analyzer that flags `json.Marshal`/`json.Unmarshal` calls where the error return is discarded with `_` | | `largefunc` | Custom `go/analysis` analyzer that flags large functions with actionable diagnostics | | `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 ( "github.com/github/gh-aw/pkg/linters/errstringmatch" "github.com/github/gh-aw/pkg/linters/execcommandwithoutcontext" "github.com/github/gh-aw/pkg/linters/fileclosenotdeferred" + "github.com/github/gh-aw/pkg/linters/hardcodedfilepath" "github.com/github/gh-aw/pkg/linters/largefunc" "github.com/github/gh-aw/pkg/linters/lenstringzero" "github.com/github/gh-aw/pkg/linters/manualmutexunlock" @@ -98,6 +101,7 @@ _ = errormessage.Analyzer _ = errstringmatch.Analyzer _ = execcommandwithoutcontext.Analyzer _ = fileclosenotdeferred.Analyzer +_ = hardcodedfilepath.Analyzer _ = largefunc.Analyzer _ = lenstringzero.Analyzer _ = manualmutexunlock.Analyzer @@ -121,6 +125,7 @@ _ = ssljson.Analyzer - `github.com/github/gh-aw/pkg/linters/fileclosenotdeferred` — file-close-not-deferred analyzer subpackage - `github.com/github/gh-aw/pkg/linters/fmterrorfnoverbs` — fmt-errorf-no-verbs analyzer subpackage - `github.com/github/gh-aw/pkg/linters/fprintlnsprintf` — fprintln-sprintf analyzer subpackage +- `github.com/github/gh-aw/pkg/linters/hardcodedfilepath` — hard-coded-file-path analyzer subpackage - `github.com/github/gh-aw/pkg/linters/jsonmarshalignoredeerror` — json-marshal-ignored-error analyzer subpackage - `github.com/github/gh-aw/pkg/linters/largefunc` — large-func analyzer subpackage - `github.com/github/gh-aw/pkg/linters/lenstringzero` — len-string-zero analyzer subpackage diff --git a/pkg/linters/hardcodedfilepath/hardcodedfilepath.go b/pkg/linters/hardcodedfilepath/hardcodedfilepath.go new file mode 100644 index 00000000000..2f5484fdfb9 --- /dev/null +++ b/pkg/linters/hardcodedfilepath/hardcodedfilepath.go @@ -0,0 +1,311 @@ +// Package hardcodedfilepath implements a Go analysis linter that flags +// hard-coded file path string literals and compares them against a known set +// of file path constants. When a literal value matches an existing named +// constant, the linter reports it with a suggestion to use the constant. +// When no matching constant exists, the linter reports it as a candidate for +// extraction into a named constant. +// +// The linter also correlates path literals with logging and print calls, +// annotating those findings with an extra note, because paths in log output +// are especially important to keep consistent via named constants. +package hardcodedfilepath + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + "strings" + "unicode/utf8" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + + "github.com/github/gh-aw/pkg/linters/internal/astutil" + "github.com/github/gh-aw/pkg/linters/internal/filecheck" + "github.com/github/gh-aw/pkg/linters/internal/nolint" +) + +// Analyzer is the hardcoded-file-path analysis pass. +var Analyzer = &analysis.Analyzer{ + Name: "hardcodedfilepath", + Doc: "reports hard-coded file path string literals that should be replaced with named constants", + URL: "https://github.com/github/gh-aw/tree/main/pkg/linters/hardcodedfilepath", + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: run, +} + +// constRef holds a reference to a named path constant. +type constRef struct { + pkgAlias string // local import alias or package name; empty for same-package consts + constName string // exported constant name +} + +func (r constRef) String() string { + if r.pkgAlias == "" { + return r.constName + } + return r.pkgAlias + "." + r.constName +} + +// pathPrefixes lists the prefixes that make a string literal qualify as a +// filesystem path. Each prefix ends with "/" so that bare tokens like "/tmp", +// ".github", or "${RUNNER_TEMP}" alone are not flagged. +var pathPrefixes = []string{ + "/tmp/", + "${RUNNER_TEMP}/", + "${{ runner.temp }}/", + ".github/", + "/opt/", + "/usr/", + "/var/", + "/home/", + "/etc/", + "/run/", +} + +// minPathRuneLen is the minimum character length of an unquoted path value +// to flag. Paths shorter than this are considered too generic. +const minPathRuneLen = 8 + +// isPathLike reports whether val (an unquoted string value) looks like a +// filesystem path worth inspecting. +func isPathLike(val string) bool { + if utf8.RuneCountInString(val) < minPathRuneLen { + return false + } + for _, prefix := range pathPrefixes { + if strings.HasPrefix(val, prefix) { + return true + } + } + return false +} + +// hasFormatVerb reports whether val contains common fmt format verbs. Strings +// with verbs are format templates, not standalone paths, so they are excluded. +func hasFormatVerb(val string) bool { + return strings.ContainsAny(val, "%") && + (strings.Contains(val, "%s") || strings.Contains(val, "%d") || + strings.Contains(val, "%v") || strings.Contains(val, "%q") || + strings.Contains(val, "%w") || strings.Contains(val, "%f")) +} + +// unquoteStringLit returns the raw string value of a Go string literal token, +// stripping surrounding double-quotes or backticks. It does not process escape +// sequences — only the outer delimiters are removed. +func unquoteStringLit(lit string) string { + if len(lit) >= 2 { + if (lit[0] == '"' && lit[len(lit)-1] == '"') || + (lit[0] == '`' && lit[len(lit)-1] == '`') { + return lit[1 : len(lit)-1] + } + } + return lit +} + +// logPrintMethods is the set of method names that produce human-readable +// output and commonly include file paths in their arguments. +var logPrintMethods = map[string]bool{ + "Print": true, "Println": true, "Printf": true, + "Fprint": true, "Fprintln": true, "Fprintf": true, + "Fatal": true, "Fatalf": true, "Fatalln": true, + "Panic": true, "Panicf": true, "Panicln": true, + "Error": true, "Errorf": true, + "Warn": true, "Warnf": true, + "Info": true, "Infof": true, + "Debug": true, "Debugf": true, + "Trace": true, "Tracef": true, + "Log": true, "Logf": true, +} + +// isLogOrPrintCall reports whether call is a package-qualified log or print +// function call (e.g. log.Printf, fmt.Println, logger.Infof). +func isLogOrPrintCall(pass *analysis.Pass, call *ast.CallExpr) bool { + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + return false + } + if !logPrintMethods[sel.Sel.Name] { + return false + } + ident, ok := sel.X.(*ast.Ident) + if !ok { + return false + } + obj := pass.TypesInfo.ObjectOf(ident) + if obj == nil { + return true // unknown object — conservative accept + } + _, isPkg := obj.(*types.PkgName) + return isPkg +} + +// collectKnownPathConsts builds a map from path string value to constRef by +// scanning: +// 1. All exported constants declared at package scope in pass.Pkg. +// 2. All exported constants in directly imported packages whose import path +// contains "constants" (e.g. "github.com/example/pkg/constants"). +// +// Only string constants whose value matches pathPrefixes are included. +func collectKnownPathConsts(pass *analysis.Pass) map[string]constRef { + out := make(map[string]constRef) + + addConst := func(c *types.Const, alias, name string) { + if !c.Exported() { + return + } + basic, ok := c.Type().Underlying().(*types.Basic) + if !ok || (basic.Kind() != types.String && basic.Kind() != types.UntypedString) { + return + } + // c.Val().String() returns the value with surrounding double-quote characters. + raw := c.Val().String() + val := strings.TrimPrefix(strings.TrimSuffix(raw, `"`), `"`) + if !isPathLike(val) { + return + } + if _, exists := out[val]; !exists { + out[val] = constRef{pkgAlias: alias, constName: name} + } + } + + // 1. Current package's own exported constants. + scope := pass.Pkg.Scope() + for _, name := range scope.Names() { + obj := scope.Lookup(name) + c, ok := obj.(*types.Const) + if !ok { + continue + } + addConst(c, "", name) + } + + // 2. Imported "constants" packages. + for _, imp := range pass.Pkg.Imports() { + if !strings.Contains(imp.Path(), "constants") { + continue + } + alias := resolveImportAlias(pass, imp) + for _, name := range imp.Scope().Names() { + obj := imp.Scope().Lookup(name) + c, ok := obj.(*types.Const) + if !ok { + continue + } + addConst(c, alias, name) + } + } + + return out +} + +// resolveImportAlias returns the local name used for pkg in the files of pass. +// If an explicit alias is set it is returned; otherwise the package's own name +// is used. +func resolveImportAlias(pass *analysis.Pass, pkg *types.Package) string { + for _, file := range pass.Files { + for _, imp := range file.Imports { + path := strings.Trim(imp.Path.Value, `"`) + if path != pkg.Path() { + continue + } + if imp.Name != nil && imp.Name.Name != "." && imp.Name.Name != "_" { + return imp.Name.Name + } + return pkg.Name() + } + } + return pkg.Name() +} + +func run(pass *analysis.Pass) (any, error) { + insp, err := astutil.Inspector(pass) + if err != nil { + return nil, err + } + + noLintLines := nolint.BuildLineIndex(pass, "hardcodedfilepath") + knownConsts := collectKnownPathConsts(pass) + + for cur := range insp.Root().Preorder((*ast.BasicLit)(nil)) { + lit, ok := cur.Node().(*ast.BasicLit) + if !ok || lit.Kind != token.STRING { + continue + } + + pos := pass.Fset.PositionFor(lit.Pos(), false) + if filecheck.IsTestFile(pos.Filename) { + continue + } + if nolint.HasDirective(pos, noLintLines) { + continue + } + + raw := unquoteStringLit(lit.Value) + if !isPathLike(raw) { + continue + } + if hasFormatVerb(raw) { + continue + } + + // Skip literals that are the value of a const declaration — those are + // the canonical definitions, not inline usages. + if isConstDeclValue(cur) { + continue + } + + // Detect whether the literal is a direct argument of a log/print call. + inLog := enclosingCallIsLogPrint(pass, cur) + + if ref, found := knownConsts[raw]; found { + msg := fmt.Sprintf( + "hard-coded file path %q: use constant %s instead of inline string literal", + raw, ref, + ) + if inLog { + msg += " (path appears in log/print call — keeping consistent via constant is especially important)" + } + pass.ReportRangef(lit, "%s", msg) + } else { + msg := fmt.Sprintf( + "hard-coded file path %q: consider extracting as a named constant", + raw, + ) + if inLog { + msg += " (path appears in log/print call)" + } + pass.ReportRangef(lit, "%s", msg) + } + } + + return nil, nil +} + +// isConstDeclValue reports whether the cursor's node is the value expression +// of a const declaration. It walks the enclosing GenDecl ancestors and checks +// for Tok == token.CONST. +func isConstDeclValue(cur inspector.Cursor) bool { + for encl := range cur.Enclosing((*ast.GenDecl)(nil)) { + decl, ok := encl.Node().(*ast.GenDecl) + if ok && decl.Tok == token.CONST { + return true + } + } + return false +} + +// enclosingCallIsLogPrint reports whether the nearest enclosing *ast.CallExpr +// is a log/print call. +func enclosingCallIsLogPrint(pass *analysis.Pass, cur inspector.Cursor) bool { + for encl := range cur.Enclosing((*ast.CallExpr)(nil)) { + call, ok := encl.Node().(*ast.CallExpr) + if !ok { + continue + } + return isLogOrPrintCall(pass, call) + } + return false +} diff --git a/pkg/linters/hardcodedfilepath/hardcodedfilepath_test.go b/pkg/linters/hardcodedfilepath/hardcodedfilepath_test.go new file mode 100644 index 00000000000..e2c65b3a285 --- /dev/null +++ b/pkg/linters/hardcodedfilepath/hardcodedfilepath_test.go @@ -0,0 +1,16 @@ +//go:build !integration + +package hardcodedfilepath_test + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" + + "github.com/github/gh-aw/pkg/linters/hardcodedfilepath" +) + +func TestHardcodedFilePath(t *testing.T) { + testdata := analysistest.TestData() + analysistest.Run(t, testdata, hardcodedfilepath.Analyzer, "constants", "hardcodedfilepath") +} diff --git a/pkg/linters/hardcodedfilepath/testdata/src/constants/constants.go b/pkg/linters/hardcodedfilepath/testdata/src/constants/constants.go new file mode 100644 index 00000000000..ed460b0f0c7 --- /dev/null +++ b/pkg/linters/hardcodedfilepath/testdata/src/constants/constants.go @@ -0,0 +1,11 @@ +// Package constants provides known file path constants for testing. +package constants + +// ConfigFilePath is the path to the config file. +const ConfigFilePath = "/tmp/gh-aw/awf-config.json" + +// ProxyLogsDir is the directory for proxy logs. +const ProxyLogsDir = "/tmp/gh-aw/sandbox/firewall/logs" + +// AuditFilePath is the path to the audit file. +const AuditFilePath = "/tmp/gh-aw/pre-agent-audit.txt" diff --git a/pkg/linters/hardcodedfilepath/testdata/src/hardcodedfilepath/hardcodedfilepath.go b/pkg/linters/hardcodedfilepath/testdata/src/hardcodedfilepath/hardcodedfilepath.go new file mode 100644 index 00000000000..5dd991dc54f --- /dev/null +++ b/pkg/linters/hardcodedfilepath/testdata/src/hardcodedfilepath/hardcodedfilepath.go @@ -0,0 +1,76 @@ +package hardcodedfilepath + +import ( + "fmt" + "log" + + "constants" +) + +// bad: inline path literal that matches a known constant. +func badMatchesConstant() string { + return "/tmp/gh-aw/awf-config.json" // want `hard-coded file path.*use constant constants\.ConfigFilePath` +} + +// bad: inline path that matches another known constant. +func badMatchesLogsDir() string { + return "/tmp/gh-aw/sandbox/firewall/logs" // want `hard-coded file path.*use constant constants\.ProxyLogsDir` +} + +// bad: inline path with no matching constant, should suggest extraction. +func badNoMatchingConst() string { + return "/tmp/gh-aw/agent-stdio.log" // want `hard-coded file path.*consider extracting` +} + +// bad: path in a log call (log correlation). +func badInLogCall() { + log.Printf("reading from %s", "/tmp/gh-aw/awf-config.json") // want `hard-coded file path.*use constant constants\.ConfigFilePath` +} + +// bad: path in fmt.Println (log correlation). +func badInFmtPrintln() { + fmt.Println("/tmp/gh-aw/pre-agent-audit.txt") // want `hard-coded file path.*use constant constants\.AuditFilePath` +} + +// bad: ${RUNNER_TEMP} style path with no matching constant. +func badRunnerTempPath() string { + return "${RUNNER_TEMP}/gh-aw/mcp-servers.json" // want `hard-coded file path.*consider extracting` +} + +// bad: .github/ relative path with no matching constant. +func badGitHubPath() string { + return ".github/dependabot.yml" // want `hard-coded file path.*consider extracting` +} + +// ok: using the constant directly. +func okUsingConst() string { + return constants.ConfigFilePath +} + +// ok: const declaration itself is acceptable. +const localPathConst = "/tmp/gh-aw/local-file.txt" + +// ok: just a plain /tmp with no suffix (too short / generic). +func okTooShort() string { + return "/tmp" +} + +// ok: not a path at all. +func okNotAPath() string { + return "hello world" +} + +// ok: suppressed with nolint directive. +func okNolint() string { + return "/tmp/gh-aw/suppressed.log" //nolint:hardcodedfilepath +} + +// ok: path is a format string with a placeholder (not a complete path). +func okFormatVerb() string { + return fmt.Sprintf("/tmp/gh-aw/runs/%s/output.json", "run-id") +} + +// ok: very short path segment (no trailing slash after prefix). +func okShortSegment() string { + return ".github" +} diff --git a/pkg/linters/spec_test.go b/pkg/linters/spec_test.go index fac2268888c..c061f1ccfe2 100644 --- a/pkg/linters/spec_test.go +++ b/pkg/linters/spec_test.go @@ -20,6 +20,7 @@ import ( "github.com/github/gh-aw/pkg/linters/fileclosenotdeferred" "github.com/github/gh-aw/pkg/linters/fmterrorfnoverbs" "github.com/github/gh-aw/pkg/linters/fprintlnsprintf" + "github.com/github/gh-aw/pkg/linters/hardcodedfilepath" "github.com/github/gh-aw/pkg/linters/jsonmarshalignoredeerror" "github.com/github/gh-aw/pkg/linters/largefunc" "github.com/github/gh-aw/pkg/linters/lenstringzero" @@ -50,7 +51,7 @@ type docAnalyzer struct { } // documentedAnalyzers returns the analyzer subpackages documented in the README -// "Public API > Subpackages" table. The README documents 24 analyzer +// "Public API > Subpackages" table. The README documents 25 analyzer // subpackages (the non-analyzer `internal` helper subpackage is excluded because // it exposes no Analyzer). // @@ -58,7 +59,7 @@ type docAnalyzer struct { // // contextcancelnotdeferred, ctxbackground, excessivefuncparams, errormessage, // errstringmatch, execcommandwithoutcontext, fileclosenotdeferred, fmterrorfnoverbs, fprintlnsprintf, -// jsonmarshalignoredeerror, largefunc, lenstringzero, manualmutexunlock, +// hardcodedfilepath, jsonmarshalignoredeerror, largefunc, lenstringzero, manualmutexunlock, // osexitinlibrary, ossetenvlibrary, panic-in-library-code, rawloginlib, // regexpcompileinfunction, seenmapbool, sortslice, ssljson, // strconvparseignorederror, tolowerequalfold, uncheckedtypeassertion @@ -73,6 +74,7 @@ func documentedAnalyzers() []docAnalyzer { {"fileclosenotdeferred", fileclosenotdeferred.Analyzer}, {"fmterrorfnoverbs", fmterrorfnoverbs.Analyzer}, {"fprintlnsprintf", fprintlnsprintf.Analyzer}, + {"hardcodedfilepath", hardcodedfilepath.Analyzer}, {"jsonmarshalignoredeerror", jsonmarshalignoredeerror.Analyzer}, {"largefunc", largefunc.Analyzer}, {"lenstringzero", lenstringzero.Analyzer},