Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -34,6 +34,7 @@ import (
"github.com/github/gh-aw/pkg/linters/httpnoctx"
"github.com/github/gh-aw/pkg/linters/httprespbodyclose"
"github.com/github/gh-aw/pkg/linters/httpstatuscode"
"github.com/github/gh-aw/pkg/linters/ioutildeprecated"
"github.com/github/gh-aw/pkg/linters/jsonmarshalignoredeerror"
"github.com/github/gh-aw/pkg/linters/largefunc"
"github.com/github/gh-aw/pkg/linters/lenstringsplit"
Expand Down Expand Up @@ -82,6 +83,7 @@ func main() {
hardcodedfilepath.Analyzer,
httpnoctx.Analyzer,
httprespbodyclose.Analyzer,
ioutildeprecated.Analyzer,
httpstatuscode.Analyzer,
largefunc.Analyzer,
manualmutexunlock.Analyzer,
Expand Down
83 changes: 83 additions & 0 deletions pkg/linters/ioutildeprecated/ioutildeprecated.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Package ioutildeprecated implements a Go analysis linter that flags calls to
// functions from the deprecated io/ioutil package and suggests their replacements
// in the io and os packages (deprecated since Go 1.16).
package ioutildeprecated

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

"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"

"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"
)

// replacements maps deprecated ioutil function names to their modern equivalents.
var replacements = map[string]string{
"ReadAll": "io.ReadAll",
"ReadFile": "os.ReadFile",
"WriteFile": "os.WriteFile",
"TempFile": "os.CreateTemp",
"TempDir": "os.MkdirTemp",
"ReadDir": "os.ReadDir",
"NopCloser": "io.NopCloser",
"Discard": "io.Discard",
}

// Analyzer is the ioutil-deprecated analysis pass.
var Analyzer = &analysis.Analyzer{
Name: "ioutildeprecated",
Doc: "reports uses of deprecated io/ioutil functions that should be replaced with io or os package equivalents",
URL: "https://github.com/github/gh-aw/tree/main/pkg/linters/ioutildeprecated",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}

func run(pass *analysis.Pass) (any, error) {
root, err := astutil.Root(pass)
if err != nil {
return nil, err
}
noLintLinesByFile := nolint.BuildLineIndex(pass, "ioutildeprecated")

for cur := range root.Preorder((*ast.SelectorExpr)(nil)) {
sel, ok := cur.Node().(*ast.SelectorExpr)
if !ok {
continue
}

pos := pass.Fset.PositionFor(sel.Pos(), false)
if filecheck.IsTestFile(pos.Filename) {
continue
}
if nolint.HasDirective(pos, noLintLinesByFile) {
continue
}

pkgIdent, ok := sel.X.(*ast.Ident)
if !ok {
continue
}
if pass.TypesInfo == nil {
continue
}
obj := pass.TypesInfo.ObjectOf(pkgIdent)
if obj == nil {
continue
}
pkgName, ok := obj.(*types.PkgName)
if !ok || pkgName.Imported().Path() != "io/ioutil" {
continue
}

funcName := sel.Sel.Name
if replacement, found := replacements[funcName]; found {
pass.ReportRangef(sel, "ioutil.%s is deprecated; use %s instead", funcName, replacement)
}
}

return nil, nil
}
16 changes: 16 additions & 0 deletions pkg/linters/ioutildeprecated/ioutildeprecated_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//go:build !integration

package ioutildeprecated_test

import (
"testing"

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

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

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

import (
"io/ioutil"
"os"
)

func BadReadAll() {
f, _ := os.Open("file.txt")
defer f.Close()
_, _ = ioutil.ReadAll(f) // want `ioutil\.ReadAll is deprecated`
}

func BadReadFile() {
_, _ = ioutil.ReadFile("file.txt") // want `ioutil\.ReadFile is deprecated`
}

func BadWriteFile() {
_ = ioutil.WriteFile("file.txt", []byte("hello"), 0644) // want `ioutil\.WriteFile is deprecated`
}

func BadTempFile() {
_, _ = ioutil.TempFile("", "prefix") // want `ioutil\.TempFile is deprecated`
}

func BadTempDir() {
_, _ = ioutil.TempDir("", "prefix") // want `ioutil\.TempDir is deprecated`
}

func BadReadDir() {
_, _ = ioutil.ReadDir(".") // want `ioutil\.ReadDir is deprecated`
}

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 GoodReadAll negative test doesn't actually call io.ReadAll — it opens a file and discards it, so it doesn't demonstrate that the modern replacement is accepted correctly.

💡 Suggested fix
func GoodReadAll() {
    f, _ := os.Open("file.txt")
    defer f.Close()
    _, _ = io.ReadAll(f) // no diagnostic expected — this is the modern API
}

This makes the test a genuine specification: "using io.ReadAll produces no warning."

@copilot please address this.


func GoodReadAll() {
f, _ := os.Open("file.txt")
defer f.Close()
// Using io.ReadAll is fine
_ = f
}

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] NopCloser and Discard are in the replacements map but have no corresponding test fixtures — so their detection is untested.

💡 Suggested additions
func BadNopCloser() {
    r := strings.NewReader("hello")
    _ = ioutil.NopCloser(r) // want `ioutil\.NopCloser is deprecated`
}

func BadDiscard() {
    _, _ = fmt.Fprintf(ioutil.Discard, "hello") // want `ioutil\.Discard is deprecated`
}

Without these cases the analyzer could silently stop reporting them and the test suite would still pass.

@copilot please address this.

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.

The testdata file covers 6 of the 8 entries in replacements but is missing test cases for NopCloser and Discard. Without these, the linter could silently fail to fire (or fire incorrectly) for those two entries. Please add cases such as:

func BadNopCloser() {
	r := strings.NewReader("")
	_ = ioutil.NopCloser(r) // want `ioutil\.NopCloser is deprecated`
}

func BadDiscard() {
	_, _ = io.Copy(ioutil.Discard, strings.NewReader("")) // want `ioutil\.Discard is deprecated`
}

@copilot please address this.

Loading