From c1c27e941b85b39185f91d96a9bd578c098c8ae3 Mon Sep 17 00:00:00 2001 From: olavostauros Date: Wed, 24 Jun 2026 10:11:48 -0300 Subject: [PATCH 1/5] feat(lib): add shared usage-parser.sh for #USAGE directive parsing - Extracts flag names, placeholders, and usage_* env var names - Functions: usage_flag_name, usage_placeholder, usage_env_name, parse_usage_flags, discover_task_files - Reusable by sibling rules (#35, #39, #43) --- lib/usage-parser.sh | 117 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100755 lib/usage-parser.sh diff --git a/lib/usage-parser.sh b/lib/usage-parser.sh new file mode 100755 index 0000000..be36d63 --- /dev/null +++ b/lib/usage-parser.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# Shared helpers for parsing #USAGE directives in .mise/tasks files. +# +# Source from .mise/tasks/lint/* (task context): +# # shellcheck source=../../lib/usage-parser.sh +# source "$MISE_CONFIG_ROOT/lib/usage-parser.sh" +# +# MISE_CONFIG_ROOT is the right primitive here — mise dispatched the task, so +# it's set correctly. Do NOT source this file from test helpers or other lib +# files using $MISE_CONFIG_ROOT; see fold/notes/mise-gotchas.md. + +# usage_flag_name +# +# Given a #USAGE flag directive like '#USAGE flag "-e --exclude "', +# extract the long flag name with -- prefix. Returns empty if no long flag. +# +# Examples: +# '#USAGE flag "--verbose"' → "--verbose" +# '#USAGE flag "--model "' → "--model" +# '#USAGE flag "-e --exclude "' → "--exclude" +# '#USAGE flag "-e"' → "" +usage_flag_name() { + local directive="$1" + # Match a '--' word. The long flag appears before any placeholder. + if [[ "$directive" =~ --([a-zA-Z0-9_-]+) ]]; then + printf '%s\n' "--${BASH_REMATCH[1]}" + fi +} + +# usage_placeholder +# +# Given a #USAGE flag directive, extract the arg placeholder <...> text +# (without angle brackets). Returns empty if no placeholder. +# +# Examples: +# '#USAGE flag "--model "' → "model" +# '#USAGE flag "--exclude "' → "x" +# '#USAGE flag "--verbose"' → "" +usage_placeholder() { + local directive="$1" + if [[ "$directive" =~ '<'([a-zA-Z0-9_-]+)'>' ]]; then + printf '%s\n' "${BASH_REMATCH[1]}" + fi +} + +# usage_env_name +# +# Convert a flag name (with --) or placeholder name to the mise usage_* env var +# name: strip leading --, replace hyphens with underscores. +# +# Examples: +# "--exclude" → "usage_exclude" +# "excludes" → "usage_excludes" +# "file-path" → "usage_file_path" +usage_env_name() { + local name="$1" + # Strip leading -- if present + name="${name#--}" + # Replace hyphens with underscores (mise normalises hyphens to underscores) + name="${name//-/_}" + printf 'usage_%s\n' "$name" +} + +# parse_usage_flags +# +# Read a task file and emit tab-separated lines of the form: +# \t\t +# for each #USAGE flag directive found. +# +# - Empty placeholder means boolean flag (no arg). +# - Empty flag_name means short-only flag (e.g. -v, no --long form). +# - Lines with codebase:ignore are skipped (respect inline opt-out). +# +# Output is tab-separated for reliable parsing with IFS=$'\t' read -r. +parse_usage_flags() { + local file="$1" + local lineno=0 + local line + + while IFS= read -r line || [[ -n "$line" ]]; do + lineno=$((lineno + 1)) + + # Non-directive lines are skipped + [[ "$line" != *"#USAGE flag"* ]] && continue + + # Inline opt-out + [[ "$line" == *"codebase:ignore"* ]] && continue + + local flag_name + local placeholder + + flag_name=$(usage_flag_name "$line") + placeholder=$(usage_placeholder "$line") + + # Skip short-only flags (no long name) — nothing to compare + [[ -z "$flag_name" ]] && continue + + printf '%s\t%s\t%s\n' "$lineno" "$flag_name" "$placeholder" + done < "$file" +} + +# discover_task_files +# +# Emit paths of task files under /.mise/tasks/, one per line. +# Excludes subdirectories named fixtures/ (lint-rule fixtures intentionally +# contain synthetic patterns that should not trigger self-flagging during +# self-hosting scans). +discover_task_files() { + local target="$1" + local tasks_dir="$target/.mise/tasks" + + if [[ ! -d "$tasks_dir" ]]; then + return + fi + + fd --type f --exclude fixtures . "$tasks_dir" 2>/dev/null || true +} \ No newline at end of file From 70bdab56f56c1e98d37d79124d7aa0f0b3435a6b Mon Sep 17 00:00:00 2001 From: olavostauros Date: Wed, 24 Jun 2026 10:11:54 -0300 Subject: [PATCH 2/5] =?UTF-8?q?feat(lint):=20add=20usage-flag-naming=20rul?= =?UTF-8?q?e=20=E2=80=94=20detect=20USAGE=20flag/placeholder=20mismatches?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scans .mise/tasks/** for #USAGE flag directives where the flag name and arg placeholder produce different usage_* env var names. When they differ, the flag silently does nothing (real bug from codebase#12). - Two-phase detection: parse #USAGE directives, compare env var names - Skips boolean flags (no placeholder), short-only (no long name) - Respects inline and file-level codebase:ignore - 16 BATS tests, 10 fixture directories - Reuses lib/usage-parser.sh for directive parsing --- .mise/tasks/lint/usage-flag-naming | 92 ++++++++++ .../fixtures/clean-boolean/.mise/tasks/bar | 3 + .../fixtures/clean-match/.mise/tasks/foo | 3 + .../fixtures/clean-short-only/.mise/tasks/baz | 3 + .../fixtures/dirty-mismatch/.mise/tasks/scan | 4 + .../dirty-multi-word/.mise/tasks/fetch | 4 + .../fixtures/ignored-file/.mise/tasks/task | 4 + .../fixtures/ignored-file/mise.toml | 2 + .../fixtures/ignored-inline/.mise/tasks/task | 4 + .../fixtures/mixed/.mise/tasks/bad | 4 + .../fixtures/mixed/.mise/tasks/good | 4 + .../fixtures/multi-flag/.mise/tasks/task | 6 + .../usage-flag-naming/usage-flag-naming.bats | 171 ++++++++++++++++++ 13 files changed, 304 insertions(+) create mode 100755 .mise/tasks/lint/usage-flag-naming create mode 100644 test/lint/usage-flag-naming/fixtures/clean-boolean/.mise/tasks/bar create mode 100644 test/lint/usage-flag-naming/fixtures/clean-match/.mise/tasks/foo create mode 100644 test/lint/usage-flag-naming/fixtures/clean-short-only/.mise/tasks/baz create mode 100644 test/lint/usage-flag-naming/fixtures/dirty-mismatch/.mise/tasks/scan create mode 100644 test/lint/usage-flag-naming/fixtures/dirty-multi-word/.mise/tasks/fetch create mode 100644 test/lint/usage-flag-naming/fixtures/ignored-file/.mise/tasks/task create mode 100644 test/lint/usage-flag-naming/fixtures/ignored-file/mise.toml create mode 100644 test/lint/usage-flag-naming/fixtures/ignored-inline/.mise/tasks/task create mode 100644 test/lint/usage-flag-naming/fixtures/mixed/.mise/tasks/bad create mode 100644 test/lint/usage-flag-naming/fixtures/mixed/.mise/tasks/good create mode 100644 test/lint/usage-flag-naming/fixtures/multi-flag/.mise/tasks/task create mode 100644 test/lint/usage-flag-naming/usage-flag-naming.bats diff --git a/.mise/tasks/lint/usage-flag-naming b/.mise/tasks/lint/usage-flag-naming new file mode 100755 index 0000000..4fd4cf2 --- /dev/null +++ b/.mise/tasks/lint/usage-flag-naming @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +#MISE description="Flag #USAGE flag directives where flag name and placeholder produce different usage_* env var names" +#USAGE arg "…" help="Paths to codebases to check (one or more)" +#USAGE example "codebase lint:usage-flag-naming ." header="Flag mismatched USAGE flag/placeholder" +#USAGE example "codebase lint:usage-flag-naming /path/to/repo" header="Check a specific repo" + +set -euo pipefail + +# shellcheck source=../../../lib/shell-files.sh +source "$MISE_CONFIG_ROOT/lib/shell-files.sh" +# shellcheck source=../../../lib/usage-parser.sh +source "$MISE_CONFIG_ROOT/lib/usage-parser.sh" + +IFS=' ' read -ra TARGETS <<< "${usage_targets}" + +if [[ ${#TARGETS[@]} -eq 0 ]]; then + echo "ERROR: at least one target is required" >&2 + exit 1 +fi + +for i in "${!TARGETS[@]}"; do + TARGETS[$i]=$(resolve_target "${TARGETS[$i]}") +done + +failures=0 + +for target in "${TARGETS[@]}"; do + if [[ ! -e "$target" ]]; then + echo "ERROR: target does not exist: $target" >&2 + exit 1 + fi + + name=$(basename "$target") + + # File-level ignore via mise.toml + toml="$target/mise.toml" + if [[ -f "$toml" ]] && grep -q 'codebase:ignore usage-flag-naming' "$toml" 2>/dev/null; then + echo "SKIP $name (codebase:ignore)" + continue + fi + + tasks_dir="$target/.mise/tasks" + if [[ ! -d "$tasks_dir" ]]; then + echo "OK $name (no .mise/tasks directory)" + continue + fi + + # Discover task files + task_files=() + while IFS= read -r f; do + [[ -n "$f" ]] && task_files+=("$f") + done < <(discover_task_files "$target") + + if [[ ${#task_files[@]} -eq 0 ]]; then + echo "OK $name (no task files found)" + continue + fi + + target_failures=0 + target_output="" + + for task_file in "${task_files[@]}"; do + rel="${task_file#"$target/"}" + + while IFS=$'\t' read -r lineno flag_name placeholder; do + [[ -z "$lineno" ]] && continue + + # Boolean flag (no placeholder) — skip + [[ -z "$placeholder" ]] && continue + + flag_env=$(usage_env_name "$flag_name") + placeholder_env=$(usage_env_name "$placeholder") + + if [[ "$flag_env" != "$placeholder_env" ]]; then + target_failures=$((target_failures + 1)) + target_output+=" $rel:$lineno: --${flag_name#--} ($flag_env) vs <$placeholder> ($placeholder_env)"$'\n' + fi + done < <(parse_usage_flags "$task_file") + done + + if [[ "$target_failures" -gt 0 ]]; then + echo "FAIL $name: $target_failures flag/placeholder mismatch(es)" + printf '%s' "$target_output" + echo " hint: use '--flag-name ' so mise's usage_* env var matches what the task reads" + echo " or annotate with '# codebase:ignore=usage-flag-naming -- reason'" + failures=$((failures + 1)) + else + echo "OK $name (${#task_files[@]} file(s) clean)" + fi +done + +exit "$failures" \ No newline at end of file diff --git a/test/lint/usage-flag-naming/fixtures/clean-boolean/.mise/tasks/bar b/test/lint/usage-flag-naming/fixtures/clean-boolean/.mise/tasks/bar new file mode 100644 index 0000000..1861eb6 --- /dev/null +++ b/test/lint/usage-flag-naming/fixtures/clean-boolean/.mise/tasks/bar @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +#USAGE flag "--verbose" help="Enable verbose output" +echo "boolean only" \ No newline at end of file diff --git a/test/lint/usage-flag-naming/fixtures/clean-match/.mise/tasks/foo b/test/lint/usage-flag-naming/fixtures/clean-match/.mise/tasks/foo new file mode 100644 index 0000000..593966a --- /dev/null +++ b/test/lint/usage-flag-naming/fixtures/clean-match/.mise/tasks/foo @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +#USAGE flag "--exclude " help="Patterns to exclude" +echo "clean task" \ No newline at end of file diff --git a/test/lint/usage-flag-naming/fixtures/clean-short-only/.mise/tasks/baz b/test/lint/usage-flag-naming/fixtures/clean-short-only/.mise/tasks/baz new file mode 100644 index 0000000..c6e41f8 --- /dev/null +++ b/test/lint/usage-flag-naming/fixtures/clean-short-only/.mise/tasks/baz @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +#USAGE flag "-v" help="Verbose mode" +echo "short only" \ No newline at end of file diff --git a/test/lint/usage-flag-naming/fixtures/dirty-mismatch/.mise/tasks/scan b/test/lint/usage-flag-naming/fixtures/dirty-mismatch/.mise/tasks/scan new file mode 100644 index 0000000..2f3de1d --- /dev/null +++ b/test/lint/usage-flag-naming/fixtures/dirty-mismatch/.mise/tasks/scan @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +#USAGE flag "-e --exclude " help="Patterns to exclude" +usage_excludes="*.tmp" +echo "mismatched flag/placeholder" \ No newline at end of file diff --git a/test/lint/usage-flag-naming/fixtures/dirty-multi-word/.mise/tasks/fetch b/test/lint/usage-flag-naming/fixtures/dirty-multi-word/.mise/tasks/fetch new file mode 100644 index 0000000..404bb2e --- /dev/null +++ b/test/lint/usage-flag-naming/fixtures/dirty-multi-word/.mise/tasks/fetch @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +#USAGE flag "--search-path " help="Directories to search" +usage_search_paths="./src" +echo "multi-word mismatch" \ No newline at end of file diff --git a/test/lint/usage-flag-naming/fixtures/ignored-file/.mise/tasks/task b/test/lint/usage-flag-naming/fixtures/ignored-file/.mise/tasks/task new file mode 100644 index 0000000..29efca2 --- /dev/null +++ b/test/lint/usage-flag-naming/fixtures/ignored-file/.mise/tasks/task @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +#USAGE flag "-e --exclude " help="Patterns to exclude" +usage_excludes="*.tmp" +echo "file-level ignored task" \ No newline at end of file diff --git a/test/lint/usage-flag-naming/fixtures/ignored-file/mise.toml b/test/lint/usage-flag-naming/fixtures/ignored-file/mise.toml new file mode 100644 index 0000000..cdae74f --- /dev/null +++ b/test/lint/usage-flag-naming/fixtures/ignored-file/mise.toml @@ -0,0 +1,2 @@ +[_.codebase] +codebase:ignore usage-flag-naming \ No newline at end of file diff --git a/test/lint/usage-flag-naming/fixtures/ignored-inline/.mise/tasks/task b/test/lint/usage-flag-naming/fixtures/ignored-inline/.mise/tasks/task new file mode 100644 index 0000000..b5fb558 --- /dev/null +++ b/test/lint/usage-flag-naming/fixtures/ignored-inline/.mise/tasks/task @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +#USAGE flag "-e --exclude " help="Patterns to exclude" # codebase:ignore=usage-flag-naming -- legacy name mismatch +usage_excludes="*.tmp" +echo "inline ignore" \ No newline at end of file diff --git a/test/lint/usage-flag-naming/fixtures/mixed/.mise/tasks/bad b/test/lint/usage-flag-naming/fixtures/mixed/.mise/tasks/bad new file mode 100644 index 0000000..9ab1609 --- /dev/null +++ b/test/lint/usage-flag-naming/fixtures/mixed/.mise/tasks/bad @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +#USAGE flag "-e --exclude " help="Patterns to exclude" +usage_excludes="*.tmp" +echo "bad task" \ No newline at end of file diff --git a/test/lint/usage-flag-naming/fixtures/mixed/.mise/tasks/good b/test/lint/usage-flag-naming/fixtures/mixed/.mise/tasks/good new file mode 100644 index 0000000..0853e69 --- /dev/null +++ b/test/lint/usage-flag-naming/fixtures/mixed/.mise/tasks/good @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +#USAGE flag "--model " help="Model name" +usage_model="gpt-4" +echo "good task" \ No newline at end of file diff --git a/test/lint/usage-flag-naming/fixtures/multi-flag/.mise/tasks/task b/test/lint/usage-flag-naming/fixtures/multi-flag/.mise/tasks/task new file mode 100644 index 0000000..3b96f2d --- /dev/null +++ b/test/lint/usage-flag-naming/fixtures/multi-flag/.mise/tasks/task @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +#USAGE flag "--verbose" +#USAGE flag "--name " help="Name to use" +usage_verbose=true +usage_name="default" +echo "multi-flag task" \ No newline at end of file diff --git a/test/lint/usage-flag-naming/usage-flag-naming.bats b/test/lint/usage-flag-naming/usage-flag-naming.bats new file mode 100644 index 0000000..8acf0ec --- /dev/null +++ b/test/lint/usage-flag-naming/usage-flag-naming.bats @@ -0,0 +1,171 @@ +#!/usr/bin/env bats +# Tests for lint:usage-flag-naming rule + +load ../../test_helper + +setup() { + FIXTURES="$BATS_TEST_DIRNAME/fixtures" +} + +# ============================================================================ +# Clean cases — should pass +# ============================================================================ + +@test "usage-flag-naming: passes on clean task (flag/placeholder match)" { + run codebase lint:usage-flag-naming "$FIXTURES/clean-match" + [ "$status" -eq 0 ] + [[ "$output" == *"OK"* ]] +} + +@test "usage-flag-naming: passes on boolean flag (no placeholder)" { + run codebase lint:usage-flag-naming "$FIXTURES/clean-boolean" + [ "$status" -eq 0 ] + [[ "$output" == *"OK"* ]] +} + +@test "usage-flag-naming: passes on short-only flag (no long name)" { + run codebase lint:usage-flag-naming "$FIXTURES/clean-short-only" + [ "$status" -eq 0 ] + [[ "$output" == *"OK"* ]] +} + +@test "usage-flag-naming: passes on multi-flag task when all match" { + run codebase lint:usage-flag-naming "$FIXTURES/multi-flag" + [ "$status" -eq 0 ] + [[ "$output" == *"OK"* ]] +} + +@test "usage-flag-naming: passes on target with no .mise/tasks directory" { + run codebase lint:usage-flag-naming "$FIXTURES/no-tasks" + [ "$status" -eq 0 ] + [[ "$output" == *"OK"* ]] +} + +# ============================================================================ +# Dirty cases — should fail +# ============================================================================ + +@test "usage-flag-naming: flags mismatched flag/placeholder" { + run codebase lint:usage-flag-naming "$FIXTURES/dirty-mismatch" + [ "$status" -ne 0 ] + [[ "$output" == *"FAIL"*"dirty-mismatch"* ]] + # --exclude (usage_exclude) vs (usage_excludes) + [[ "$output" == *"--exclude"* ]] + [[ "$output" == *"usage_exclude"* ]] + [[ "$output" == *"excludes"* ]] + [[ "$output" == *"usage_excludes"* ]] +} + +@test "usage-flag-naming: flags multi-word placeholder mismatch" { + run codebase lint:usage-flag-naming "$FIXTURES/dirty-multi-word" + [ "$status" -ne 0 ] + [[ "$output" == *"FAIL"*"dirty-multi-word"* ]] + # --search-path (usage_search_path) vs (usage_search_paths) + [[ "$output" == *"search-path"* ]] + [[ "$output" == *"usage_search_path"* ]] + [[ "$output" == *"search-paths"* ]] + [[ "$output" == *"usage_search_paths"* ]] +} + +# ============================================================================ +# Mixed cases +# ============================================================================ + +@test "usage-flag-naming: flags only dirty tasks in mixed target" { + run codebase lint:usage-flag-naming "$FIXTURES/mixed" + [ "$status" -ne 0 ] + [[ "$output" == *"FAIL"*"mixed"* ]] + # Only the bad task should be flagged, good task should not appear in findings + [[ "$output" == *".mise/tasks/bad"* ]] + [[ "$output" != *".mise/tasks/good"* ]] +} + +# ============================================================================ +# Ignore mechanisms +# ============================================================================ + +@test "usage-flag-naming: respects inline codebase:ignore" { + run codebase lint:usage-flag-naming "$FIXTURES/ignored-inline" + [ "$status" -eq 0 ] + [[ "$output" == *"OK"* ]] +} + +@test "usage-flag-naming: respects file-level codebase:ignore in mise.toml" { + run codebase lint:usage-flag-naming "$FIXTURES/ignored-file" + [ "$status" -eq 0 ] + [[ "$output" == *"SKIP"* ]] +} + +# ============================================================================ +# Output format +# ============================================================================ + +@test "usage-flag-naming: output includes task file path and line number" { + run codebase lint:usage-flag-naming "$FIXTURES/dirty-mismatch" + [ "$status" -ne 0 ] + [[ "$output" == *".mise/tasks/scan:"* ]] + [[ "$output" == *"2:"* ]] # line 2 has the #USAGE flag directive (line 1 is shebang) +} + +@test "usage-flag-naming: output includes hint for fix" { + run codebase lint:usage-flag-naming "$FIXTURES/dirty-mismatch" + [ "$status" -ne 0 ] + [[ "$output" == *"hint"* ]] + [[ "$output" == *"mise"* ]] +} + +# ============================================================================ +# Edge cases +# ============================================================================ + +@test "usage-flag-naming: handles empty target directory" { + run codebase lint:usage-flag-naming "$FIXTURES/no-tasks" + [ "$status" -eq 0 ] + [[ "$output" == *"OK"* ]] +} + +@test "usage-flag-naming: does not self-flag on own fixtures tree" { + # When scanning the codebase repo with this rule, the rule's own fixture + # directories contain intentional mismatches under .mise/tasks/. The fd + # --exclude fixtures flag skips subdirectories named fixtures/ within the + # tasks tree, so self-hosting should pass clean. This test verifies that + # behavior by scanning the entire fixtures tree. Note: this passes because + # fd --exclude fixtures skips the actual dirty fixture content; if it ever + # fails, the exclusion mechanism broke. + run codebase lint:usage-flag-naming "$FIXTURES" + [ "$status" -eq 0 ] +} + +@test "usage-flag-naming: does NOT flag matching hyphens (file-path → file_path)" { + # --file-path should match: both normalize to usage_file_path. + local tmpdir + tmpdir=$(mktemp -d) + mkdir -p "$tmpdir/.mise/tasks" + cat > "$tmpdir/.mise/tasks/hyphen-match" << 'TASK' +#!/usr/bin/env bash +#USAGE flag "--file-path " help="File path" +echo "hyphen match" +TASK + + run codebase lint:usage-flag-naming "$tmpdir" + [ "$status" -eq 0 ] + [[ "$output" == *"OK"* ]] + rm -rf "$tmpdir" +} + +@test "usage-flag-naming: does NOT flag matching plural (exclude → exclude)" { + # --exclude should match: both normalize to usage_exclude. + local tmpdir + tmpdir=$(mktemp -d) + mkdir -p "$tmpdir/.mise/tasks" + cat > "$tmpdir/.mise/tasks/plural-match" << 'TASK' +#!/usr/bin/env bash +#USAGE flag "--exclude " help="Patterns to exclude" +echo "plural match" +TASK + + run codebase lint:usage-flag-naming "$tmpdir" + [ "$status" -eq 0 ] + [[ "$output" == *"OK"* ]] + rm -rf "$tmpdir" +} \ No newline at end of file From 4f9dfd9df41329125e9466769b01fe4ae77aeca3 Mon Sep 17 00:00:00 2001 From: olavostauros Date: Wed, 24 Jun 2026 10:11:59 -0300 Subject: [PATCH 3/5] fix: align scan USAGE placeholder with flag name --exclude The USAGE flag directive declared '--exclude ' but mise exports usage_exclude (from the flag name), not usage_excludes. The task code correctly reads , but the placeholder was misleading. Caught by the new usage-flag-naming self-hosting check. --- .mise/tasks/scan | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mise/tasks/scan b/.mise/tasks/scan index 2c512e7..a5d8fc5 100755 --- a/.mise/tasks/scan +++ b/.mise/tasks/scan @@ -3,7 +3,7 @@ #USAGE arg "…" help="Paths to codebases to scan (one or more)" #USAGE flag "-p --pattern " required=#true help="ast-grep pattern to search for" #USAGE flag "-l --lang " help="Language (default: bash)" -#USAGE flag "-e --exclude " help="Glob patterns to exclude (space-separated)" +#USAGE flag "-e --exclude " help="Glob patterns to exclude (space-separated)" set -euo pipefail From 868958bba339eb2b67e323600427c2a2eb4e7d55 Mon Sep 17 00:00:00 2001 From: olavostauros Date: Wed, 24 Jun 2026 10:12:03 -0300 Subject: [PATCH 4/5] docs: add usage-flag-naming to AGENTS.md lint rules table --- AGENTS.md | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a7635b8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,59 @@ +# AGENTS.md — codebase + +**Structural code analysis, linting & migrations.** A toolbox of convention lints, AST-based scanning, and codemod migrations for KnickKnackLabs repos. Designed to be used as a shiv dependency by other repos. + +## Commands + +| Command | Description | +|---------|-------------| +| `lint [target]` | Run all configured convention lints (from repo's `[_.codebase]` mise config) | +| `lint: ` | Run a specific lint check by name | +| `scan -p -l ` | AST pattern matching via ast-grep | +| `migrate task-pattern ` | Migrate mise run calls to `_task` pattern (or reverse) | +| `pre-commit [--check \| --revert]` | Install, check, or remove the codebase pre-commit hook | +| `test [args...]` | Run the BATS test suite | + +### Available lint checks + +| Lint | What it checks | +|------|---------------| +| `mise-settings` | `mise.toml` has required settings (`experimental`, `quiet`, `task_output`) | +| `gum-table` | Detects manual table formatting that should use `gum table` | +| `bats-test-helper` | Flags direct script invocation from BATS tests (should call the tool) | +| `bats-test-task` | Enforces canonical BATS test-task shape in `.mise/tasks/test` | +| `mcr-scope` | Forbids `MISE_CONFIG_ROOT` in `test/` and `lib/` | +| `or-true` | Classifies risky unannotated `\|\| true` / `\|\| :` failure suppression | +| `shellcheck` | Runs shellcheck against shell files | +| `caller-pwd-contract` | Checks shiv caller-cwd environment variable contract | +| `github-actions` | Lints workflows and creates a default KKL workflow when missing | +| `bash-empty-array-expansions` | Flags empty array expansions under nounset (macOS Bash 3.2 compat) | +| `usage-flag-naming` | Detects #USAGE flag directives where flag name and arg placeholder produce different `usage_*` env var names (flag silently does nothing) | + +## Install + +```bash +shiv install codebase +``` + +Projects add it to `mise.toml`: + +```toml +[tools] +"shiv:codebase" = "latest" + +[_.codebase] +lint = [ + "mise-settings", + "bats-test-task", + "or-true", + "shellcheck", + "caller-pwd-contract", + "github-actions", +] +``` + +## Key patterns + +- Lint checks are per-repo opt-in via `[_.codebase]` in `mise.toml` +- Uses `ast-grep` for structural pattern matching (not regex) +- Sub-commands: `lint/` directory contains individual lint scripts, `migrate/` contains codemod scripts \ No newline at end of file From ce330024c4663b0ddf414144735bec474f29dca7 Mon Sep 17 00:00:00 2001 From: olavostauros Date: Wed, 24 Jun 2026 10:31:23 -0300 Subject: [PATCH 5/5] style: collapse 3-line section comment blocks to single line --- .mise/tasks/lint/usage-flag-naming | 2 +- AGENTS.md | 20 ++++++++++++++++++- lib/usage-parser.sh | 2 +- test/lib/shell-files.bats | 2 -- .../bats-test-helper/bats-test-helper.bats | 14 ------------- test/lint/bats-test-task/bats-test-task.bats | 12 ----------- test/lint/gum-table/gum-table.bats | 16 --------------- test/lint/mcr-scope/mcr-scope.bats | 10 ---------- test/lint/mise-settings/mise-settings.bats | 6 ------ test/lint/or-true/or-true.bats | 18 ----------------- test/lint/shellcheck/shellcheck.bats | 10 ---------- .../fixtures/clean-boolean/.mise/tasks/bar | 2 +- .../fixtures/clean-match/.mise/tasks/foo | 2 +- .../fixtures/clean-short-only/.mise/tasks/baz | 2 +- .../fixtures/dirty-mismatch/.mise/tasks/scan | 2 +- .../dirty-multi-word/.mise/tasks/fetch | 2 +- .../fixtures/ignored-file/.mise/tasks/task | 2 +- .../fixtures/ignored-file/mise.toml | 2 +- .../fixtures/ignored-inline/.mise/tasks/task | 2 +- .../fixtures/mixed/.mise/tasks/bad | 2 +- .../fixtures/mixed/.mise/tasks/good | 2 +- .../fixtures/multi-flag/.mise/tasks/task | 2 +- .../usage-flag-naming/usage-flag-naming.bats | 14 +------------ .../migrations/task-pattern/task-pattern.bats | 10 ---------- test/pre-commit/pre-commit.bats | 16 --------------- test/scan/scan.bats | 10 ---------- 26 files changed, 33 insertions(+), 151 deletions(-) mode change 100755 => 100644 .mise/tasks/lint/usage-flag-naming mode change 100755 => 100644 lib/usage-parser.sh diff --git a/.mise/tasks/lint/usage-flag-naming b/.mise/tasks/lint/usage-flag-naming old mode 100755 new mode 100644 index 4fd4cf2..d8701ea --- a/.mise/tasks/lint/usage-flag-naming +++ b/.mise/tasks/lint/usage-flag-naming @@ -89,4 +89,4 @@ for target in "${TARGETS[@]}"; do fi done -exit "$failures" \ No newline at end of file +exit "$failures" diff --git a/AGENTS.md b/AGENTS.md index a7635b8..4bd09b3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,8 +52,26 @@ lint = [ ] ``` +## Conventions + +### Commentary style + +Use single-line `# Section title` comments for test section headers. Do not use three-line `# ====== / # Title / # ======` dividers — they waste vertical space without adding information. + +```bash +# Good — single line +# Detection + +@test "..." { + +# Bad — three-line divider +# ==================================================================== +# Detection +# ==================================================================== +``` + ## Key patterns - Lint checks are per-repo opt-in via `[_.codebase]` in `mise.toml` - Uses `ast-grep` for structural pattern matching (not regex) -- Sub-commands: `lint/` directory contains individual lint scripts, `migrate/` contains codemod scripts \ No newline at end of file +- Sub-commands: `lint/` directory contains individual lint scripts, `migrate/` contains codemod scripts diff --git a/lib/usage-parser.sh b/lib/usage-parser.sh old mode 100755 new mode 100644 index be36d63..5fa2914 --- a/lib/usage-parser.sh +++ b/lib/usage-parser.sh @@ -114,4 +114,4 @@ discover_task_files() { fi fd --type f --exclude fixtures . "$tasks_dir" 2>/dev/null || true -} \ No newline at end of file +} diff --git a/test/lib/shell-files.bats b/test/lib/shell-files.bats index a462482..5d6ed6e 100644 --- a/test/lib/shell-files.bats +++ b/test/lib/shell-files.bats @@ -7,9 +7,7 @@ setup() { source "$REPO_DIR/lib/shell-files.sh" } -# ============================================================================ # resolve_target -# ============================================================================ @test "resolve_target: absolute path passes through unchanged" { result=$(resolve_target "/some/absolute/path") diff --git a/test/lint/bats-test-helper/bats-test-helper.bats b/test/lint/bats-test-helper/bats-test-helper.bats index 27ebba4..f4b0e76 100644 --- a/test/lint/bats-test-helper/bats-test-helper.bats +++ b/test/lint/bats-test-helper/bats-test-helper.bats @@ -7,9 +7,7 @@ setup() { FIXTURES="$BATS_TEST_DIRNAME/fixtures" } -# ============================================================================ # Pass paths -# ============================================================================ @test "bats-test-helper: passes on clean wrapper-based tests" { run codebase lint:bats-test-helper "$FIXTURES/clean" @@ -24,9 +22,7 @@ setup() { [[ "$output" == *"no test/ files found"* ]] } -# ============================================================================ # Invocation signatures — each form fails -# ============================================================================ @test "bats-test-helper: flags 'bash \$TASK' (var whose name contains TASK)" { run codebase lint:bats-test-helper "$FIXTURES/dirty-task-var" @@ -84,9 +80,7 @@ setup() { [[ "$output" == *"bash \$REPO_DIR/.mise/tasks/lint/check"* ]] } -# ============================================================================ # False positives — none of these are actual invocations -# ============================================================================ @test "bats-test-helper: does NOT flag reading a task file as data (grep/cat)" { # Regression guard: 'grep "$MCR/.mise/tasks/foo"' reads the script, doesn't @@ -97,9 +91,7 @@ setup() { [[ "$output" == *"OK"* ]] } -# ============================================================================ # Ignore directives -# ============================================================================ @test "bats-test-helper: inline '# codebase:ignore' suppresses a single line" { run codebase lint:bats-test-helper "$FIXTURES/ignored-inline" @@ -113,9 +105,7 @@ setup() { [[ "$output" == *"SKIP"*"ignored-file"* ]] } -# ============================================================================ # Output details -# ============================================================================ @test "bats-test-helper: fail output includes file:line citations" { run codebase lint:bats-test-helper "$FIXTURES/dirty-task-var" @@ -130,9 +120,7 @@ setup() { [[ "$output" == *"Call the Tool"* ]] } -# ============================================================================ # Error handling -# ============================================================================ @test "bats-test-helper: fails when no targets given" { run codebase lint:bats-test-helper @@ -146,9 +134,7 @@ setup() { [[ "$output" == *"does not exist"* ]] } -# ============================================================================ # Multi-target -# ============================================================================ @test "bats-test-helper: accepts multiple targets and reports each" { run codebase lint:bats-test-helper "$FIXTURES/clean" "$FIXTURES/dirty-task-var" diff --git a/test/lint/bats-test-task/bats-test-task.bats b/test/lint/bats-test-task/bats-test-task.bats index 295d92f..f761f2b 100644 --- a/test/lint/bats-test-task/bats-test-task.bats +++ b/test/lint/bats-test-task/bats-test-task.bats @@ -7,9 +7,7 @@ setup() { FIXTURES="$BATS_TEST_DIRNAME/fixtures" } -# ============================================================================ # Pass paths -# ============================================================================ @test "bats-test-task: passes on the canonical pattern" { run codebase lint:bats-test-task "$FIXTURES/clean" @@ -33,9 +31,7 @@ setup() { [[ "$output" == *"OK"*"shimmer-variant"* ]] } -# ============================================================================ # Failure modes -# ============================================================================ @test "bats-test-task: flags missing USAGE arg spec" { run codebase lint:bats-test-task "$FIXTURES/missing-usage-arg" @@ -80,9 +76,7 @@ setup() { [[ "$output" != *"invocations found"* ]] } -# ============================================================================ # Ignore directive -# ============================================================================ @test "bats-test-task: 'codebase:ignore bats-test-task' in mise.toml skips the target" { run codebase lint:bats-test-task "$FIXTURES/ignored-file" @@ -90,9 +84,7 @@ setup() { [[ "$output" == *"SKIP"*"ignored-file"* ]] } -# ============================================================================ # Output details -# ============================================================================ @test "bats-test-task: fail output includes the remediation hint" { run codebase lint:bats-test-task "$FIXTURES/missing-usage-arg" @@ -109,9 +101,7 @@ setup() { [[ "$output" == *"invocations"* ]] } -# ============================================================================ # Error handling -# ============================================================================ @test "bats-test-task: fails when no targets given" { run codebase lint:bats-test-task @@ -125,9 +115,7 @@ setup() { [[ "$output" == *"does not exist"* ]] } -# ============================================================================ # Multi-target -# ============================================================================ @test "bats-test-task: accepts multiple targets and reports each" { run codebase lint:bats-test-task "$FIXTURES/clean" "$FIXTURES/missing-examples" diff --git a/test/lint/gum-table/gum-table.bats b/test/lint/gum-table/gum-table.bats index 2dfddaa..809b670 100644 --- a/test/lint/gum-table/gum-table.bats +++ b/test/lint/gum-table/gum-table.bats @@ -7,9 +7,7 @@ setup() { FIXTURES="$BATS_TEST_DIRNAME/fixtures" } -# ============================================================================ # High confidence: column -t (always a true positive) -# ============================================================================ @test "column-t: detects piping to column -t" { run codebase lint:gum-table "$FIXTURES/manual-padding/task-c" @@ -18,9 +16,7 @@ setup() { [[ "$output" == *"WARN"* ]] } -# ============================================================================ # High confidence: printf padding inside a loop -# ============================================================================ @test "loop-table: detects printf %-Ns inside while-read" { run codebase lint:gum-table "$FIXTURES/manual-padding/task-b" @@ -45,9 +41,7 @@ setup() { echo "$output" | grep "padding" | grep -q "INFO" } -# ============================================================================ # Low confidence: printf padding outside loops (INFO only, not a failure) -# ============================================================================ @test "padding: printf %-Ns outside loop is INFO, not a failure" { run codebase lint:gum-table "$FIXTURES/manual-padding/task-a" @@ -69,9 +63,7 @@ setup() { [[ "$output" == *"INFO"* ]] } -# ============================================================================ # True negatives — no output at all -# ============================================================================ @test "clean: already using gum table" { run codebase lint:gum-table "$FIXTURES/clean/task-gum" @@ -103,9 +95,7 @@ setup() { [[ "$output" == *"OK"* ]] } -# ============================================================================ # Multi-file scanning -# ============================================================================ @test "directory scan finds high-confidence hits" { run codebase lint:gum-table "$FIXTURES/manual-padding" @@ -121,9 +111,7 @@ setup() { [ "$status" -eq 0 ] } -# ============================================================================ # Output format -# ============================================================================ @test "WARN output includes file path, category, and line number" { run codebase lint:gum-table "$FIXTURES/manual-padding/task-b" @@ -136,9 +124,7 @@ setup() { [[ "$output" =~ INFO.*task-a:\[padding\].*[0-9]+: ]] } -# ============================================================================ # Error handling -# ============================================================================ @test "fails when target does not exist" { run codebase lint:gum-table /nonexistent @@ -146,9 +132,7 @@ setup() { [[ "$output" == *"ERROR"* ]] } -# ============================================================================ # Relative path resolution (regression: codebase#24) -# ============================================================================ @test "relative path resolves against CODEBASE_CALLER_PWD (dirty fixture)" { # Regression: relative targets resolved against codebase's install diff --git a/test/lint/mcr-scope/mcr-scope.bats b/test/lint/mcr-scope/mcr-scope.bats index 3ae6215..49869b4 100644 --- a/test/lint/mcr-scope/mcr-scope.bats +++ b/test/lint/mcr-scope/mcr-scope.bats @@ -7,9 +7,7 @@ setup() { FIXTURES="$BATS_TEST_DIRNAME/fixtures" } -# ============================================================================ # Detection -# ============================================================================ @test "mcr-scope: passes on a clean codebase" { run codebase lint:mcr-scope "$FIXTURES/clean" @@ -102,9 +100,7 @@ setup() { [[ "$output" == *"no test/ or lib/ files found"* ]] } -# ============================================================================ # Ignore directives -# ============================================================================ @test "mcr-scope: inline '# codebase:ignore' suppresses a single line" { run codebase lint:mcr-scope "$FIXTURES/ignored-inline" @@ -118,9 +114,7 @@ setup() { [[ "$output" == *"SKIP"*"ignored-file"* ]] } -# ============================================================================ # Output details -# ============================================================================ @test "mcr-scope: fail output includes file:line citations" { run codebase lint:mcr-scope "$FIXTURES/dirty-lib" @@ -136,9 +130,7 @@ setup() { [[ "$output" == *"BASH_SOURCE"* ]] } -# ============================================================================ # Error handling -# ============================================================================ @test "mcr-scope: fails when no targets given" { run codebase lint:mcr-scope @@ -153,9 +145,7 @@ setup() { [[ "$output" == *"does not exist"* ]] } -# ============================================================================ # Multi-target -# ============================================================================ @test "mcr-scope: accepts multiple targets and reports each" { run codebase lint:mcr-scope "$FIXTURES/clean" "$FIXTURES/dirty-test" diff --git a/test/lint/mise-settings/mise-settings.bats b/test/lint/mise-settings/mise-settings.bats index 565be0a..d925495 100644 --- a/test/lint/mise-settings/mise-settings.bats +++ b/test/lint/mise-settings/mise-settings.bats @@ -7,9 +7,7 @@ setup() { FIXTURES="$BATS_TEST_DIRNAME/fixtures" } -# ============================================================================ # Detection -# ============================================================================ @test "lint: passes when all settings present" { run codebase lint:mise-settings "$FIXTURES/complete" @@ -54,9 +52,7 @@ setup() { [[ "$output" == *"FAIL"*"missing-both"* ]] } -# ============================================================================ # Fix mode -# ============================================================================ @test "fix: adds missing settings" { WORK_DIR="$BATS_TEST_TMPDIR/fix-test" @@ -96,9 +92,7 @@ setup() { [[ "$output" == *"OK"* ]] } -# ============================================================================ # Error handling -# ============================================================================ @test "lint: fails when target does not exist" { run codebase lint:mise-settings /nonexistent diff --git a/test/lint/or-true/or-true.bats b/test/lint/or-true/or-true.bats index 31d7776..68cfaca 100644 --- a/test/lint/or-true/or-true.bats +++ b/test/lint/or-true/or-true.bats @@ -7,9 +7,7 @@ setup() { FIXTURES="$BATS_TEST_DIRNAME/fixtures" } -# ============================================================================ # Detection -# ============================================================================ @test "or-true: passes on a clean codebase" { run codebase lint:or-true "$FIXTURES/clean" @@ -76,9 +74,7 @@ setup() { [[ "$output" == *"if !"* ]] } -# ============================================================================ # Corpus-calibrated diagnostics -# ============================================================================ @test "or-true: arithmetic increments get a safer arithmetic suggestion" { local tmp @@ -173,9 +169,7 @@ EOF [[ "$output" == *"Intentional cases need a rule-specific reason"* ]] } -# ============================================================================ # Ignore directives -# ============================================================================ @test "or-true: inline '# codebase:ignore or-true — reason' skips the line" { run codebase lint:or-true "$FIXTURES/ignored-inline" @@ -189,9 +183,7 @@ EOF [[ "$output" == *"SKIP"*"ignored-file"* ]] } -# ============================================================================ # Scope / discovery -# ============================================================================ @test "or-true: walks the whole target — finds hits outside .mise/tasks and lib/" { run codebase lint:or-true "$FIXTURES/broad-walk" @@ -213,9 +205,7 @@ EOF [[ "$output" == *"no shell files"* ]] } -# ============================================================================ # Discovery correctness -# ============================================================================ @test "or-true: discovery skips non-bash/sh shebangs (fish, zsh, …)" { # Regression: the shebang regex '^#!.*(bash|sh)\b' matched 'sh' as @@ -255,9 +245,7 @@ EOF rm -rf "$tmp" } -# ============================================================================ # Comment handling -# ============================================================================ @test "or-true: does not flag '|| true' inside a single-quoted string" { # Accidental protection: the closing quote ''' is not in the @@ -293,9 +281,7 @@ EOF rm -rf "$tmp" } -# ============================================================================ # Multi-target -# ============================================================================ @test "or-true: checks multiple targets and reports each" { run codebase lint:or-true "$FIXTURES/clean" "$FIXTURES/dirty" @@ -309,9 +295,7 @@ EOF [ "$status" -eq 2 ] } -# ============================================================================ # Error paths -# ============================================================================ @test "or-true: fails when target does not exist" { run codebase lint:or-true "$FIXTURES/does-not-exist" @@ -328,9 +312,7 @@ EOF [[ "$output" == *""* ]] } -# ============================================================================ # Relative path resolution (regression: codebase#24) -# ============================================================================ @test "or-true: relative path resolves against CODEBASE_CALLER_PWD, not codebase install dir" { # Regression: when invoked via the shiv shim, relative paths resolved diff --git a/test/lint/shellcheck/shellcheck.bats b/test/lint/shellcheck/shellcheck.bats index 370c7d6..f0ac032 100644 --- a/test/lint/shellcheck/shellcheck.bats +++ b/test/lint/shellcheck/shellcheck.bats @@ -7,9 +7,7 @@ setup() { FIXTURES="$BATS_TEST_DIRNAME/fixtures" } -# ============================================================================ # Detection -# ============================================================================ @test "lint: passes on a clean codebase" { run codebase lint:shellcheck "$FIXTURES/clean" @@ -37,9 +35,7 @@ setup() { [[ "$output" == *"SC"* ]] } -# ============================================================================ # Ignore directive -# ============================================================================ @test "lint: skips when codebase:ignore shellcheck is set in mise.toml" { run codebase lint:shellcheck "$FIXTURES/ignored" @@ -76,9 +72,7 @@ setup() { [[ "$output" == *"SC2154"* ]] } -# ============================================================================ # Scope -# ============================================================================ @test "lint: works on a codebase with no mise.toml" { run codebase lint:shellcheck "$FIXTURES/no-toml" @@ -100,9 +94,7 @@ setup() { [[ "$output" == *"bad.sh"* ]] } -# ============================================================================ # Multi-target -# ============================================================================ @test "lint: checks multiple targets and reports each" { run codebase lint:shellcheck "$FIXTURES/clean" "$FIXTURES/dirty" @@ -116,9 +108,7 @@ setup() { [ "$status" -eq 2 ] } -# ============================================================================ # Error paths -# ============================================================================ @test "lint: fails when target does not exist" { run codebase lint:shellcheck "$FIXTURES/does-not-exist" diff --git a/test/lint/usage-flag-naming/fixtures/clean-boolean/.mise/tasks/bar b/test/lint/usage-flag-naming/fixtures/clean-boolean/.mise/tasks/bar index 1861eb6..6ff671d 100644 --- a/test/lint/usage-flag-naming/fixtures/clean-boolean/.mise/tasks/bar +++ b/test/lint/usage-flag-naming/fixtures/clean-boolean/.mise/tasks/bar @@ -1,3 +1,3 @@ #!/usr/bin/env bash #USAGE flag "--verbose" help="Enable verbose output" -echo "boolean only" \ No newline at end of file +echo "boolean only" diff --git a/test/lint/usage-flag-naming/fixtures/clean-match/.mise/tasks/foo b/test/lint/usage-flag-naming/fixtures/clean-match/.mise/tasks/foo index 593966a..f3c7746 100644 --- a/test/lint/usage-flag-naming/fixtures/clean-match/.mise/tasks/foo +++ b/test/lint/usage-flag-naming/fixtures/clean-match/.mise/tasks/foo @@ -1,3 +1,3 @@ #!/usr/bin/env bash #USAGE flag "--exclude " help="Patterns to exclude" -echo "clean task" \ No newline at end of file +echo "clean task" diff --git a/test/lint/usage-flag-naming/fixtures/clean-short-only/.mise/tasks/baz b/test/lint/usage-flag-naming/fixtures/clean-short-only/.mise/tasks/baz index c6e41f8..4e57151 100644 --- a/test/lint/usage-flag-naming/fixtures/clean-short-only/.mise/tasks/baz +++ b/test/lint/usage-flag-naming/fixtures/clean-short-only/.mise/tasks/baz @@ -1,3 +1,3 @@ #!/usr/bin/env bash #USAGE flag "-v" help="Verbose mode" -echo "short only" \ No newline at end of file +echo "short only" diff --git a/test/lint/usage-flag-naming/fixtures/dirty-mismatch/.mise/tasks/scan b/test/lint/usage-flag-naming/fixtures/dirty-mismatch/.mise/tasks/scan index 2f3de1d..811231a 100644 --- a/test/lint/usage-flag-naming/fixtures/dirty-mismatch/.mise/tasks/scan +++ b/test/lint/usage-flag-naming/fixtures/dirty-mismatch/.mise/tasks/scan @@ -1,4 +1,4 @@ #!/usr/bin/env bash #USAGE flag "-e --exclude " help="Patterns to exclude" usage_excludes="*.tmp" -echo "mismatched flag/placeholder" \ No newline at end of file +echo "mismatched flag/placeholder" diff --git a/test/lint/usage-flag-naming/fixtures/dirty-multi-word/.mise/tasks/fetch b/test/lint/usage-flag-naming/fixtures/dirty-multi-word/.mise/tasks/fetch index 404bb2e..1f8812a 100644 --- a/test/lint/usage-flag-naming/fixtures/dirty-multi-word/.mise/tasks/fetch +++ b/test/lint/usage-flag-naming/fixtures/dirty-multi-word/.mise/tasks/fetch @@ -1,4 +1,4 @@ #!/usr/bin/env bash #USAGE flag "--search-path " help="Directories to search" usage_search_paths="./src" -echo "multi-word mismatch" \ No newline at end of file +echo "multi-word mismatch" diff --git a/test/lint/usage-flag-naming/fixtures/ignored-file/.mise/tasks/task b/test/lint/usage-flag-naming/fixtures/ignored-file/.mise/tasks/task index 29efca2..dbbadcb 100644 --- a/test/lint/usage-flag-naming/fixtures/ignored-file/.mise/tasks/task +++ b/test/lint/usage-flag-naming/fixtures/ignored-file/.mise/tasks/task @@ -1,4 +1,4 @@ #!/usr/bin/env bash #USAGE flag "-e --exclude " help="Patterns to exclude" usage_excludes="*.tmp" -echo "file-level ignored task" \ No newline at end of file +echo "file-level ignored task" diff --git a/test/lint/usage-flag-naming/fixtures/ignored-file/mise.toml b/test/lint/usage-flag-naming/fixtures/ignored-file/mise.toml index cdae74f..c23777d 100644 --- a/test/lint/usage-flag-naming/fixtures/ignored-file/mise.toml +++ b/test/lint/usage-flag-naming/fixtures/ignored-file/mise.toml @@ -1,2 +1,2 @@ [_.codebase] -codebase:ignore usage-flag-naming \ No newline at end of file +codebase:ignore usage-flag-naming diff --git a/test/lint/usage-flag-naming/fixtures/ignored-inline/.mise/tasks/task b/test/lint/usage-flag-naming/fixtures/ignored-inline/.mise/tasks/task index b5fb558..d30d229 100644 --- a/test/lint/usage-flag-naming/fixtures/ignored-inline/.mise/tasks/task +++ b/test/lint/usage-flag-naming/fixtures/ignored-inline/.mise/tasks/task @@ -1,4 +1,4 @@ #!/usr/bin/env bash #USAGE flag "-e --exclude " help="Patterns to exclude" # codebase:ignore=usage-flag-naming -- legacy name mismatch usage_excludes="*.tmp" -echo "inline ignore" \ No newline at end of file +echo "inline ignore" diff --git a/test/lint/usage-flag-naming/fixtures/mixed/.mise/tasks/bad b/test/lint/usage-flag-naming/fixtures/mixed/.mise/tasks/bad index 9ab1609..4a5b396 100644 --- a/test/lint/usage-flag-naming/fixtures/mixed/.mise/tasks/bad +++ b/test/lint/usage-flag-naming/fixtures/mixed/.mise/tasks/bad @@ -1,4 +1,4 @@ #!/usr/bin/env bash #USAGE flag "-e --exclude " help="Patterns to exclude" usage_excludes="*.tmp" -echo "bad task" \ No newline at end of file +echo "bad task" diff --git a/test/lint/usage-flag-naming/fixtures/mixed/.mise/tasks/good b/test/lint/usage-flag-naming/fixtures/mixed/.mise/tasks/good index 0853e69..c38f82e 100644 --- a/test/lint/usage-flag-naming/fixtures/mixed/.mise/tasks/good +++ b/test/lint/usage-flag-naming/fixtures/mixed/.mise/tasks/good @@ -1,4 +1,4 @@ #!/usr/bin/env bash #USAGE flag "--model " help="Model name" usage_model="gpt-4" -echo "good task" \ No newline at end of file +echo "good task" diff --git a/test/lint/usage-flag-naming/fixtures/multi-flag/.mise/tasks/task b/test/lint/usage-flag-naming/fixtures/multi-flag/.mise/tasks/task index 3b96f2d..dee42f8 100644 --- a/test/lint/usage-flag-naming/fixtures/multi-flag/.mise/tasks/task +++ b/test/lint/usage-flag-naming/fixtures/multi-flag/.mise/tasks/task @@ -3,4 +3,4 @@ #USAGE flag "--name " help="Name to use" usage_verbose=true usage_name="default" -echo "multi-flag task" \ No newline at end of file +echo "multi-flag task" diff --git a/test/lint/usage-flag-naming/usage-flag-naming.bats b/test/lint/usage-flag-naming/usage-flag-naming.bats index 8acf0ec..bb5d9a0 100644 --- a/test/lint/usage-flag-naming/usage-flag-naming.bats +++ b/test/lint/usage-flag-naming/usage-flag-naming.bats @@ -7,9 +7,7 @@ setup() { FIXTURES="$BATS_TEST_DIRNAME/fixtures" } -# ============================================================================ # Clean cases — should pass -# ============================================================================ @test "usage-flag-naming: passes on clean task (flag/placeholder match)" { run codebase lint:usage-flag-naming "$FIXTURES/clean-match" @@ -41,9 +39,7 @@ setup() { [[ "$output" == *"OK"* ]] } -# ============================================================================ # Dirty cases — should fail -# ============================================================================ @test "usage-flag-naming: flags mismatched flag/placeholder" { run codebase lint:usage-flag-naming "$FIXTURES/dirty-mismatch" @@ -67,9 +63,7 @@ setup() { [[ "$output" == *"usage_search_paths"* ]] } -# ============================================================================ # Mixed cases -# ============================================================================ @test "usage-flag-naming: flags only dirty tasks in mixed target" { run codebase lint:usage-flag-naming "$FIXTURES/mixed" @@ -80,9 +74,7 @@ setup() { [[ "$output" != *".mise/tasks/good"* ]] } -# ============================================================================ # Ignore mechanisms -# ============================================================================ @test "usage-flag-naming: respects inline codebase:ignore" { run codebase lint:usage-flag-naming "$FIXTURES/ignored-inline" @@ -96,9 +88,7 @@ setup() { [[ "$output" == *"SKIP"* ]] } -# ============================================================================ # Output format -# ============================================================================ @test "usage-flag-naming: output includes task file path and line number" { run codebase lint:usage-flag-naming "$FIXTURES/dirty-mismatch" @@ -114,9 +104,7 @@ setup() { [[ "$output" == *"mise"* ]] } -# ============================================================================ # Edge cases -# ============================================================================ @test "usage-flag-naming: handles empty target directory" { run codebase lint:usage-flag-naming "$FIXTURES/no-tasks" @@ -168,4 +156,4 @@ TASK [ "$status" -eq 0 ] [[ "$output" == *"OK"* ]] rm -rf "$tmpdir" -} \ No newline at end of file +} diff --git a/test/migrations/task-pattern/task-pattern.bats b/test/migrations/task-pattern/task-pattern.bats index d86c33e..5f8a90d 100644 --- a/test/migrations/task-pattern/task-pattern.bats +++ b/test/migrations/task-pattern/task-pattern.bats @@ -23,9 +23,7 @@ assert_matches_before() { diff -u "$FIXTURES/before/$file" "$WORK_DIR/$file" } -# ============================================================================ # Individual variant tests -# ============================================================================ @test "migrate: simple mise run → _task" { codebase migrate:task-pattern "$WORK_DIR" @@ -57,9 +55,7 @@ assert_matches_before() { assert_matches_after ".mise/tasks/error-strings" } -# ============================================================================ # Full migration test -# ============================================================================ @test "migrate: all files match expected after state" { codebase migrate:task-pattern "$WORK_DIR" @@ -68,9 +64,7 @@ assert_matches_before() { [ "$status" -eq 0 ] } -# ============================================================================ # Reverse migration tests -# ============================================================================ @test "reverse: _task → mise run" { # Start from the after state @@ -101,9 +95,7 @@ assert_matches_before() { ! grep -q '_task' "$WORK_DIR/.mise/tasks/in-subshell" } -# ============================================================================ # Round-trip tests -# ============================================================================ @test "round-trip: forward then reverse restores lossless fixtures" { # Only test fixtures where forward is lossless (no -q flag) @@ -114,9 +106,7 @@ assert_matches_before() { assert_matches_before ".mise/tasks/error-strings" } -# ============================================================================ # Error handling -# ============================================================================ @test "migrate: fails when target does not exist" { run codebase migrate:task-pattern /nonexistent diff --git a/test/pre-commit/pre-commit.bats b/test/pre-commit/pre-commit.bats index 96bd4f2..c0fbddc 100644 --- a/test/pre-commit/pre-commit.bats +++ b/test/pre-commit/pre-commit.bats @@ -24,9 +24,7 @@ EOF export CODEBASE_CALLER_PWD="$REPO" } -# ============================================================================ # Install — fresh repo -# ============================================================================ @test "install: creates dispatcher" { codebase pre-commit @@ -97,9 +95,7 @@ EOF [ -x "$REPO/.git/hooks/pre-commit" ] } -# ============================================================================ # Install — existing dispatcher -# ============================================================================ @test "install: preserves existing dispatcher and other hooks" { mkdir -p "$REPO/.git/hooks/pre-commit.d" @@ -121,9 +117,7 @@ EOF [ -f "$REPO/.git/hooks/pre-commit.d/codebase" ] } -# ============================================================================ # Install — existing plain hook (not a dispatcher) -# ============================================================================ @test "install: errors when existing plain hook is not a dispatcher" { cat > "$REPO/.git/hooks/pre-commit" <<'EOF' @@ -137,9 +131,7 @@ EOF [[ "$output" == *"not a dispatcher"* ]] } -# ============================================================================ # Idempotent -# ============================================================================ @test "install: running twice is safe" { codebase pre-commit @@ -149,9 +141,7 @@ EOF [ -f "$REPO/.git/hooks/pre-commit.d/codebase" ] } -# ============================================================================ # --check -# ============================================================================ @test "check: exits 0 when hook is current" { codebase pre-commit @@ -189,9 +179,7 @@ EOF [ "$status" -ne 0 ] } -# ============================================================================ # --revert -# ============================================================================ @test "revert: removes codebase hook" { codebase pre-commit @@ -225,9 +213,7 @@ EOF [[ "$output" == *"No codebase hook"* ]] } -# ============================================================================ # Scope -# ============================================================================ @test "scope: default scopes are delegated to aggregate lint" { cat > "$REPO/mise.toml" <<'EOF' @@ -258,9 +244,7 @@ EOF ! grep -q 'src/scripts' "$REPO/.git/hooks/pre-commit.d/codebase" } -# ============================================================================ # Error handling -# ============================================================================ @test "error: fails outside git repo" { export CODEBASE_CALLER_PWD="$BATS_TEST_TMPDIR" diff --git a/test/scan/scan.bats b/test/scan/scan.bats index 9552433..e52a500 100644 --- a/test/scan/scan.bats +++ b/test/scan/scan.bats @@ -8,9 +8,7 @@ setup() { FIXTURES_B="$BATS_TEST_DIRNAME/fixtures-b" } -# ============================================================================ # Single target (basic matching) -# ============================================================================ @test "scan: finds mise run calls in extension-less task files" { run codebase scan -p 'mise run $$$ARGS' "$FIXTURES_A" @@ -39,9 +37,7 @@ setup() { [[ -z "$output" ]] } -# ============================================================================ # Different patterns -# ============================================================================ @test "scan: finds _task calls with custom pattern" { run codebase scan -p '_task $$$ARGS' "$FIXTURES_A" @@ -56,9 +52,7 @@ setup() { [ "$count" -eq 4 ] } -# ============================================================================ # Multiple targets -# ============================================================================ @test "multi: finds matches across multiple codebases" { run codebase scan -p 'mise run $$$ARGS' "$FIXTURES_A" "$FIXTURES_B" @@ -84,9 +78,7 @@ setup() { [[ "$output" != *"fixtures-b:"* ]] } -# ============================================================================ # Exclude filter -# ============================================================================ @test "exclude: filters out files matching glob" { run codebase scan -p 'mise run $$$ARGS' -e '.mise/tasks/ci/*' "$FIXTURES_A" @@ -110,9 +102,7 @@ setup() { [[ "$output" != *"ci/deploy"* ]] } -# ============================================================================ # Error handling -# ============================================================================ @test "error: fails when no pattern provided" { run codebase scan "$FIXTURES_A"