CommunityCoding & Developmentgithub.com

smith-ai-lab/smith-check

smith check, smith fmt and smith lint: checking a project before committing, formatting with .clang-format, clang-tidy with .clang-tidy, and on .smith files. Load before committing, when a check fails, or on 'code should be clang-formatted' and clang-tidy findings.

What is smith-check?

smith-check is a Claude Code agent skill that smith check, smith fmt and smith lint: checking a project before committing, formatting with .clang-format, clang-tidy with .clang-tidy, and on .smith files. Load before committing, when a check fails, or on 'code should be clang-formatted' and clang-tidy findings.

Works with✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/smith-ai-lab/smith/tree/HEAD/.agents/skills/smith-check

Installed? Explore more Coding & Development skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

smith check, fmt, lint and lsp

smith fmt, smith lint and smith lsp are clang-format, clang-tidy and clangd, linked into smith from the same LLVM as the compiler, so every machine formats and lints alike.

On the project: no arguments

The verbs mean the same in every project and read what its build.c declares; a project writes no task for them.

  • smith fmt rewrites every C, C++, Objective-C and .smith file git sees (tracked, and untracked but not ignored; .c .h .m .mm .cpp .cc .cxx .hpp .hh .smith .metal) to .clang-format (a .metal file is formatted as C++, the language clang-format gives an extension it does not know). smith fmt --dry-run (or -n, with --Werror or -i) and smith lint --quiet are the project forms too: options alone, no file names, so they never wait for standard input; any other option, or a file name, makes them the tools (smith fmt --assume-filename=x.c reads standard input). A file named in .clang-format-ignore (clang-format reads it itself: a generated parser.c, a fixture) is left alone. Outside a git repository it says to name the files (smith fmt -i <files>).
  • smith lint builds what the compiles read (generated headers), then runs clang-tidy with .clang-tidy on the project's own sources among the build's compiles (smith_object, and smith_compile_command for sources the build never compiles), side by side. Not .smith/, build/ outputs or packages. Every .smith source is linted as smith lint x.smith -- <flags> does: the build's own with their compile's command, and each other .smith file git sees with the nearest compile's command, except what .clang-format-ignore names (the one list of files that are not ordinary source: test inputs, deliberately wrong fixtures, generated and copied files; smith fmt reads it too, so there is no second list). A finding fails the lint; its last line says linted N files (smith check: lint ok: N files), and a project with .smith sources that lints none fails. A .clang-tidy in a folder (such as InheritParentConfig: true with other checks) applies there.
  • smith lint is incremental: clang-tidy's result for a file is cached under ~/.cache/smith/lint and replayed while the file, what it includes (for a .smith file, also the generated headers of the modules it imports), its .clang-tidy and smith are unchanged, so the last line reads smith lint: linted 180 files (3 run, 177 cached, 1.2 s) and an unchanged tree runs clang-tidy on no file. SMITH_LINT_CACHE=0 turns it off, SMITH_LINT_CACHE=why says why a file ran, smith cache clean lint empties it. A file whose clang-tidy fails is named in the last lines (smith lint: c.c: exited with status 1, or was killed by signal 9, which prints nothing of its own) and at the end of its own output.
  • smith lint --changed[=<ref>] lints only the files changed since the merge-base with <ref> (default: the branch's upstream, else origin/HEAD's branch or origin/main, never a local main; none of them is an error naming --changed=<ref>; the line it prints names the base; edits and untracked files count) and the files that include or import them; a changed .clang-tidy, build file, shim/ or patches/ selects every file, and nothing affected says so and succeeds. It also builds only what those files wait for. Plain smith lint lints everything (CI). A file that waits for a package the network does not give (the tree-sitter parser in a cloud sandbox) is skipped by name (smith lint: skipped runtime/tree_sitter_data.c: the tree-sitter parser it embeds cannot be fetched (<url>)) and the rest is linted; CI (CI or SMITH_PRIORITY=ci) fails instead. smith lint FILE in a project not built yet (no compile_commands.json) stops and says to run smith build smith_needs once, rather than report the missing generated headers as errors.
  • smith docs checks knowledge/ and the skills (smith-docs).
  • smith check runs fmt (clang-format's dry run), docs, lint, test and prove, in that order, and does not stop at a failure: each part prints one line, smith check: <part> ok: <detail> (<n> s wall, <n> s cpu) or FAILED (status N) with a bounded excerpt of the failure and the path of the full log (.smith/log/<part>.log; the output of a passing part is there too; smith check -v prints everything as it happens), then smith check: promises: N (what smith trusts, from a default build it makes if needed), and the last line is smith check: ok or smith check: failed: <parts>; the exit status is 1 when any failed. The prove line says the coverage, smith check: prove ok (4 of 23 functions): ... (functions with a contract of their own, of the project's; 0 of N functions when there is nothing to prove, which is a success; nothing fails on low coverage). Each part's line ends with its wall and CPU time ((91.2 s wall, 480.1 s cpu): CPU is user plus system, of the part and the processes it waited for). It takes no arguments but -D options and -v. On a host that is not macOS it runs the tests with --skip-macos and says so (the macOS-only tests do not run on this host). smith test ends with a table of the build's tasks (.smith/task/<name> steps, as smith's own test-<name>), slowest first: wall, CPU and system seconds and the number of smith processes each started (SMITH_START_LOG, which smith appends to as main begins; a task that clears the environment is not counted); the slowest eight, every task with smith check -v or SMITH_ACCOUNT=all, none with SMITH_ACCOUNT=no. The tasks run through smith --smith-account <file> <name> <command...>, which writes the line.
  • .smith files in the new syntax are not clang-format's (#1890, #1886, #2040): smith fmt sends a .smith file to smith's own formatter (front::format) unless it is in today's syntax (a line starts with a form only today's syntax or C has, today's parser reads it without an error, or its first error in the new syntax has a fix, like a // comment) or has a parse error and no line only the new syntax has (fn, let, pub, #): one style and no .clang-format for it (any consistent indentation comes out at 4 spaces, one statement a line, # comments kept). A new-syntax file with a parse error is reported as path:line:column: message, left byte for byte as it was (with --dry-run too) and makes smith fmt exit 1: it is never sent to clang-format, which would mangle it. It is idempotent, honours .clang-format-ignore, and --dry-run / -n names the files that would change (path: would be formatted) without writing. A .smith file in today's syntax (and every .c) stays clang-format's until the switch-over (#1631) removes that fall-back. Tested by tests/fmt_smith_test.smith.
  • smith fmt FILE refuses a file whose extension is not one of those (a .sh, a .md), with an error that names the file and writes nothing, since clang-format would read it as C and corrupt it; --assume-filename=x.c says what it is.
  • Before committing: smith fmt, then smith check.
  • A task of your own (a custom check, a generator) is smith_task in build.c and runs with smith build <task>; no task or alias may be named fmt, lint, docs, prove, check, run or test. smith build check and the other verb names are refused: use the verb.

On files: with arguments

smith lint DIR (a folder among the arguments) lints the C, C++, Objective-C and .smith sources under it that git sees and .clang-format-ignore does not name, like the project lint (no headers, which are read through their sources); smith lint front -- <flags> and smith lint front/ main.c mix with files. A folder with no such source is an error.

clang-tidy's analyzer (clang-analyzer-*) reads smith_requires, smith_ensures, smith_invariant and smith_assert of <smith/contract.h> as assumptions (target::static_analyzer), so an index a contract bounds is not reported by clang-analyzer-security.ArrayBound.

smith's default .clang-tidy turns off two analyzer checks that were noise (on game, learner and smith: 546 of 616 findings): clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling (every memcpy) and clang-analyzer-optin.performance.Padding. Turn one back on in the project's own .clang-tidy (smith lint --dump-config > .clang-tidy writes the effective file) by deleting its -clang-analyzer-... line.

smith lint --fix FILE.smith... (no --) applies the fixes that smith's own front end carries in its parse errors (an old or C-style form with its smith spelling: the edits of one fix are applied together or not at all, an overlapping fix is skipped, and a file is parsed again until nothing applies, at most 16 passes) and prints path: N fixes applied; a second run prints 0 fixes applied. It reads .smith files only and needs the full smith. With -- <flags> it is clang-tidy's --fix, above. No parse error carries a fix yet: the old forms get theirs ticket by ticket (#1428).

smith lint FILE adds --extra-arg=-I for build/<os>/front-project/.smith/include when that folder exists (front modules import each other through it), and on a host that is not macOS prints one line: CI lints on a Mac, where u64 is unsigned long long and usize is unsigned long, so (U64)(i + 1) with a usize is a bugprone-misplaced-widening-cast finding there only; fix such a cast by writing (U64)i + 1.

With arguments they are the tools: smith fmt main.c rewrites the file in place (clang-format would print it; smith adds -i when you name files and no option chooses another mode: -n, --dry-run, --lines, --assume-filename keep clang-format's meaning, and smith fmt --assume-filename=x.c < x.c prints the result), smith fmt --dry-run --Werror main.c, smith fmt --dry-run --Werror main.c, smith lint --quiet main.c -- <flags>, smith lsp --check=main.c. A project's .clang-format and .clang-tidy apply. A stage0 smith runs Homebrew's tools, or $SMITH_CLANG_FORMAT, $SMITH_CLANG_TIDY, $SMITH_CLANGD.

smith's defaults, where a project has no file (issue #43). A project with no .clang-format (or _clang-format) and no .clang-tidy, looked up from each file's folder and up as clang-format and clang-tidy do, is formatted and linted with smith's own, embedded in smith (the files smith new writes today); there is nothing to write. A file in the project replaces smith's whole, never merged, and an explicit --style (or --fallback-style), --config or --config-file wins. With standard input, --assume-filename names where to look. Naming files with and without a project file in one command works (smith runs the second kind as another process). smith fmt --dump-config > .clang-format and smith lint --dump-config > .clang-tidy print the effective one (the project's, else smith's), so a project takes the defaults over by dumping them and edits what it wants; the defaults change rarely and on purpose, so a project that wants them fixed dumps them. smith writes its copy to $XDG_DATA_HOME/smith/defaults/ (else ~/.local/share/...). smith cc is never affected: it stays a pure passthrough.

.smith files

smith fmt x.smith (and -i, --dry-run, standard input with --assume-filename=x.smith) formats a .smith file as C, with its :: paths, module a::b; and import a::b as x; lines and import <sys/wait.h>; kept whole; smith fmt with no arguments formats the project's .smith files too (git sees them). import and module inside a function are names, as in C. Needs a smith whose clang-format has the .smith patch (a stage0 runs Homebrew's, which reads .smith as C++ and so misreads C names that are C++ keywords, such as throw). clangd formats .smith files the same way.

smith lint x.smith -- <flags> (clang-tidy, with the project's .clang-tidy) lints a .smith file as smith cc reads it (-x c, and -std=gnu2y unless you name a standard; a compile database's own command is used as it is) and reports as the source is written: findings, notes and fix-its at the lines and columns of the .smith text, with the names as written (area and Box in the module, box::area where it is imported, never geometry_box_area). readability-identifier-naming judges the written name of a module's file-scope names, so the template's rules (snake_case functions and variables, PascalCase types) apply to area and Box, and --fix rewrites the token in the source; the sized types (U32, I32), the lowering's copies of prototypes and the headers smith build writes are never reported. A finding about an export is reported in the module's file, not in each importer, and a fix there does not rename the importers. Other checks show the written names in their messages, but a check that compares the name itself (a length or a reserved-name check, say) still reads the lowered one. Needs a smith whose clang has the token hook (smith cc --smith-which x.smith says token-hook yes); a stage0 runs Homebrew's clang-tidy, which cannot read .smith.

Also in this family: smith-check-lints.

Related Skills