diff --git a/.mise/tasks/lint/mise-run-quiet b/.mise/tasks/lint/mise-run-quiet new file mode 100644 index 0000000..f3018cc --- /dev/null +++ b/.mise/tasks/lint/mise-run-quiet @@ -0,0 +1,227 @@ +#!/usr/bin/env bash +#MISE description="Flag mise run without -q in clean-output contexts" +#USAGE arg "…" help="Paths to codebases to check (one or more)" +#USAGE example "codebase lint:mise-run-quiet ." header="Flag mise run calls missing -q" +#USAGE example "codebase lint:mise-run-quiet /path/to/repo" + +set -euo pipefail + +# shellcheck source=../../../lib/shell-files.sh +source "$MISE_CONFIG_ROOT/lib/shell-files.sh" + +# Rationale: 'mise run' without -q prints task runner headers such as +# '[shell] $ ...' to stderr. That is useful for debugging, but harmful +# when the command output is being eval'd, piped, embedded in generated +# output, or used in shell startup. +# +# Example failure mode: eval "$(shiv shell)" visibly printed the task +# header in the terminal on every shell startup. +# +# Contexts flagged: +# - command substitution: $(mise run ...) — header goes into captured output +# - eval: eval "$(mise run ...)" — header pollutes the eval'd string +# - pipeline: mise run ... | consumer — header goes into the pipe +# +# Safe contexts (not flagged): +# - mise run -q ... — already quiet +# - mise run ... 2>/dev/null — stderr explicitly discarded +# - bare mise run at line start (interactive/debug) — no capture +# - mise run in comments or string literals + +IFS=' ' read -ra TARGETS <<< "${usage_targets}" + +if [[ ${#TARGETS[@]} -eq 0 ]]; then + echo "ERROR: at least one target is required" >&2 + exit 1 +fi + +# Resolve relative paths against CALLER_PWD (see lib/shell-files.sh). +for i in "${!TARGETS[@]}"; do + TARGETS[i]=$(resolve_target "${TARGETS[i]}") +done + +# ── Helpers ────────────────────────────────────────────────────────────── + +# has_stderr_redirect +# Returns 0 if the line redirects stderr (2>...). +has_stderr_redirect() { + local line="$1" + echo "$line" | grep -qE '2>[>&]?[^&[:space:]]' 2>/dev/null +} + +# is_pattern_match +# Returns 0 if 'mise run' appears inside a grep/sed/awk pattern or string +# literal rather than as an actual command invocation. +is_pattern_match() { + local line="$1" + # grep/sed/awk with 'mise run' in the pattern (single or double quoted) + echo "$line" | grep -qE "(grep|sed|awk)[[:space:]].*['\"].*mise run" 2>/dev/null && return 0 + # echo with 'mise run' in a string + echo "$line" | grep -qE "echo[[:space:]].*['\"].*mise run" 2>/dev/null && return 0 + # printf with 'mise run' in a format string + echo "$line" | grep -qE "printf[[:space:]].*['\"].*mise run" 2>/dev/null && return 0 + return 1 +} + +# classify_context +# Emit the context classification for a mise run invocation. +classify_context() { + local line="$1" + + # Skip if mise run is inside a grep/sed pattern or string literal + is_pattern_match "$line" && echo "pattern" && return + + # Already quiet — skip + if echo "$line" | grep -qE 'mise run\s+-q'; then + echo "quiet" + return + fi + + # eval context: eval "$(mise run ...)" + if echo "$line" | grep -qE 'eval\s+"\$\([^)]*mise run'; then + echo "eval" + return + fi + + # Command substitution context: $(mise run ...) + if echo "$line" | grep -qE '\$\([^)]*mise run'; then + echo "command-sub" + return + fi + + # Pipeline context: mise run ... | consumer + if echo "$line" | grep -qE 'mise run[^|]*\|'; then + echo "pipeline" + return + fi + + # Has stderr redirect — safe + if has_stderr_redirect "$line"; then + echo "stderr-redirect" + return + fi + + # Bare mise run (interactive/debug) — safe + echo "bare" +} + +# has_line_ignore +# Returns 0 if the line has a rule-specific inline ignore. +has_line_ignore() { + local line="$1" + echo "$line" | grep -qE 'codebase:ignore[[:space:]]+mise-run-quiet' 2>/dev/null +} + +# scan_file +# Emit flagged line numbers and context for mise run calls without -q. +scan_file() { + local file="$1" + local lineno=0 + local line + + while IFS= read -r line || [[ -n "$line" ]]; do + lineno=$((lineno + 1)) + + # Skip full-line comments. + [[ "$line" =~ ^[[:space:]]*# ]] && continue + + # Skip lines without mise run. + [[ "$line" != *"mise run"* ]] && continue + + # Rule-specific inline ignore with reason is accepted. + has_line_ignore "$line" && continue + + # Classify the context. + local context + context=$(classify_context "$line") + + case "$context" in + quiet|stderr-redirect|bare|pattern) + # Safe contexts — skip + ;; + command-sub) + local trimmed + trimmed="${line#"${line%%[![:space:]]*}"}" + echo "[command-sub] $lineno: $trimmed" + ;; + eval) + local trimmed + trimmed="${line#"${line%%[![:space:]]*}"}" + echo "[eval] $lineno: $trimmed" + ;; + pipeline) + local trimmed + trimmed="${line#"${line%%[![:space:]]*}"}" + echo "[pipeline] $lineno: $trimmed" + ;; + esac + done < "$file" +} + +# ── Main ────────────────────────────────────────────────────────────────── + +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 -m1 -q 'codebase:ignore mise-run-quiet' "$toml"; then + echo "SKIP $name (codebase:ignore)" + continue + fi + + # Collect shell files + files=() + while IFS= read -r f; do + [[ -n "$f" ]] && files+=("$f") + done < <(discover_shell_files "$target") + + if [[ ${#files[@]} -eq 0 ]]; then + echo "OK $name (no shell files found)" + continue + fi + + # Scan each file; collect hits + hit_count=0 + warn_count=0 + target_output="" + for file in "${files[@]}"; do + rel="${file#"$target"/}" + while IFS= read -r hit; do + [[ -z "$hit" ]] && continue + target_output+=" $rel:$hit"$'\n' + if [[ "$hit" == \[command-sub\]* ]] || [[ "$hit" == \[eval\]* ]]; then + hit_count=$((hit_count + 1)) + elif [[ "$hit" == \[pipeline\]* ]]; then + warn_count=$((warn_count + 1)) + fi + done < <(scan_file "$file") + done + + if [[ "$hit_count" -gt 0 ]] || [[ "$warn_count" -gt 0 ]]; then + detail="" + [[ "$hit_count" -gt 0 ]] && detail+="$hit_count mise run call(s) without -q in capture context(s)" + [[ "$hit_count" -gt 0 && "$warn_count" -gt 0 ]] && detail+=", " + [[ "$warn_count" -gt 0 ]] && detail+="$warn_count pipeline(s) without -q" + echo "FAIL $name: $detail" + printf '%s' "$target_output" + cat <<'HINT' + hint: Add -q to mise run calls in capture contexts: mise run -q + For pipelines: mise run -q | consumer + For eval: eval "$(mise run -q )" + For command substitution: $(mise run -q ) +HINT + failures=$((failures + 1)) + else + echo "OK $name (${#files[@]} file(s) clean)" + fi +done + +exit "$failures" 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-run-quiet/fixtures/clean-already-quiet/.mise/tasks/t b/test/lint/mise-run-quiet/fixtures/clean-already-quiet/.mise/tasks/t new file mode 100644 index 0000000..cfee627 --- /dev/null +++ b/test/lint/mise-run-quiet/fixtures/clean-already-quiet/.mise/tasks/t @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Already using -q — should be fine +mise run -q build +mise run -q test -- --filter unit +result=$(mise run -q status) diff --git a/test/lint/mise-run-quiet/fixtures/clean-already-quiet/mise.toml b/test/lint/mise-run-quiet/fixtures/clean-already-quiet/mise.toml new file mode 100644 index 0000000..8a0a5a4 --- /dev/null +++ b/test/lint/mise-run-quiet/fixtures/clean-already-quiet/mise.toml @@ -0,0 +1,2 @@ +[settings] +quiet = true diff --git a/test/lint/mise-run-quiet/fixtures/clean/.mise/tasks/t b/test/lint/mise-run-quiet/fixtures/clean/.mise/tasks/t new file mode 100644 index 0000000..559debd --- /dev/null +++ b/test/lint/mise-run-quiet/fixtures/clean/.mise/tasks/t @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +# No mise run calls here — just regular shell commands +echo "hello world" +ls -la +git status diff --git a/test/lint/mise-run-quiet/fixtures/clean/mise.toml b/test/lint/mise-run-quiet/fixtures/clean/mise.toml new file mode 100644 index 0000000..8a0a5a4 --- /dev/null +++ b/test/lint/mise-run-quiet/fixtures/clean/mise.toml @@ -0,0 +1,2 @@ +[settings] +quiet = true diff --git a/test/lint/mise-run-quiet/fixtures/dirty-command-sub/.mise/tasks/t b/test/lint/mise-run-quiet/fixtures/dirty-command-sub/.mise/tasks/t new file mode 100644 index 0000000..493ccb8 --- /dev/null +++ b/test/lint/mise-run-quiet/fixtures/dirty-command-sub/.mise/tasks/t @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Command substitution — header goes into captured output +result=$(mise run build) +echo "$result" + +# Also in a more complex expression +count=$(mise run count --all) diff --git a/test/lint/mise-run-quiet/fixtures/dirty-command-sub/mise.toml b/test/lint/mise-run-quiet/fixtures/dirty-command-sub/mise.toml new file mode 100644 index 0000000..8a0a5a4 --- /dev/null +++ b/test/lint/mise-run-quiet/fixtures/dirty-command-sub/mise.toml @@ -0,0 +1,2 @@ +[settings] +quiet = true diff --git a/test/lint/mise-run-quiet/fixtures/dirty-eval/.mise/tasks/t b/test/lint/mise-run-quiet/fixtures/dirty-eval/.mise/tasks/t new file mode 100644 index 0000000..4deeb26 --- /dev/null +++ b/test/lint/mise-run-quiet/fixtures/dirty-eval/.mise/tasks/t @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +# eval — header pollutes the eval'd string +eval "$(mise run env)" + +# Also with additional flags +eval "$(mise run shell --init)" diff --git a/test/lint/mise-run-quiet/fixtures/dirty-eval/mise.toml b/test/lint/mise-run-quiet/fixtures/dirty-eval/mise.toml new file mode 100644 index 0000000..8a0a5a4 --- /dev/null +++ b/test/lint/mise-run-quiet/fixtures/dirty-eval/mise.toml @@ -0,0 +1,2 @@ +[settings] +quiet = true diff --git a/test/lint/mise-run-quiet/fixtures/dirty-mixed/.mise/tasks/t b/test/lint/mise-run-quiet/fixtures/dirty-mixed/.mise/tasks/t new file mode 100644 index 0000000..9a3a05b --- /dev/null +++ b/test/lint/mise-run-quiet/fixtures/dirty-mixed/.mise/tasks/t @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Command substitution +result=$(mise run build) + +# eval +eval "$(mise run env)" + +# Pipeline +mise run check | grep error + +# Bare invocation (interactive) — should be fine +mise run status + +# Already quiet — should be fine +mise run -q deploy + +# Stderr redirect — should be fine +mise run lint 2>/dev/null diff --git a/test/lint/mise-run-quiet/fixtures/dirty-mixed/mise.toml b/test/lint/mise-run-quiet/fixtures/dirty-mixed/mise.toml new file mode 100644 index 0000000..8a0a5a4 --- /dev/null +++ b/test/lint/mise-run-quiet/fixtures/dirty-mixed/mise.toml @@ -0,0 +1,2 @@ +[settings] +quiet = true diff --git a/test/lint/mise-run-quiet/fixtures/dirty-pipe/.mise/tasks/t b/test/lint/mise-run-quiet/fixtures/dirty-pipe/.mise/tasks/t new file mode 100644 index 0000000..3ed5dfe --- /dev/null +++ b/test/lint/mise-run-quiet/fixtures/dirty-pipe/.mise/tasks/t @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Pipeline — header goes into the pipe +mise run check | grep error + +# Also with multiple pipes +mise run list | sort | uniq diff --git a/test/lint/mise-run-quiet/fixtures/dirty-pipe/mise.toml b/test/lint/mise-run-quiet/fixtures/dirty-pipe/mise.toml new file mode 100644 index 0000000..8a0a5a4 --- /dev/null +++ b/test/lint/mise-run-quiet/fixtures/dirty-pipe/mise.toml @@ -0,0 +1,2 @@ +[settings] +quiet = true diff --git a/test/lint/mise-run-quiet/fixtures/ignored-file/.mise/tasks/t b/test/lint/mise-run-quiet/fixtures/ignored-file/.mise/tasks/t new file mode 100644 index 0000000..33859ff --- /dev/null +++ b/test/lint/mise-run-quiet/fixtures/ignored-file/.mise/tasks/t @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +# This file has repo-level ignore, so violations are skipped +result=$(mise run build) +eval "$(mise run env)" +mise run check | grep error diff --git a/test/lint/mise-run-quiet/fixtures/ignored-file/mise.toml b/test/lint/mise-run-quiet/fixtures/ignored-file/mise.toml new file mode 100644 index 0000000..f8a0eb4 --- /dev/null +++ b/test/lint/mise-run-quiet/fixtures/ignored-file/mise.toml @@ -0,0 +1,3 @@ +codebase:ignore mise-run-quiet +[settings] +quiet = true diff --git a/test/lint/mise-run-quiet/fixtures/ignored-inline/.mise/tasks/t b/test/lint/mise-run-quiet/fixtures/ignored-inline/.mise/tasks/t new file mode 100644 index 0000000..bd59f9b --- /dev/null +++ b/test/lint/mise-run-quiet/fixtures/ignored-inline/.mise/tasks/t @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Inline ignore with reason — accepted +result=$(mise run build) # codebase:ignore mise-run-quiet — intentional, header is part of UX + +# No ignore — should be flagged +eval "$(mise run env)" diff --git a/test/lint/mise-run-quiet/fixtures/ignored-inline/mise.toml b/test/lint/mise-run-quiet/fixtures/ignored-inline/mise.toml new file mode 100644 index 0000000..8a0a5a4 --- /dev/null +++ b/test/lint/mise-run-quiet/fixtures/ignored-inline/mise.toml @@ -0,0 +1,2 @@ +[settings] +quiet = true diff --git a/test/lint/mise-run-quiet/fixtures/no-shell-files/mise.toml b/test/lint/mise-run-quiet/fixtures/no-shell-files/mise.toml new file mode 100644 index 0000000..8a0a5a4 --- /dev/null +++ b/test/lint/mise-run-quiet/fixtures/no-shell-files/mise.toml @@ -0,0 +1,2 @@ +[settings] +quiet = true diff --git a/test/lint/mise-run-quiet/mise-run-quiet.bats b/test/lint/mise-run-quiet/mise-run-quiet.bats new file mode 100644 index 0000000..7919f1f --- /dev/null +++ b/test/lint/mise-run-quiet/mise-run-quiet.bats @@ -0,0 +1,143 @@ +#!/usr/bin/env bats +# Tests for lint:mise-run-quiet rule + +load ../../test_helper + +setup() { + FIXTURES="$BATS_TEST_DIRNAME/fixtures" +} + +# Detection + +@test "mise-run-quiet: passes on a clean codebase (no mise run calls)" { + run codebase lint:mise-run-quiet "$FIXTURES/clean" + [ "$status" -eq 0 ] + [[ "$output" == *"OK"* ]] +} + +@test "mise-run-quiet: passes when -q is already used" { + run codebase lint:mise-run-quiet "$FIXTURES/clean-already-quiet" + [ "$status" -eq 0 ] + [[ "$output" == *"OK"* ]] +} + +@test "mise-run-quiet: flags mise run in command substitution" { + run codebase lint:mise-run-quiet "$FIXTURES/dirty-command-sub" + [ "$status" -ne 0 ] + [[ "$output" == *"FAIL"*"dirty-command-sub"* ]] + [[ "$output" == *"[command-sub]"* ]] + [[ "$output" == *'$(mise run build)'* ]] +} + +@test "mise-run-quiet: flags mise run in eval" { + run codebase lint:mise-run-quiet "$FIXTURES/dirty-eval" + [ "$status" -ne 0 ] + [[ "$output" == *"FAIL"*"dirty-eval"* ]] + [[ "$output" == *"[eval]"* ]] + [[ "$output" == *'eval "$(mise run env)"'* ]] +} + +@test "mise-run-quiet: warns on mise run in pipeline" { + run codebase lint:mise-run-quiet "$FIXTURES/dirty-pipe" + [ "$status" -ne 0 ] + [[ "$output" == *"FAIL"*"dirty-pipe"* ]] + [[ "$output" == *"[pipeline]"* ]] + [[ "$output" == *'mise run check | grep error'* ]] +} + +@test "mise-run-quiet: flags multiple violations in one file" { + run codebase lint:mise-run-quiet "$FIXTURES/dirty-mixed" + [ "$status" -ne 0 ] + [[ "$output" == *"FAIL"*"dirty-mixed"* ]] + [[ "$output" == *"[command-sub]"* ]] + [[ "$output" == *"[eval]"* ]] + [[ "$output" == *"[pipeline]"* ]] +} + +@test "mise-run-quiet: fail output includes the violating file path" { + run codebase lint:mise-run-quiet "$FIXTURES/dirty-command-sub" + [ "$status" -ne 0 ] + [[ "$output" == *".mise/tasks/t"* ]] +} + +@test "mise-run-quiet: fail output suggests adding -q" { + run codebase lint:mise-run-quiet "$FIXTURES/dirty-command-sub" + [ "$status" -ne 0 ] + [[ "$output" == *"mise run -q"* ]] +} + +# Safe contexts + +@test "mise-run-quiet: does not flag bare mise run (interactive)" { + run codebase lint:mise-run-quiet "$FIXTURES/dirty-mixed" + [ "$status" -ne 0 ] + # Should flag the violations but NOT the bare 'mise run status' line + [[ "$output" == *"FAIL"* ]] + # The bare line should not appear in the output + local bare_count + bare_count=$(echo "$output" | grep -c "mise run status" || true) + # It may appear in the hint text, but should not be flagged as a violation + [[ "$output" != *"[command-sub]".*"mise run status"* ]] + [[ "$output" != *"[eval]".*"mise run status"* ]] + [[ "$output" != *"[pipeline]".*"mise run status"* ]] +} + +@test "mise-run-quiet: does not flag mise run with stderr redirect" { + run codebase lint:mise-run-quiet "$FIXTURES/dirty-mixed" + [ "$status" -ne 0 ] + # The stderr redirect line should not appear as a violation + [[ "$output" != *"mise run lint 2>/dev/null"* ]] +} + +@test "mise-run-quiet: does not flag mise run -q in command substitution" { + run codebase lint:mise-run-quiet "$FIXTURES/clean-already-quiet" + [ "$status" -eq 0 ] + [[ "$output" == *"OK"* ]] +} + +# Ignore mechanisms + +@test "mise-run-quiet: respects file-level codebase:ignore" { + run codebase lint:mise-run-quiet "$FIXTURES/ignored-file" + [ "$status" -eq 0 ] + [[ "$output" == *"SKIP"*"ignored-file"* ]] +} + +@test "mise-run-quiet: respects inline codebase:ignore with reason" { + run codebase lint:mise-run-quiet "$FIXTURES/ignored-inline" + [ "$status" -ne 0 ] + # Should flag the eval line (no ignore) but not the command sub line (has ignore) + [[ "$output" == *"[eval]"* ]] + # The ignored line should not appear as a violation + [[ "$output" != *"codebase:ignore mise-run-quiet"* ]] +} + +# Edge cases + +@test "mise-run-quiet: skips directories with no shell files" { + run codebase lint:mise-run-quiet "$FIXTURES/no-shell-files" + [ "$status" -eq 0 ] + [[ "$output" == *"OK"*"no shell files"* ]] +} + +@test "mise-run-quiet: handles multiple targets" { + run codebase lint:mise-run-quiet "$FIXTURES/clean" "$FIXTURES/dirty-command-sub" + [ "$status" -ne 0 ] + [[ "$output" == *"OK"*"clean"* ]] + [[ "$output" == *"FAIL"*"dirty-command-sub"* ]] +} + +@test "mise-run-quiet: does not flag 'mise run' in comments" { + run codebase lint:mise-run-quiet "$FIXTURES/clean" + [ "$status" -eq 0 ] + # The clean fixture has no mise run calls at all + [[ "$output" == *"OK"* ]] +} + +@test "mise-run-quiet: does not flag mise run with flags (not just bare)" { + run codebase lint:mise-run-quiet "$FIXTURES/dirty-command-sub" + [ "$status" -ne 0 ] + # Should flag both $(mise run build) and $(mise run count --all) + [[ "$output" == *"mise run build"* ]] + [[ "$output" == *"mise run count"* ]] +} 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/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"