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
2 changes: 2 additions & 0 deletions cmd/linters/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/github/gh-aw/pkg/linters/excessivefuncparams"
"github.com/github/gh-aw/pkg/linters/fileclosenotdeferred"
"github.com/github/gh-aw/pkg/linters/fprintlnsprintf"
"github.com/github/gh-aw/pkg/linters/jsonmarshalignoredeerror"
"github.com/github/gh-aw/pkg/linters/largefunc"
"github.com/github/gh-aw/pkg/linters/manualmutexunlock"
"github.com/github/gh-aw/pkg/linters/osexitinlibrary"
Expand Down Expand Up @@ -53,6 +54,7 @@ func main() {
regexpcompileinfunction.Analyzer,
ssljson.Analyzer,
strconvparseignorederror.Analyzer,
jsonmarshalignoredeerror.Analyzer,
uncheckedtypeassertion.Analyzer,
)
}
79 changes: 79 additions & 0 deletions pkg/linters/jsonmarshalignoredeerror/jsonmarshalignoredeerror.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Package jsonmarshalignoredeerror implements a Go analysis linter that flags
// json.Marshal and json.Unmarshal calls where the error return is discarded with _.
package jsonmarshalignoredeerror

import (
"go/ast"
"go/types"

"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)

// Analyzer is the json-marshal-ignored-error analysis pass.
var Analyzer = &analysis.Analyzer{
Name: "jsonmarshalignoredeerror",
Doc: "reports json.Marshal and json.Unmarshal calls where the error return is discarded with _",
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in analyzer Name (and package/directory name): jsonmarshalignoredeerror contains a double-e"ignoredeerror" should be "ignorederror".

💡 Details

The misspelling is baked into:

  • The directory name jsonmarshalignoredeerror/
  • The package declaration
  • Analyzer.Name (the string that appears in diagnostics and //nolint:jsonmarshalignoredeerror suppressions)

Users writing //nolint suppressions will hit a silent no-op if they spell the analyzer name correctly (jsonmarshalignorederror), because the registered name is the typo'd form. A rename now (before widespread suppression comments accumulate) is cheaper than a later migration.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/grill-with-docs] Typo in the public analyzer Name: the package is spelled jsonmarshalignoredeerror (double e: ignoredd + errore = de but the package has dee). This string appears in lint output and is a permanent API identifier once merged.

💡 Suggested fix

Rename throughout (directory, package declaration, Analyzer.Name, Analyzer.Doc, and URL):

  • jsonmarshalignoredeerror (current — double e)
  • jsonmarshalignoredererror or more readably jsonmarshalignoredeerror

Precisely: json + marshal + ignored + errorjsonmarshalignoredererror. Check: ignored = i-g-n-o-r-e-d, error = e-r-r-o-r, so joined = ignorederror. Current name ignoredeerror inserts an extra e.

URL: "https://github.com/github/gh-aw/tree/main/pkg/linters/jsonmarshalignoredeerror",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}
Comment on lines +14 to +21

func run(pass *analysis.Pass) (any, error) {
insp := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{(*ast.AssignStmt)(nil)}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The linter only inspects *ast.AssignStmt nodes, so it misses json.Unmarshal(data, &v) called as a bare expression statement — which is also a discarded error and arguably more common.

💡 Suggested fix

Add (*ast.ExprStmt)(nil) to the nodeFilter and handle it:

nodeFilter := []ast.Node{(*ast.AssignStmt)(nil), (*ast.ExprStmt)(nil)}

// in the callback:
exprStmt, ok := n.(*ast.ExprStmt)
if ok {
    call, ok := exprStmt.X.(*ast.CallExpr)
    if ok {
        if isJSONFunc(pass, call, "Unmarshal") {
            pass.ReportRangef(call, "error return from json.Unmarshal is discarded; unmarshal failures leave the target value in a partial state")
        }
    }
}

Add a fixture in the test data:

func BadBare() {
    var f Foo
    json.Unmarshal([]byte(`{}`), &f) // want `error return from json\.Unmarshal is discarded`
}

insp.Preorder(nodeFilter, func(n ast.Node) {
assign, ok := n.(*ast.AssignStmt)
if !ok {
return
}

// Pattern: val, _ := json.Marshal(x) — 2 lhs, 1 rhs, Lhs[1] is blank
if len(assign.Lhs) == 2 && len(assign.Rhs) == 1 {
blank, ok := assign.Lhs[1].(*ast.Ident)
if ok && blank.Name == "_" {
call, ok := assign.Rhs[0].(*ast.CallExpr)
if ok {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] json.MarshalIndent also returns ([]byte, error) and is used throughout the codebase, but the linter only checks for "Marshal" by exact name — MarshalIndent calls with a discarded error will be silently skipped.

💡 Suggested fix

Extend isJSONFunc to accept a list of names, or add a second check:

// Check for both Marshal and MarshalIndent
for _, fn := range []string{"Marshal", "MarshalIndent"} {
    if isJSONFunc(pass, call, fn) {
        pass.ReportRangef(call, "error return from json.%s is discarded; marshal failures produce nil bytes silently", fn)
    }
}

Add a fixture:

val2, _ := json.MarshalIndent(f, "", "  ") // want `error return from json\.MarshalIndent is discarded`
_ = val2

if isJSONFunc(pass, call, "Marshal") {
pass.ReportRangef(call, "error return from json.Marshal is discarded; marshal failures produce nil bytes silently")
}
}
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

json.MarshalIndent is not covered: the linter silently ignores discarded errors from json.MarshalIndent, which has the same ([]byte, error) signature and the same failure modes.

💡 Suggested fix

Extend the Marshal branch to also check MarshalIndent:

if isJSONFunc(pass, call, "Marshal") || isJSONFunc(pass, call, "MarshalIndent") {
    pass.ReportRangef(call, "error return from json.Marshal/MarshalIndent is discarded; marshal failures produce nil bytes silently")
}

Or make isJSONFunc accept a variadic list of names:

func isJSONFunc(pass *analysis.Pass, call *ast.CallExpr, names ...string) bool {
    ...
    for _, n := range names {
        if sel.Sel.Name == n { return true }
    }
    return false
}

json.MarshalIndent can fail for the same reasons (json.Marshal does (cyclic values, unsupported types) and is used widely in formatting and logging paths.

// Pattern: _ = json.Unmarshal(data, &v) — 1 lhs, 1 rhs, Lhs[0] is blank
if len(assign.Lhs) == 1 && len(assign.Rhs) == 1 {
blank, ok := assign.Lhs[0].(*ast.Ident)
if ok && blank.Name == "_" {
call, ok := assign.Rhs[0].(*ast.CallExpr)
if ok {
if isJSONFunc(pass, call, "Unmarshal") {
pass.ReportRangef(call, "error return from json.Unmarshal is discarded; unmarshal failures leave the target value in a partial state")
}
}
}
}
})
return nil, nil
}

func isJSONFunc(pass *analysis.Pass, call *ast.CallExpr, name string) bool {
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return false
}
if sel.Sel.Name != name {
return false
}
ident, ok := sel.X.(*ast.Ident)
if !ok {
return false
}
obj := pass.TypesInfo.Uses[ident]
pkgName, ok := obj.(*types.PkgName)
if !ok {
return false
}
return pkgName.Imported().Path() == "encoding/json"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//go:build !integration

package jsonmarshalignoredeerror_test

import (
"testing"

"golang.org/x/tools/go/analysis/analysistest"

"github.com/github/gh-aw/pkg/linters/jsonmarshalignoredeerror"
)

func TestAnalyzer(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, jsonmarshalignoredeerror.Analyzer, "jsonmarshalignoredeerror")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package jsonmarshalignoredeerror

import "encoding/json"

type Foo struct{ X int }

func Bad() {
f := Foo{X: 1}
val, _ := json.Marshal(f) // want `error return from json\.Marshal is discarded`
_ = val

var f2 Foo
_ = json.Unmarshal([]byte(`{}`), &f2) // want `error return from json\.Unmarshal is discarded`
}

func Good() error {
f := Foo{X: 1}
val, err := json.Marshal(f)
if err != nil {
return err
}
_ = val

var f2 Foo
if err := json.Unmarshal([]byte(`{}`), &f2); err != nil {
return err
}
return nil
}
Loading