{"id":19865761,"url":"https://github.com/Antonboom/nilnil","last_synced_at":"2025-05-02T05:32:00.239Z","repository":{"id":42393092,"uuid":"405848533","full_name":"Antonboom/nilnil","owner":"Antonboom","description":"The Golang linter that checks that there is no simultaneous return of `nil` error and an invalid value.","archived":false,"fork":false,"pushed_at":"2025-04-01T08:52:32.000Z","size":93,"stargazers_count":65,"open_issues_count":1,"forks_count":5,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-24T16:02:52.839Z","etag":null,"topics":["go","golang","linter","static-analysis"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Antonboom.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-09-13T05:49:12.000Z","updated_at":"2025-04-08T05:05:48.000Z","dependencies_parsed_at":"2022-09-26T20:11:03.252Z","dependency_job_id":"5c750b69-0821-43b6-894e-fef82a853f74","html_url":"https://github.com/Antonboom/nilnil","commit_stats":null,"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Antonboom%2Fnilnil","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Antonboom%2Fnilnil/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Antonboom%2Fnilnil/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Antonboom%2Fnilnil/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Antonboom","download_url":"https://codeload.github.com/Antonboom/nilnil/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251992985,"owners_count":21677022,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go","golang","linter","static-analysis"],"created_at":"2024-11-12T15:24:07.000Z","updated_at":"2025-05-02T05:31:55.231Z","avatar_url":"https://github.com/Antonboom.png","language":"Go","funding_links":[],"categories":["Go"],"sub_categories":[],"readme":"# nilnil\n\n![Latest release](https://img.shields.io/github/v/release/Antonboom/nilnil)\n[![CI](https://github.com/Antonboom/nilnil/actions/workflows/ci.yml/badge.svg)](https://github.com/Antonboom/nilnil/actions/workflows/ci.yml)\n[![Go Report Card](https://goreportcard.com/badge/github.com/Antonboom/nilnil)](https://goreportcard.com/report/github.com/Antonboom/nilnil)\n[![Coverage](https://coveralls.io/repos/github/Antonboom/nilnil/badge.svg?branch=master)](https://coveralls.io/github/Antonboom/nilnil?branch=master)\n[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)\n\nChecks that there is no simultaneous return of `nil` error and an invalid value.\n\n## Installation \u0026 usage\n\n```\n$ go install github.com/Antonboom/nilnil@latest\n$ nilnil ./...\n```\n\n## Motivation\n\n`return nil, nil` is not idiomatic for Go. The developers are used to the fact that \nif there is no error, then the return value is valid and can be used without additional checks:\n```go\nuser, err := getUser()\nif err != nil {\n    return err\n}\nif user != nil { // Ambiguous!\n    // Use user.\n}\n```\n\nAt best this leads to comments [like](https://github.com/golang/go/blob/07fc59199b9522bfe0d14f35c4391394efc336c9/src/debug/dwarf/line.go#L142)\n\n```go\n// ...\n// If this compilation unit has no line table, it returns nil, nil.\nfunc (d *Data) LineReader(cu *Entry) (*LineReader, error) {\n```\n\nwhich is not a _true_ constraint. And in the worst case such code can lead to **panic**.\n\u003cbr\u003e\n\nRewrite the example above with sentinel error:\n```go\nuser, err := getUser()\nif errors.Is(err, errUserNotFound) {\n    // Do something and return.\n}\nif err != nil {\n    return err\n}\n\n// Use user.\n```\n\n### Opposite situation\n\nSometimes people consider the opposite situation (returning a non-nil error and a valid value at the same time) to be\nan anti-pattern too, since it can lead to hard-to-find bugs.\n\nFor example, this **kubernetes** code\n\n```go\nfunc (fh *fakeHistory) UpdateControllerRevision(revision *apps.ControllerRevision, newRevision int64) (*apps.ControllerRevision, error) {\n    clone := revision.DeepCopy()\n    clone.Revision = newRevision\n    return clone, fh.indexer.Update(clone)\n}\n```\n\ncan be rewritten as follows\n\n```go\nfunc (fh *fakeHistory) UpdateControllerRevision(revision *apps.ControllerRevision, newRevision int64) (*apps.ControllerRevision, error) {\n    clone := revision.DeepCopy()\n    clone.Revision = newRevision\n    if err := fh.indexer.Update(clone); err != nil {\n        return nil, fmt.Errorf(\"update index: %w\", err)\n    }\n    return clone, nil\n}\n```\n\n### What if I think it's bullshit?\n\nI understand that each case needs to be analyzed separately, \nbut I hope that the linter will make you think again –\nis it necessary to use an **ambiguous API** or is it better to do it using a sentinel error?\n\u003cbr\u003e\n\nIn any case, you can just not enable the linter.\n\n## Configuration\n\n### CLI\n\n```shell\n$ nilnil --checked-types chan,func,iface,map,ptr,uintptr,unsafeptr ./...\n$ nilnil --detect-opposite ./...\n$ nilnil --detect-opposite --checked-types ptr ./..\n```\n\n### golangci-lint\n\nhttps://golangci-lint.run/usage/linters/#nilnil\n\n```yaml\nlinters-settings:\n  nilnil:\n    # In addition, detect opposite situation (simultaneous return of non-nil error and valid value).\n    # Default: false\n    detect-opposite: true\n    # List of return types to check.\n    # Default: [\"chan\", \"func\", \"iface\", \"map\", \"ptr\", \"uintptr\", \"unsafeptr\"]\n    checked-types:\n      - chan\n      - func\n      - iface\n      - map\n      - ptr\n      - uintptr\n      - unsafeptr\n```\n\n## Examples\n\n\u003cdetails\u003e\n  \u003csummary\u003eparsePublicKey from crypto/tls\u003c/summary\u003e\n\n```go\n// BEFORE\n\nfunc parsePublicKey(algo PublicKeyAlgorithm, keyData *publicKeyInfo) (interface{}, error) {\n    der := cryptobyte.String(keyData.PublicKey.RightAlign())\n    switch algo {\n    case RSA:\n        // ...\n        return pub, nil\n    case ECDSA:\n        // ...\n        return pub, nil\n    case Ed25519:\n        // ...\n        return ed25519.PublicKey(der), nil\n    case DSA:\n        // ...\n        return pub, nil\n    default:\n        return nil, nil\n    }\n}\n\n// AFTER\n\nvar errUnknownPublicKeyAlgo = errors.New(\"unknown public key algo\")\n\nfunc parsePublicKey(algo PublicKeyAlgorithm, keyData *publicKeyInfo) (interface{}, error) {\n    der := cryptobyte.String(keyData.PublicKey.RightAlign())\n    switch algo {\n    case RSA:\n        // ...\n        return pub, nil\n    case ECDSA:\n        // ...\n        return pub, nil\n    case Ed25519:\n        // ...\n        return ed25519.PublicKey(der), nil\n    case DSA:\n        // ...\n        return pub, nil\n    default:\n        return nil, fmt.Errorf(\"%w: %v\", errUnknownPublicKeyAlgo, algo)\n    }\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003ehttp2clientConnReadLoop from net/http\u003c/summary\u003e\n\n```go\n// BEFORE\n\n// As a special case, handleResponse may return (nil, nil) to skip the frame.\nfunc (rl *http2clientConnReadLoop) handleResponse(/* ... */) (*Response, error) {\n    if statusCode \u003e= 100 \u0026\u0026 statusCode \u003c= 199 {\n        return nil, nil\n    }\n}\n\n// ...\nres, err := rl.handleResponse(cs, f)\nif err != nil {\n\treturn err\n}\nif res == nil {\n    // (nil, nil) special case. See handleResponse docs.\n    return nil\n}\n\n// AFTER\n\nvar errNeedSkipFrame = errors.New(\"need skip frame\")\n\n// As a special case, handleResponse may return errNeedSkipFrame to skip the frame.\nfunc (rl *http2clientConnReadLoop) handleResponse(/* ... */) (*Response, error) {\n    if statusCode \u003e= 100 \u0026\u0026 statusCode \u003c= 199 {\n        return nil, errNeedSkipFrame\n    }\n}\n\n// ...\nres, err := rl.handleResponse(cs, f)\nif errors.Is(err, errNeedSkipFrame) {\n    return nil\n}\nif err != nil {\n    return err\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eNot implemented\u003c/summary\u003e\n\n```go\n// BEFORE\n\nfunc (s *Service) StartStream(ctx context.Context) (*Stream, error) {\n    return nil, nil\n}\n\n// AFTER\n\nfunc (s *Service) StartStream(ctx context.Context) (*Stream, error) {\n    return nil, errors.New(\"not implemented\")\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003enil-safe type\u003c/summary\u003e\n\n```go\npackage ratelimiter\n\ntype RateLimiter struct {\n    // ...\n}\n\nfunc New() (*RateLimiter, error) {\n    // It's OK, RateLimiter is nil-safe.\n    // But it's better not to do it anyway.\n    return nil, nil\n}\n\nfunc (r *RateLimiter) Allow() bool {\n    if r == nil {\n        return true\n    }\n    return r.allow()\n}\n```\n\n\u003c/details\u003e\n\n## Assumptions\n\n- Linter only checks functions with two return arguments, the last of which implements `error`.\n- Next types are checked:\n  * pointers (including `uinptr` and `unsafe.Pointer`), functions and interfaces (`panic: invalid memory address or nil pointer dereference`);\n  * maps (`panic: assignment to entry in nil map`);\n  * channels (`fatal error: all goroutines are asleep - deadlock!`)\n- Only explicit `return nil, nil` are supported.\n\n## Check Go 1.22.2 source code\n\n\u003cdetails\u003e\n  \u003csummary\u003eClick to expand\u003c/summary\u003e\n\n```shell\n$ cd $GOROOT/src\n$ nilnil ./... 2\u003e\u00261 | grep -v \"_test.go\"  \n/usr/local/go/src/internal/bisect/bisect.go:196:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/net/fd_unix.go:71:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/net/fd_unix.go:79:4: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/net/fd_unix.go:156:4: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/net/iprawsock_posix.go:36:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/net/tcpsock_posix.go:38:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/net/udpsock_posix.go:37:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/net/unixsock_posix.go:92:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/crypto/tls/key_agreement.go:46:2: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/crypto/tls/ticket.go:355:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/crypto/tls/ticket.go:359:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/database/sql/driver/types.go:157:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/database/sql/driver/types.go:232:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/database/sql/driver/types.go:263:4: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/database/sql/convert.go:548:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/database/sql/sql.go:205:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/database/sql/sql.go:231:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/database/sql/sql.go:257:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/database/sql/sql.go:284:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/database/sql/sql.go:311:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/database/sql/sql.go:337:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/database/sql/sql.go:363:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/database/sql/sql.go:389:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/database/sql/sql.go:422:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/debug/dwarf/entry.go:884:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/debug/dwarf/line.go:146:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/debug/dwarf/line.go:153:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/debug/dwarf/typeunit.go:138:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/debug/pe/file.go:470:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/net/http/h2_bundle.go:9530:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/net/http/transfer.go:765:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/net/http/transfer.go:775:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/net/http/transfer.go:798:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/go/build/build.go:1442:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/go/build/build.go:1453:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/go/build/build.go:1457:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/go/build/build.go:1491:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/go/internal/gccgoimporter/ar.go:125:3: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/image/jpeg/reader.go:622:5: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/image/png/reader.go:434:4: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/internal/profile/legacy_profile.go:1089:4: return both a `nil` error and an invalid value: use a sentinel error instead\n/usr/local/go/src/net/internal/socktest/switch.go:142:3: return both a `nil` error and an invalid value: use a sentinel error instead\n```\n\n\u003c/details\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FAntonboom%2Fnilnil","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FAntonboom%2Fnilnil","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FAntonboom%2Fnilnil/lists"}