closewrite

package module
v0.1.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 24, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package closewrite provides a go/analysis analyzer that reports a discarded Close error on a file opened for WRITING.

Close is where a write is finally decided. The kernel may defer the failure of an earlier Write until the descriptor is closed — a full disk, a quota, a dropped network filesystem — so a discarded Close error on a written file is the program declaring success over a file that may be truncated, empty, or absent. Eight such defects were found by hand across this fleet; six were real data loss, and every one of them read as a routine `defer f.Close()`.

The rule is deliberately narrow, because a gate that cries wolf is worse than no gate. It fires only where the file's own creation proves the intent to write: os.Create, os.CreateTemp, or os.OpenFile whose FLAG argument carries O_WRONLY, O_RDWR, O_CREATE, O_APPEND or O_TRUNC. The permission argument is never read as a flag: it is a mode bitmask that collides with the flag values, so reading it reported read-only opens, and reported them differently on darwin and on linux.

What is discarded, and where

A Close whose error goes nowhere is reported wherever the analyzer can see that it is UNCONDITIONAL — `defer f.Close()`, `f.Close()` and `_ = f.Close()` throw away the same error and lose the same data, so the rule cannot turn on the `defer` keyword without making evasion one token cheaper than compliance. The remedy in every spelling is to bind the error: return it, assign it, or take it out of a deferred closure with `defer func() { err = f.Close() }()`.

Every exemption

Each of these silences a Close that the clauses above would otherwise report. They are the complete set; anything else that goes unreported is a scope limitation below, or a defect.

  1. A reader's Close. Nothing is lost by failing to close a file you only read, which keeps the overwhelmingly common `defer f.Close()` after os.Open silent, exactly as it should be.
  2. A close reached only through a BRANCH — an `if`, a loop, a `switch`, a label. The shape this exists for is cleanup on a path that has already failed, `if err != nil { _ = f.Close(); return err }`, where the caller is about to be told about a failure that is not this one. Which branch is the failing one is NOT decided here, and deliberately: an earlier revision recognised the check and an adversarial pass wrote the identical cleanup five other ways it missed, while `if <a non-nil error> != nil { … }` silenced a true finding in two lines. A `defer` statement is not covered by this, because deferring is itself the unconditional act.
  3. A close already handled elsewhere in the same body: a bound `f.Close()`, or a bound SEAM — a call handed the file whose result is an error and NOTHING ELSE, which is what `return closeOutput(file)` looks like and is the sanctioned repair for this rule. A second result is what separates a write from a close: io.Copy, Fprintln and Write all hand back a count, and a close has nothing to count.

Every scope limitation

These are not exemptions — nothing here was judged and forgiven. They are shapes the analyzer cannot see, and each is a silence a reader should not mistake for approval.

  1. One function body. A file handed to another function is that function's responsibility, and widening this would mean guessing about ownership. A close moved into a closure assigned to a variable is out of reach for the same reason.
  2. Plain identifiers on both sides. A file held in a field — `h.f, err = os.Create(p)` with `defer h.f.Close()` — is neither opened nor closed as far as this rule can tell.
  3. Flag resolution reaches a literal `os.O_*` selector, a constant the checker folded, and one level of assignment to a local variable in the same body. A flag arriving as a parameter, bound from a call's second result, or spread from a call supplying the whole argument list, resolves to nothing and the open is silent.
  4. A seam is recognised by its RESULT, and no signature separates a close from anything else shaped like one. A bound `writeAll(f) error` settles the file although it closes nothing, and a bound `closeBoth(f, g) error` settles both files whether or not it closes either.
  5. Test files are judged like any other source: this analyzer declares no test scope and the yze runner does not list it as source-only.

Index

Constants

This section is empty.

Variables

View Source
var Analyzer = &analysis.Analyzer{
	Name:     "closewrite",
	Doc:      "reports a discarded Close error on a file opened for writing, where the loss is real data",
	Requires: []*analysis.Analyzer{inspect.Analyzer},
	Run:      run,
}

Analyzer reports discarded Close errors on files opened for writing.

View Source
var Registration = goyze.Registration{
	Precision:  goyze.PrecisionExact,
	Name:       "closewrite",
	Categories: []goyze.Category{"errors"},
	URL:        "https://docs.gomatic.dev/yze/closewrite",
	Analyzer:   Analyzer,
}

Registration declares this analyzer to the yze framework.

Functions

This section is empty.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL