Skip to content
nathantsPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

31 Commits

Folders and files

Repository files navigation

Libcheck

Why

Every repository grows its own lint script: a loop that checks tools are installed, linters invoked one by one, advisory steps that never fail, and configs that drift apart from every other repository's.

I want one command that runs the same strict policy on every Go and TypeScript repository, fails on every finding, and needs no configuration to start.

Linting should:

  • Be one command
  • Need no configuration
  • Gate on every finding
  • Never modify files unless asked
  • Never install anything
  • Pin every tool it runs

What

Libcheck is a thin wrapper around proven engines. The policy lives in libcheck, so a repository keeps no engine configuration of its own.

  • Go: gofmt, golangci-lint, go mod tidy, deadcode, and govulncheck.
  • TypeScript: oxlint with type-aware rules, the project's own tsc or tsgo with stricter options, Prettier, and pnpm audit.

Install

go install github.com/nathants/libcheck@latest

export PATH=$PATH:$(go env GOPATH)/bin

Libcheck needs git and Go 1.27 or later. The TypeScript engines need Node, and libcheck security audits pnpm lockfiles with pnpm.

Libcheck runs globally installed tools at pinned versions and never installs them. libcheck tools verifies each one and prints the command that installs any tool that is missing or at the wrong version. TypeScript projects provide their own tsc or tsgo through their lockfile, since the compiler version must match the project's code. go and pnpm run as installed: every go command runs with GOTOOLCHAIN=local and GOWORK=off, and pnpm with pnpm_config_pm_on_fail=ignore, so neither downloads the version a project pins. Go steps fetch missing module dependencies into the module cache, as any go build does.

Usage

libcheck check      # verify tools, then lint without modifying files (the default)
libcheck fix        # apply gofmt, go mod tidy, and prettier fixes
libcheck security   # govulncheck and pnpm audit (needs network)
libcheck tools      # verify every installed tool version
libcheck -C DIR     # check the repository at DIR
libcheck -only errcheck,gofmt   # run only these steps or golangci-lint linters

A repository's check fails fast when libcheck is missing:

command -v libcheck >/dev/null || { echo 'libcheck not found; install it with: go install github.com/nathants/libcheck@latest' >&2; exit 1; }
libcheck check

Discovery

Libcheck discovers every Go module (go.mod) and TypeScript project (tsconfig.json) among the files git lists, so ignored build output is never checked. A nested module or project owns its own files.

Go

Each Go module runs gofmt -l, golangci-lint, go mod tidy -diff, and deadcode when it has main packages. golangci-lint also checks each standalone program, a file constrained by //go:build ignore and run with go run FILE, by name, since package patterns skip it.

golangci-lint and deadcode analyze the host's build. A module with "goos": ["linux", "windows"] is analyzed once per listed GOOS, and each build must pass on its own, so a helper in a shared file that only Windows code calls is unused in the Linux build: move it into a Windows file. A GOOS other than the host's builds without cgo.

golangci-lint runs asasalint, bidichk, bodyclose, durationcheck, errcheck, errorlint, exhaustive, gocheckcompilerdirectives, govet (defaults plus nilness and unusedwrite), ineffassign, modernize, nilnesserr, nolintlint, reassign, recvcheck, revive unused-parameter, staticcheck, unconvert, unparam, unused, usetesting, and wastedassign. usetesting reports os.MkdirTemp only when its parent directory is "", as it does os.CreateTemp; an explicit parent counts only on the call's first line. errorlint accepts an fmt.Errorf whose format string wraps at least one error with %w, so other errors can be detail formatted with %v. unparam skips parameters that always receive the same argument.

errorlint accepts err == io.EOF only when every assignment to err comes from a call documented to return io.EOF unwrapped, such as io.Reader.Read. When it flags one because the variable also holds other errors, give the Read error its own variable rather than switching to errors.Is, which also accepts a wrapped EOF.

exhaustive requires every switch over an enum to have a default case and to list every member, including for enums from other modules, so the switch shows every value. List the members the default handles as a case that falls through to it. With "exhaustive": "default-signifies-exhaustive", a switch with a default case passes, and one without must list every member.

go mod tidy -diff fails on a requirement kept only to raise a module that no package in the build imports, such as a floor above a vulnerable version that appears only in the module graph. Instead, exclude the older dependency version whose requirements bring the vulnerable one in; tidy keeps the exclude.

modernize's omitzero finding flags an omitempty that never omits anything, such as on a time.Time field. Switching to omitzero changes the encoded JSON, so remove the no-op omitempty instead unless omitting zero values is intended.

errcheck has no autofix. Rewriting defer f.Close() as defer func() { _ = f.Close() }() moves the evaluation of f from the defer statement to function exit, which changes behavior if f is reassigned afterward or the defer sits in a loop. Pass the value in instead: defer func(f *os.File) { _ = f.Close() }(f).

TypeScript

Each TypeScript project runs:

  • oxlint with type-aware rules: the correctness category, whose no-unused-vars accepts properties destructured only to leave them out of a rest element and whose no-floating-promises accepts node:test's test and suite calls, plus rules-of-hooks, ban-ts-comment, no-deprecated, no-misused-promises, only-throw-error, and switch-exhaustiveness-check.
  • The project's own tsc or tsgo with stricter options added: noFallthroughCasesInSwitch, noImplicitOverride, noImplicitReturns, noUncheckedIndexedAccess, and noUnusedParameters.
  • prettier --check over the project's files with libcheck's Prettier settings. Project Prettier configs and .editorconfig are ignored, and package manager lockfiles are skipped.

Every JavaScript or TypeScript file must belong to a project, a tsconfig.json in its directory or above it, and be compiled by that tsconfig.json, since type-aware rules find a file's types through the nearest tsconfig.json. A file outside every project, or outside its project's program, fails the check until a tsconfig includes it or libcheck.json excludes it.

oxlint reads tsconfig.json as written, while libcheck adds --noEmit only to tsc. A project whose tsconfig sets allowJs without noEmit or outDir fails oxlint with a tsconfig-error, because each JavaScript input would also be its own output. Set noEmit when a bundler emits the code.

Security

libcheck security queries vulnerability databases over the network, so libcheck check leaves it out. It runs:

  • govulncheck on each Go module, once per listed GOOS, which reports the vulnerabilities the code can reach.
  • pnpm audit in the directory of each pnpm-lock.yaml, which reports every advisory for a locked package, devDependencies included. The projects of a workspace share its lockfile, so each lockfile is audited once.

libcheck.json

Optional. It narrows discovery and records exceptions:

{
  "exclude": ["scratch", "third_party"],
  "go": {
    ".": {
      "tags": ["integration"],
      "goos": ["linux", "windows"],
      "disable": ["bodyclose"],
      "exhaustive": "default-signifies-exhaustive",
      "modernizeDisable": ["any", "stringsseq"],
      "deadcodeIgnore": ["example.com/app/internal/term.clearScreen"]
    }
  }
}
  • include or exclude (not both): repository-relative directories, each covering everything below it. The compiler still type-checks a left-out source file that a checked file imports, such as vendored code, but its diagnostics are dropped.
  • go is keyed by module directory; a key that names no checked module is an error.
  • goos lists the operating systems whose builds golangci-lint, deadcode, and govulncheck analyze.
  • disable takes golangci-lint linter names or deadcode.
  • deadcodeIgnore names functions whose callers deadcode cannot see, such as cgo exports. An entry that matches nothing in any GOOS fails the check.

Suppressions

Suppress a single golangci-lint finding with //nolint:LINTER // reason. nolintlint fails a directive that names no linter, gives no reason, or suppresses nothing. staticcheck's //lint:ignore is not honored.

Suppress an oxlint finding with // oxlint-disable-next-line PLUGIN/RULE -- reason. oxlint fails an oxlint-disable or eslint-disable directive that suppresses nothing, and the suppressions step fails one that names no rule or gives no reason after --. That step scans text, not syntax, so a directive inside a string literal counts too. ban-ts-comment bans @ts-ignore and @ts-nocheck and requires a description after @ts-expect-error.

The suppressions step also fails any directive that names exhaustive-deps or rules-of-hooks. Such a directive turns off oxlint's React Compiler rules, such as immutability, refs, and set-state-in-effect, for the whole component, as the React Compiler skips such components. Fix the finding instead; useEffectEvent lets an effect read current values without listing them as dependencies.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages