Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .config/flakebox/id
Original file line number Diff line number Diff line change
@@ -1 +1 @@
91208ded671a9539e4199fdb58b0bb8d17eba15563781ad1d034a57c282e176de0abd493a2ee8cf41fc0a88be0cd3590a0a6d4eeff4d1012fce55502a161d795
6e5c13b823dbc2228a5144d5e86d92a0e3a92e873c9f2a3adedd2e7669f8b1bf2863d328c89e6e3d3ef57ec095899c1671085bbd8d183c513b85eaa165a803c7
127 changes: 127 additions & 0 deletions checks/ast-grep-module.nix
Original file line number Diff line number Diff line change
@@ -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"
''
1 change: 1 addition & 0 deletions checks/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
34 changes: 34 additions & 0 deletions docs/best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,37 @@ 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:
- .config/ast-grep/rules
testConfigs:
- testDir: .config/ast-grep/rule-tests
```

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:

```nix
ast-grep = {
pre-commit.args = [
"--error=complex-conditional"
"--error=deeply-nested-if"
];
};
```
5 changes: 5 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
79 changes: 79 additions & 0 deletions lib/modules/ast-grep.nix
Original file line number Diff line number Diff line change
@@ -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}
'';
})
]
);
}
13 changes: 13 additions & 0 deletions misc/git-hooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -168,6 +180,7 @@ parallel \
--nonotice \
flakebox_run_check \
::: \
check_ast_grep \
check_cargo_lock \
check_semgrep \
check_shellcheck \
Expand Down
Loading