From 1be221501cabb191628a7aa700314922ab9b2fcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ci=C4=99=C5=BCarkiewicz?= Date: Fri, 24 Jul 2026 09:34:27 -0700 Subject: [PATCH 1/2] feat(lint): add ast-grep project integration ### Summary Add ast-grep as a default flakebox development tool and wire standard sgconfig.yml projects into just and pre-commit workflows. Repositories without a configuration remain unaffected because the hook exits quietly when its configured file is absent or empty. ### Details The new module exposes package, configFile, just.enable, pre-commit.enable, and pre-commit.args options. The default just recipe runs `ast-grep scan`, while the generated hook honors native rule severities and supports CLI severity overrides. Evaluation tests cover defaults, independent disabling, custom packages and paths, and shell-escaped arguments. A generated-hook fixture verifies missing and empty configs skip the tool and a non-empty config passes the exact configured path and arguments. User documentation shows ast-grep's standard rules and rule-tests directory layout, and generated flakebox files were refreshed. ### Reviews A reviewer found three documentation and coverage gaps: the moved-config example did not adjust paths or the test command; generated-hook skip and invocation behavior lacked behavioral coverage; and option isolation/custom package selection lacked complete evaluation coverage. I removed the inconsistent example and added the requested behavioral and evaluation tests. Re-review passed with only optional test-strengthening nits. ### Verification `selfci check --candidate otzsxlon` passed after the reviewed fixups. --- .config/flakebox/id | 2 +- checks/ast-grep-module.nix | 127 +++++++++++++++++++++++++++++++++++++ checks/default.nix | 1 + docs/best-practices.md | 31 +++++++++ justfile | 5 ++ lib/modules/ast-grep.nix | 79 +++++++++++++++++++++++ misc/git-hooks/pre-commit | 13 ++++ 7 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 checks/ast-grep-module.nix create mode 100644 lib/modules/ast-grep.nix diff --git a/.config/flakebox/id b/.config/flakebox/id index a1dc364..c3ac39a 100644 --- a/.config/flakebox/id +++ b/.config/flakebox/id @@ -1 +1 @@ -91208ded671a9539e4199fdb58b0bb8d17eba15563781ad1d034a57c282e176de0abd493a2ee8cf41fc0a88be0cd3590a0a6d4eeff4d1012fce55502a161d795 +6e5c13b823dbc2228a5144d5e86d92a0e3a92e873c9f2a3adedd2e7669f8b1bf2863d328c89e6e3d3ef57ec095899c1671085bbd8d183c513b85eaa165a803c7 diff --git a/checks/ast-grep-module.nix b/checks/ast-grep-module.nix new file mode 100644 index 0000000..e465719 --- /dev/null +++ b/checks/ast-grep-module.nix @@ -0,0 +1,127 @@ +{ + pkgs, + mkLib, +}: +let + inherit (pkgs) lib; + inherit (lib) assertMsg; + timer = pkgs.callPackage ../lib/pkgs/git-hook-check-timer.nix { }; + fakeAstGrep = pkgs.writeShellScriptBin "ast-grep" '' + printf '%s\n' "$*" >> "$AST_GREP_LOG" + ''; + + defaultLib = mkLib pkgs { }; + disabledLib = mkLib pkgs { + config.ast-grep.enable = false; + }; + justDisabledLib = mkLib pkgs { + config.ast-grep.just.enable = false; + }; + preCommitDisabledLib = mkLib pkgs { + config.ast-grep.pre-commit.enable = false; + }; + configuredLib = mkLib pkgs { + config = { + ast-grep = { + package = fakeAstGrep; + configFile = ".config/ast-grep/sgconfig.yml"; + pre-commit.args = [ + "--error=complex-conditional" + "--filter" + "rust-.*" + ]; + }; + cargo.pre-commit.cargo-lock.enable = false; + git.pre-commit = { + trailing_newline = false; + trailing_whitespace = false; + }; + semgrep.enable = false; + shellcheck.enable = false; + treefmt.enable = false; + typos.enable = false; + }; + }; +in +assert assertMsg defaultLib.config.ast-grep.enable "ast-grep is enabled by default"; +assert assertMsg defaultLib.config.ast-grep.just.enable + "ast-grep just recipe is enabled by default"; +assert assertMsg defaultLib.config.ast-grep.pre-commit.enable + "ast-grep pre-commit hook is enabled by default"; +assert assertMsg (builtins.hasAttr "ast-grep" defaultLib.config.just.rules) + "ast-grep just recipe is generated by default"; +assert assertMsg (builtins.hasAttr "ast_grep" defaultLib.config.git.pre-commit.hooks) + "ast-grep pre-commit hook is generated by default"; +assert assertMsg (lib.elem pkgs.ast-grep defaultLib.config.env.shellPackages) + "ast-grep package is added to development shells"; +assert assertMsg ( + !(builtins.hasAttr "ast-grep" disabledLib.config.just.rules) +) "top-level disable removes the ast-grep just recipe"; +assert assertMsg ( + !(builtins.hasAttr "ast_grep" disabledLib.config.git.pre-commit.hooks) +) "top-level disable removes the ast-grep pre-commit hook"; +assert assertMsg ( + !(lib.elem pkgs.ast-grep disabledLib.config.env.shellPackages) +) "top-level disable removes the ast-grep package"; +assert assertMsg ( + !(builtins.hasAttr "ast-grep" justDisabledLib.config.just.rules) +) "just disable removes only the ast-grep just recipe"; +assert assertMsg (builtins.hasAttr "ast_grep" justDisabledLib.config.git.pre-commit.hooks) + "just disable preserves the ast-grep pre-commit hook"; +assert assertMsg ( + !(builtins.hasAttr "ast_grep" preCommitDisabledLib.config.git.pre-commit.hooks) +) "pre-commit disable removes only the ast-grep hook"; +assert assertMsg (builtins.hasAttr "ast-grep" preCommitDisabledLib.config.just.rules) + "pre-commit disable preserves the ast-grep just recipe"; +assert assertMsg (lib.elem fakeAstGrep configuredLib.config.env.shellPackages) + "a custom ast-grep package replaces the default package"; +assert assertMsg ( + !(lib.elem pkgs.ast-grep configuredLib.config.env.shellPackages) +) "a custom ast-grep package removes the default package"; +assert assertMsg + (lib.hasInfix "--config .config/ast-grep/sgconfig.yml" configuredLib.config.just.rules.ast-grep.content) + "custom config path is rendered into the just recipe"; +assert assertMsg + (lib.hasInfix "'--error=complex-conditional' --filter 'rust-.*'" configuredLib.config.git.pre-commit.hooks.ast_grep) + "pre-commit arguments are shell escaped and rendered into the hook"; +pkgs.runCommand "ast-grep-module-tests" + { + nativeBuildInputs = [ + fakeAstGrep + pkgs.bash + pkgs.coreutils + pkgs.git + pkgs.gnugrep + pkgs.parallel + timer + ]; + } + '' + set -euo pipefail + + mkdir repo + cd repo + git init -q + touch tracked + git add tracked + + export AST_GREP_LOG="$PWD/ast-grep.log" + pre_commit="${configuredLib.root}/misc/git-hooks/pre-commit" + + NO_STASH=1 bash "$pre_commit" + [ ! -e "$AST_GREP_LOG" ] + + mkdir -p .config/ast-grep + touch .config/ast-grep/sgconfig.yml + NO_STASH=1 bash "$pre_commit" + [ ! -e "$AST_GREP_LOG" ] + + printf 'ruleDirs: []\n' > .config/ast-grep/sgconfig.yml + NO_STASH=1 bash "$pre_commit" + printf '%s\n' \ + "scan --config .config/ast-grep/sgconfig.yml --error=complex-conditional --filter rust-.*" \ + > expected.log + cmp expected.log "$AST_GREP_LOG" + + touch "$out" + '' diff --git a/checks/default.nix b/checks/default.nix index d91d6dd..2c26bf6 100644 --- a/checks/default.nix +++ b/checks/default.nix @@ -23,6 +23,7 @@ onlyDrvs ( mergeArgsTests = callPackage ./mergeArgs-tests.nix { }; craneMultiBuildTests = callPackage ./crane-multi-build-tests.nix { }; cargoCrapModule = callPackage ./cargo-crap-module.nix { inherit mkLib; }; + astGrepModule = callPackage ./ast-grep-module.nix { inherit mkLib; }; gitHooks = callPackage ./git-hooks.nix { inherit mkLib; }; } // lib.optionalAttrs (pkgs.stdenv.buildPlatform.system == "x86_64-linux") { diff --git a/docs/best-practices.md b/docs/best-practices.md index 66599cf..d25ba78 100644 --- a/docs/best-practices.md +++ b/docs/best-practices.md @@ -112,3 +112,34 @@ Customize the generated `.cargo-crap.toml` by setting `cargo-crap.config.content`. Disable only that generated file with `cargo-crap.config.enable = false`, or disable the whole cargo-crap integration with `cargo-crap.enable = false`. + + +## Enforce structural rules with ast-grep + +Flakebox installs [ast-grep](https://ast-grep.github.io/) and runs +`ast-grep scan` from the generated pre-commit hook when the repository contains +a non-empty `sgconfig.yml`. It also generates an `ast-grep` just recipe. + +Use ast-grep's standard project layout: + +```yaml +# sgconfig.yml +ruleDirs: + - rules +testConfigs: + - testDir: rule-tests +``` + +Store rule YAML under `rules/`, then run `just ast-grep` to scan the project or +`ast-grep test` to test the rules. + +Projects can promote selected rules to commit-blocking errors: + +```nix +ast-grep = { + pre-commit.args = [ + "--error=complex-conditional" + "--error=deeply-nested-if" + ]; +}; +``` diff --git a/justfile b/justfile index 62de0ad..61f6434 100644 --- a/justfile +++ b/justfile @@ -78,6 +78,11 @@ watch *ARGS="-x run": env RUST_LOG=${RUST_LOG:-debug} cargo watch {{ARGS}} +# scan the project with ast-grep rules +ast-grep *ARGS="": + ast-grep scan --config sgconfig.yml {{ARGS}} + + # run cargo-crap on the workspace crap *ARGS="--workspace": #!/usr/bin/env bash diff --git a/lib/modules/ast-grep.nix b/lib/modules/ast-grep.nix new file mode 100644 index 0000000..1671cc0 --- /dev/null +++ b/lib/modules/ast-grep.nix @@ -0,0 +1,79 @@ +{ + pkgs, + lib, + config, + ... +}: +let + inherit (lib) types; + configFileArg = lib.escapeShellArg config.ast-grep.configFile; + preCommitArgs = lib.escapeShellArgs config.ast-grep.pre-commit.args; +in +{ + options.ast-grep = { + enable = lib.mkEnableOption "ast-grep integration" // { + default = true; + }; + + package = lib.mkOption { + type = types.package; + default = pkgs.ast-grep; + defaultText = lib.literalExpression "pkgs.ast-grep"; + description = "ast-grep package to add to flakebox development shells."; + }; + + configFile = lib.mkOption { + type = types.str; + default = "sgconfig.yml"; + description = "Path to the ast-grep project configuration, relative to the repository root."; + }; + + just.enable = lib.mkEnableOption "ast-grep just recipe" // { + default = true; + }; + + pre-commit = { + enable = lib.mkEnableOption "ast-grep git pre-commit hook" // { + default = true; + }; + + args = lib.mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + Additional arguments passed to `ast-grep scan` by the pre-commit hook. + Use severity overrides such as `--error=RULE_ID` to make selected + findings block commits. + ''; + }; + }; + }; + + config = lib.mkIf config.ast-grep.enable ( + lib.mkMerge [ + { + env.shellPackages = [ config.ast-grep.package ]; + } + + (lib.mkIf config.ast-grep.just.enable { + just.rules.ast-grep = { + content = '' + # scan the project with ast-grep rules + ast-grep *ARGS="": + ast-grep scan --config ${configFileArg} {{ARGS}} + ''; + }; + }) + + (lib.mkIf config.ast-grep.pre-commit.enable { + git.pre-commit.hooks.ast_grep = '' + if [ ! -s ${configFileArg} ]; then + return 0 + fi + + ast-grep scan --config ${configFileArg} ${preCommitArgs} + ''; + }) + ] + ); +} diff --git a/misc/git-hooks/pre-commit b/misc/git-hooks/pre-commit index fc09456..dea34f3 100755 --- a/misc/git-hooks/pre-commit +++ b/misc/git-hooks/pre-commit @@ -56,6 +56,18 @@ else FLAKEBOX_GIT_LS_TEXT="$(echo "$FLAKEBOX_GIT_LS" | grep -v -E "\.(png|ods|jpg|jpeg|woff2|keystore|wasm|ttf|jar|ico|gif)\$" | grep -v -E "${FLAKEBOX_GIT_LS_TEXT_IGNORE}")" fi +# NOTE: THIS FILE IS AUTO-GENERATED BY FLAKEBOX +function check_ast_grep() { + set -euo pipefail + + if [ ! -s sgconfig.yml ]; then + return 0 + fi + + ast-grep scan --config sgconfig.yml +} +export -f check_ast_grep + # NOTE: THIS FILE IS AUTO-GENERATED BY FLAKEBOX function check_cargo_lock() { set -euo pipefail @@ -168,6 +180,7 @@ parallel \ --nonotice \ flakebox_run_check \ ::: \ + check_ast_grep \ check_cargo_lock \ check_semgrep \ check_shellcheck \ From 8fda0ada3ff5d9e12ba9a0d6fdb2edbfcd5ac504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ci=C4=99=C5=BCarkiewicz?= Date: Fri, 24 Jul 2026 10:01:03 -0700 Subject: [PATCH 2/2] docs: recommend discrete ast-grep rule directories ### Summary Keep ast-grep's discoverable `sgconfig.yml` at the repository root while documenting rules and tests under `.config/ast-grep/`. This avoids invasive top-level tool directories without losing bare `ast-grep scan` and `ast-grep test` commands. ### Details Update the recommended `ruleDirs` and `testConfigs` paths and explain why the split layout is preferable. ### Reviews Not required for this small documentation-only change. ### Verification `selfci check --candidate vqvoslkx` passed. --- docs/best-practices.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/best-practices.md b/docs/best-practices.md index d25ba78..1e7e128 100644 --- a/docs/best-practices.md +++ b/docs/best-practices.md @@ -125,13 +125,16 @@ Use ast-grep's standard project layout: ```yaml # sgconfig.yml ruleDirs: - - rules + - .config/ast-grep/rules testConfigs: - - testDir: rule-tests + - testDir: .config/ast-grep/rule-tests ``` -Store rule YAML under `rules/`, then run `just ast-grep` to scan the project or -`ast-grep test` to test the rules. +Keeping `sgconfig.yml` at the repository root preserves ast-grep's automatic +project discovery. Store the rules and their tests under +`.config/ast-grep/` to avoid adding tool-specific directories at the root. +Then run `just ast-grep` to scan the project or `ast-grep test` to test the +rules. Projects can promote selected rules to commit-blocking errors: