@toolu/opencode 6.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/generated/GENERATED-NOTES.md +16 -0
- package/generated/agents/toolu--architect.md +52 -0
- package/generated/agents/toolu--deep-explore.md +65 -0
- package/generated/agents/toolu--implementer.md +50 -0
- package/generated/agents/toolu--quick-task.md +54 -0
- package/generated/agents/toolu--research-agent.md +101 -0
- package/generated/commands/toolu--commit.md +8 -0
- package/generated/commands/toolu--review-and-commit.md +8 -0
- package/generated/opencode.toolu.json +116 -0
- package/generated/skills/toolu--brainstorm/SKILL.md +57 -0
- package/generated/skills/toolu--brainstorm/references/design-questions.md +36 -0
- package/generated/skills/toolu--commit--skill/SKILL.md +11 -0
- package/generated/skills/toolu--debug/SKILL.md +54 -0
- package/generated/skills/toolu--deep-research/SKILL.md +51 -0
- package/generated/skills/toolu--execution/SKILL.md +77 -0
- package/generated/skills/toolu--orchestrator/SKILL.md +145 -0
- package/generated/skills/toolu--orchestrator/references/delegation-cost.md +104 -0
- package/generated/skills/toolu--orchestrator/references/model-routing.md +112 -0
- package/generated/skills/toolu--plan/SKILL.md +56 -0
- package/generated/skills/toolu--plan/references/ledger.md +18 -0
- package/generated/skills/toolu--plan-review/SKILL.md +48 -0
- package/generated/skills/toolu--review-and-commit--skill/SKILL.md +10 -0
- package/generated/skills/toolu--setup/SKILL.md +24 -0
- package/generated/skills/toolu--setup/scripts/setup.sh +206 -0
- package/generated/skills/toolu--spec/SKILL.md +67 -0
- package/generated/skills/toolu--spec-review/SKILL.md +50 -0
- package/generated/skills/toolu--test/SKILL.md +49 -0
- package/package.json +51 -0
- package/plugins/agent-browser/.claude-plugin/plugin.json +18 -0
- package/plugins/agent-browser/.codex-plugin/plugin.json +7 -0
- package/plugins/agent-browser/README.md +18 -0
- package/plugins/agent-browser/hooks/hooks.json +15 -0
- package/plugins/agent-browser/hooks/session-start.sh +42 -0
- package/plugins/agent-browser/skills/agent-browser/SKILL.md +77 -0
- package/plugins/agent-browser/skills/agent-browser/scripts/agent-browser.sh +80 -0
- package/plugins/ast-grep/.claude-plugin/plugin.json +18 -0
- package/plugins/ast-grep/.codex-plugin/plugin.json +7 -0
- package/plugins/ast-grep/README.md +21 -0
- package/plugins/ast-grep/hooks/hooks.json +15 -0
- package/plugins/ast-grep/hooks/post-tools.d/byte-savings.sh +67 -0
- package/plugins/ast-grep/hooks/pre-tools.d/search-nudge.sh +170 -0
- package/plugins/ast-grep/hooks/register.sh +68 -0
- package/plugins/ast-grep/scripts/byte-savings-report.sh +28 -0
- package/plugins/ast-grep/skills/ast-grep/SKILL.md +127 -0
- package/plugins/ast-grep/skills/ast-grep/references/ast-grep-advanced.md +44 -0
- package/plugins/ast-grep/skills/ast-grep/scripts/ast-grep.sh +103 -0
- package/plugins/context7/.claude-plugin/plugin.json +18 -0
- package/plugins/context7/.codex-plugin/plugin.json +7 -0
- package/plugins/context7/README.md +19 -0
- package/plugins/context7/hooks/hooks.json +15 -0
- package/plugins/context7/hooks/session-start.sh +42 -0
- package/plugins/context7/skills/context7/SKILL.md +64 -0
- package/plugins/context7/skills/context7/scripts/search.sh +151 -0
- package/plugins/exa-search/.claude-plugin/plugin.json +18 -0
- package/plugins/exa-search/.codex-plugin/plugin.json +7 -0
- package/plugins/exa-search/README.md +19 -0
- package/plugins/exa-search/hooks/hooks.json +15 -0
- package/plugins/exa-search/hooks/session-start.sh +34 -0
- package/plugins/exa-search/skills/exa-search/SKILL.md +91 -0
- package/plugins/exa-search/skills/exa-search/scripts/search.sh +221 -0
- package/plugins/jev/.claude-plugin/plugin.json +19 -0
- package/plugins/jev/.codex-plugin/plugin.json +7 -0
- package/plugins/jev/README.md +61 -0
- package/plugins/jev/hooks/hooks.json +25 -0
- package/plugins/jev/hooks/lib/common.sh +49 -0
- package/plugins/jev/hooks/session-start.sh +45 -0
- package/plugins/jev/hooks/user-prompt-submit.sh +41 -0
- package/plugins/jev/skills/jev/SKILL.md +81 -0
- package/plugins/jev/skills/jev/evals/README.md +98 -0
- package/plugins/jev/skills/jev/references/problem-solving.md +198 -0
- package/plugins/jev/skills/jev/scripts/jev.sh +405 -0
- package/plugins/jira/.claude-plugin/plugin.json +18 -0
- package/plugins/jira/.codex-plugin/plugin.json +7 -0
- package/plugins/jira/README.md +42 -0
- package/plugins/jira/hooks/hooks.json +15 -0
- package/plugins/jira/hooks/session-start.sh +34 -0
- package/plugins/jira/skills/jira/SKILL.md +123 -0
- package/plugins/jira/skills/jira/scripts/jira.sh +70 -0
- package/plugins/jira/skills/jira/scripts/lib/adf.sh +17 -0
- package/plugins/jira/skills/jira/scripts/lib/attachment.sh +94 -0
- package/plugins/jira/skills/jira/scripts/lib/board.sh +58 -0
- package/plugins/jira/skills/jira/scripts/lib/http.sh +184 -0
- package/plugins/jira/skills/jira/scripts/lib/issue.sh +159 -0
- package/plugins/jira/skills/jira/scripts/lib/paginate.sh +45 -0
- package/plugins/jira/skills/jira/scripts/lib/plan-parse.sh +70 -0
- package/plugins/jira/skills/jira/scripts/lib/plan-run.sh +115 -0
- package/plugins/jira/skills/jira/scripts/lib/plan-store.sh +168 -0
- package/plugins/jira/skills/jira/scripts/lib/plan.sh +108 -0
- package/plugins/jira/skills/jira/scripts/lib/project.sh +42 -0
- package/plugins/jira/skills/jira/scripts/lib/raw.sh +15 -0
- package/plugins/jira/skills/jira/scripts/lib/search.sh +41 -0
- package/plugins/jira/skills/jira/scripts/lib/sprint.sh +88 -0
- package/plugins/jira/skills/jira/scripts/lib/user.sh +45 -0
- package/plugins/jira/skills/jira/scripts/lib/worklog.sh +56 -0
- package/plugins/pr-babysit/.claude-plugin/plugin.json +25 -0
- package/plugins/pr-babysit/.codex-plugin/plugin.json +7 -0
- package/plugins/pr-babysit/README.md +38 -0
- package/plugins/pr-babysit/commands/babysit.md +13 -0
- package/plugins/pr-babysit/hooks/check-toolu.sh +20 -0
- package/plugins/pr-babysit/hooks/hooks.json +15 -0
- package/plugins/pr-babysit/scripts/babysit-tick.sh +133 -0
- package/plugins/pr-babysit/scripts/collect-pr.sh +224 -0
- package/plugins/pr-babysit/scripts/lib/common.sh +146 -0
- package/plugins/pr-babysit/scripts/lib/gh.sh +150 -0
- package/plugins/pr-babysit/scripts/lib/lock.sh +84 -0
- package/plugins/pr-babysit/scripts/lib/normalize.sh +98 -0
- package/plugins/pr-babysit/scripts/lib/state.sh +37 -0
- package/plugins/pr-babysit/scripts/parse-verdict.sh +171 -0
- package/plugins/pr-babysit/scripts/record.sh +69 -0
- package/plugins/pr-babysit/scripts/reduce-state.sh +245 -0
- package/plugins/pr-babysit/scripts/reply-thread.sh +90 -0
- package/plugins/pr-babysit/scripts/resolve-thread.sh +71 -0
- package/plugins/pr-babysit/skills/babysit/SKILL.md +31 -0
- package/plugins/pr-babysit/skills/babysit/references/helper.md +209 -0
- package/plugins/pr-babysit/workflows/babysit.md +722 -0
- package/plugins/python-quality/.claude-plugin/plugin.json +24 -0
- package/plugins/python-quality/.codex-plugin/plugin.json +6 -0
- package/plugins/python-quality/README.md +23 -0
- package/plugins/python-quality/hooks/check-toolu.sh +20 -0
- package/plugins/python-quality/hooks/concerns/00-preamble.sh +85 -0
- package/plugins/python-quality/hooks/concerns/10-size-file.sh +6 -0
- package/plugins/python-quality/hooks/concerns/20-tests.sh +37 -0
- package/plugins/python-quality/hooks/concerns/30-suppression.sh +33 -0
- package/plugins/python-quality/hooks/concerns/50-size-fn.sh +49 -0
- package/plugins/python-quality/hooks/concerns/70-no-mocks.sh +76 -0
- package/plugins/python-quality/hooks/concerns/90-docs.sh +52 -0
- package/plugins/python-quality/hooks/concerns/99-finalize.sh +36 -0
- package/plugins/python-quality/hooks/hooks.json +19 -0
- package/plugins/python-quality/hooks/register.sh +65 -0
- package/plugins/rust-quality/.claude-plugin/plugin.json +24 -0
- package/plugins/rust-quality/.codex-plugin/plugin.json +6 -0
- package/plugins/rust-quality/README.md +24 -0
- package/plugins/rust-quality/hooks/check-toolu.sh +20 -0
- package/plugins/rust-quality/hooks/concerns/00-preamble.sh +93 -0
- package/plugins/rust-quality/hooks/concerns/10-size-file.sh +10 -0
- package/plugins/rust-quality/hooks/concerns/20-tests.sh +84 -0
- package/plugins/rust-quality/hooks/concerns/30-suppression.sh +26 -0
- package/plugins/rust-quality/hooks/concerns/40-unsafe.sh +39 -0
- package/plugins/rust-quality/hooks/concerns/50-size-fn.sh +38 -0
- package/plugins/rust-quality/hooks/concerns/55-size-impl.sh +28 -0
- package/plugins/rust-quality/hooks/concerns/60-error-handling.sh +149 -0
- package/plugins/rust-quality/hooks/concerns/70-no-mocks.sh +139 -0
- package/plugins/rust-quality/hooks/concerns/90-docs.sh +32 -0
- package/plugins/rust-quality/hooks/concerns/99-finalize.sh +36 -0
- package/plugins/rust-quality/hooks/hooks.json +19 -0
- package/plugins/rust-quality/hooks/register.sh +65 -0
- package/plugins/statusline/.claude-plugin/plugin.json +19 -0
- package/plugins/statusline/.codex-plugin/plugin.json +7 -0
- package/plugins/statusline/README.md +108 -0
- package/plugins/statusline/commands/setup.md +30 -0
- package/plugins/statusline/hooks/hooks.json +15 -0
- package/plugins/statusline/hooks/session-start.sh +45 -0
- package/plugins/statusline/scripts/collect-status.sh +147 -0
- package/plugins/statusline/scripts/setup.sh +93 -0
- package/plugins/statusline/scripts/status.sh +55 -0
- package/plugins/statusline/skills/status/SKILL.md +13 -0
- package/plugins/statusline/statusline.sh +177 -0
- package/plugins/toolu/.claude-plugin/plugin.json +19 -0
- package/plugins/toolu/.codex-plugin/plugin.json +7 -0
- package/plugins/toolu/README.md +35 -0
- package/plugins/toolu/agents/.gitkeep +0 -0
- package/plugins/toolu/agents/architect.md +52 -0
- package/plugins/toolu/agents/deep-explore.md +65 -0
- package/plugins/toolu/agents/implementer.md +50 -0
- package/plugins/toolu/agents/quick-task.md +54 -0
- package/plugins/toolu/agents/research-agent.md +107 -0
- package/plugins/toolu/assets/agents/architect.toml +9 -0
- package/plugins/toolu/assets/agents/deep-explore.toml +9 -0
- package/plugins/toolu/assets/agents/implementer.toml +9 -0
- package/plugins/toolu/assets/agents/quick-task.toml +9 -0
- package/plugins/toolu/assets/agents/research-agent.toml +9 -0
- package/plugins/toolu/commands/.gitkeep +0 -0
- package/plugins/toolu/commands/commit.md +5 -0
- package/plugins/toolu/commands/review-and-commit.md +5 -0
- package/plugins/toolu/hooks/docs/gates.md +157 -0
- package/plugins/toolu/hooks/docs/model-routing.md +14 -0
- package/plugins/toolu/hooks/docs/post-compaction.md +3 -0
- package/plugins/toolu/hooks/docs/push-review.md +123 -0
- package/plugins/toolu/hooks/docs/session-start-python.md +3 -0
- package/plugins/toolu/hooks/docs/session-start-rust.md +4 -0
- package/plugins/toolu/hooks/docs/session-start-ts.md +3 -0
- package/plugins/toolu/hooks/docs/session-start.md +16 -0
- package/plugins/toolu/hooks/hooks.json +76 -0
- package/plugins/toolu/hooks/lib/config.sh +355 -0
- package/plugins/toolu/hooks/lib/detect.sh +929 -0
- package/plugins/toolu/hooks/lib/diff-sha.sh +32 -0
- package/plugins/toolu/hooks/lib/dispatch.sh +365 -0
- package/plugins/toolu/hooks/lib/docs-sync-config.sh +81 -0
- package/plugins/toolu/hooks/lib/edit-records.sh +157 -0
- package/plugins/toolu/hooks/lib/gate-file.sh +201 -0
- package/plugins/toolu/hooks/lib/gate-mode.sh +244 -0
- package/plugins/toolu/hooks/lib/host.sh +205 -0
- package/plugins/toolu/hooks/lib/permissions.sh +125 -0
- package/plugins/toolu/hooks/lib/plan-ledger-parse.sh +219 -0
- package/plugins/toolu/hooks/lib/plan-ledger-preflight.sh +137 -0
- package/plugins/toolu/hooks/lib/plan-ledger.sh +749 -0
- package/plugins/toolu/hooks/lib/push-waiver.sh +116 -0
- package/plugins/toolu/hooks/lib/quality-config.sh +184 -0
- package/plugins/toolu/hooks/lib/registry.sh +72 -0
- package/plugins/toolu/hooks/lib/state-sweeper.sh +194 -0
- package/plugins/toolu/hooks/lib/telemetry.sh +94 -0
- package/plugins/toolu/hooks/lib/verdict.sh +477 -0
- package/plugins/toolu/hooks/post-tools/mod.sh +42 -0
- package/plugins/toolu/hooks/post-tools/modules/gate-status.sh +140 -0
- package/plugins/toolu/hooks/post-tools/modules/push-waiver.sh +97 -0
- package/plugins/toolu/hooks/pre-compact.sh +18 -0
- package/plugins/toolu/hooks/pre-tools/agent-tier.sh +111 -0
- package/plugins/toolu/hooks/pre-tools/mod.sh +37 -0
- package/plugins/toolu/hooks/pre-tools/modules/bash-commands.sh +187 -0
- package/plugins/toolu/hooks/pre-tools/modules/code-edit-rules.sh +78 -0
- package/plugins/toolu/hooks/pre-tools/modules/commit-gate.sh +70 -0
- package/plugins/toolu/hooks/pre-tools/modules/docs-sync.sh +137 -0
- package/plugins/toolu/hooks/pre-tools/modules/mcp-blocker.sh +151 -0
- package/plugins/toolu/hooks/pre-tools/modules/plan-ledger.sh +201 -0
- package/plugins/toolu/hooks/pre-tools/modules/protected-files.sh +177 -0
- package/plugins/toolu/hooks/pre-tools/modules/push-review.sh +273 -0
- package/plugins/toolu/hooks/pre-tools/modules/quality-gate.sh +87 -0
- package/plugins/toolu/hooks/session-start.sh +379 -0
- package/plugins/toolu/hooks/user-prompt-submit.sh +197 -0
- package/plugins/toolu/scripts/context-budget.sh +111 -0
- package/plugins/toolu/scripts/debug-log.sh +98 -0
- package/plugins/toolu/scripts/debug-stack.sh +108 -0
- package/plugins/toolu/scripts/debug-testfail.sh +97 -0
- package/plugins/toolu/settings/.gitkeep +0 -0
- package/plugins/toolu/settings/README.md +127 -0
- package/plugins/toolu/settings/bash-allowlist.txt +12 -0
- package/plugins/toolu/settings/bash-denylist.txt +25 -0
- package/plugins/toolu/settings/code-edit-rules.json +67 -0
- package/plugins/toolu/settings/commit-prefixes.txt +12 -0
- package/plugins/toolu/settings/mcp-blocklist.txt +8 -0
- package/plugins/toolu/settings/permissions.fragment.json +36 -0
- package/plugins/toolu/settings/protected-files.txt +30 -0
- package/plugins/toolu/settings/rust-unsafe-exemptions.txt +5 -0
- package/plugins/toolu/settings/toolu.config.example.json +130 -0
- package/plugins/toolu/skills/.gitkeep +0 -0
- package/plugins/toolu/skills/brainstorm/SKILL.md +57 -0
- package/plugins/toolu/skills/brainstorm/references/design-questions.md +36 -0
- package/plugins/toolu/skills/commit/SKILL.md +11 -0
- package/plugins/toolu/skills/debug/SKILL.md +54 -0
- package/plugins/toolu/skills/debug/evals/README.md +17 -0
- package/plugins/toolu/skills/debug/evals/cases.json +24 -0
- package/plugins/toolu/skills/debug/evals/results.json +34 -0
- package/plugins/toolu/skills/deep-research/SKILL.md +51 -0
- package/plugins/toolu/skills/execution/SKILL.md +77 -0
- package/plugins/toolu/skills/orchestrator/SKILL.md +145 -0
- package/plugins/toolu/skills/orchestrator/references/delegation-cost.md +104 -0
- package/plugins/toolu/skills/orchestrator/references/model-routing.md +112 -0
- package/plugins/toolu/skills/plan/SKILL.md +56 -0
- package/plugins/toolu/skills/plan/references/ledger.md +18 -0
- package/plugins/toolu/skills/plan-review/SKILL.md +48 -0
- package/plugins/toolu/skills/review-and-commit/SKILL.md +10 -0
- package/plugins/toolu/skills/setup/SKILL.md +24 -0
- package/plugins/toolu/skills/setup/scripts/setup.sh +206 -0
- package/plugins/toolu/skills/spec/SKILL.md +67 -0
- package/plugins/toolu/skills/spec-review/SKILL.md +50 -0
- package/plugins/toolu/skills/test/SKILL.md +49 -0
- package/plugins/toolu/workflows/commit.md +21 -0
- package/plugins/toolu/workflows/host-mapping.md +22 -0
- package/plugins/toolu/workflows/review-and-commit.md +54 -0
- package/plugins/toolu/workflows/semantic-judgments.md +35 -0
- package/plugins/toolu-review/.claude-plugin/plugin.json +18 -0
- package/plugins/toolu-review/.codex-plugin/plugin.json +7 -0
- package/plugins/toolu-review/README.md +17 -0
- package/plugins/toolu-review/hooks/hooks.json +15 -0
- package/plugins/toolu-review/hooks/session-start.sh +34 -0
- package/plugins/toolu-review/skills/review/SKILL.md +90 -0
- package/plugins/toolu-review/skills/review/scripts/write-state.sh +189 -0
- package/plugins/ts-quality/.claude-plugin/plugin.json +24 -0
- package/plugins/ts-quality/.codex-plugin/plugin.json +6 -0
- package/plugins/ts-quality/README.md +24 -0
- package/plugins/ts-quality/hooks/check-toolu.sh +20 -0
- package/plugins/ts-quality/hooks/concerns/00-preamble.sh +123 -0
- package/plugins/ts-quality/hooks/concerns/10-imports.sh +33 -0
- package/plugins/ts-quality/hooks/concerns/15-type-as.sh +9 -0
- package/plugins/ts-quality/hooks/concerns/20-tests.sh +26 -0
- package/plugins/ts-quality/hooks/concerns/25-size-file.sh +40 -0
- package/plugins/ts-quality/hooks/concerns/30-size-fn.sh +53 -0
- package/plugins/ts-quality/hooks/concerns/35-react-hooks.sh +7 -0
- package/plugins/ts-quality/hooks/concerns/40-factory.sh +5 -0
- package/plugins/ts-quality/hooks/concerns/45-typeguard.sh +6 -0
- package/plugins/ts-quality/hooks/concerns/50-type-dup.sh +16 -0
- package/plugins/ts-quality/hooks/concerns/55-naming.sh +11 -0
- package/plugins/ts-quality/hooks/concerns/60-console.sh +6 -0
- package/plugins/ts-quality/hooks/concerns/65-suppression.sh +12 -0
- package/plugins/ts-quality/hooks/concerns/70-ui-confirm.sh +8 -0
- package/plugins/ts-quality/hooks/concerns/72-ui-radix.sh +12 -0
- package/plugins/ts-quality/hooks/concerns/74-react-props.sh +6 -0
- package/plugins/ts-quality/hooks/concerns/76-toast.sh +10 -0
- package/plugins/ts-quality/hooks/concerns/78-error-ast.sh +242 -0
- package/plugins/ts-quality/hooks/concerns/80-throw-literal.sh +23 -0
- package/plugins/ts-quality/hooks/concerns/85-no-mocks.sh +109 -0
- package/plugins/ts-quality/hooks/concerns/90-duplication.sh +30 -0
- package/plugins/ts-quality/hooks/concerns/92-docs.sh +36 -0
- package/plugins/ts-quality/hooks/concerns/94-handler.sh +15 -0
- package/plugins/ts-quality/hooks/concerns/99-finalize.sh +46 -0
- package/plugins/ts-quality/hooks/hooks.json +19 -0
- package/plugins/ts-quality/hooks/register.sh +65 -0
- package/src/adapter/__tests__/evaluate.test.ts +82 -0
- package/src/adapter/__tests__/permission-map.test.ts +91 -0
- package/src/adapter/evaluate.ts +54 -0
- package/src/adapter/permission-map.ts +177 -0
- package/src/bootstrap/__tests__/bootstrap.test.ts +148 -0
- package/src/bootstrap/entrypoint.ts +18 -0
- package/src/bootstrap/index.ts +5 -0
- package/src/bootstrap/readiness.ts +59 -0
- package/src/bootstrap/result.ts +21 -0
- package/src/bootstrap/runtime.ts +99 -0
- package/src/bootstrap/test-helpers.ts +25 -0
- package/src/host/__tests__/host.test.ts +37 -0
- package/src/host/detect.ts +46 -0
- package/src/host/index.ts +10 -0
- package/src/host/roots.ts +59 -0
- package/src/host/types.ts +14 -0
- package/src/inventory/__tests__/inventory.test.ts +69 -0
- package/src/inventory/index.ts +9 -0
- package/src/inventory/manifest.ts +46 -0
- package/src/inventory/scan.ts +66 -0
- package/src/inventory/selection.ts +92 -0
- package/src/inventory/types.ts +22 -0
- package/src/lifecycle/__tests__/lifecycle.test.ts +7 -0
- package/src/lifecycle/index.ts +2 -0
- package/src/lifecycle/table.ts +42 -0
- package/src/plugin/__tests__/plugin-stub.test.ts +6 -0
- package/src/plugin/plugin-stub.ts +7 -0
- package/src/plugin/toolu.ts +142 -0
- package/src/preflight/__tests__/preflight.test.ts +20 -0
- package/src/preflight/check.ts +75 -0
- package/src/preflight/index.ts +2 -0
- package/src/select/__tests__/select.test.ts +52 -0
- package/src/select/index.ts +2 -0
- package/src/select/resolve.ts +87 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Post-tool check: Rust quality rules.
|
|
3
|
+
# Project-agnostic: no-op outside Rust projects or when cargo is missing.
|
|
4
|
+
# Unsafe-block exemptions for FFI crates come from
|
|
5
|
+
# $TOOLU_SETTINGS_DIR/rust-unsafe-exemptions.txt.
|
|
6
|
+
#
|
|
7
|
+
# Inputs (from parent dispatcher post-tools/mod.sh, via `export`):
|
|
8
|
+
# $tool_name - name of the tool being invoked
|
|
9
|
+
# $input - raw JSON payload on stdin
|
|
10
|
+
# $PROJECT_ROOT - repository root
|
|
11
|
+
|
|
12
|
+
: "${tool_name:=}"
|
|
13
|
+
: "${input:=}"
|
|
14
|
+
: "${PROJECT_ROOT:=$(pwd)}"
|
|
15
|
+
|
|
16
|
+
# Core lib comes from the toolu dispatcher via TOOLU_LIB_DIR (set by
|
|
17
|
+
# plugins/toolu/hooks/post-tools/mod.sh before registry dispatch). Outside
|
|
18
|
+
# that pipeline there is no relative path to it — fail SOFT: a quality check
|
|
19
|
+
# must never break a tool call by erroring.
|
|
20
|
+
[ -n "${TOOLU_LIB_DIR:-}" ] && [ -f "$TOOLU_LIB_DIR/detect.sh" ] || exit 0
|
|
21
|
+
# shellcheck source=../../../toolu/hooks/lib/detect.sh
|
|
22
|
+
. "$TOOLU_LIB_DIR/detect.sh"
|
|
23
|
+
# Threshold resolver (defaults + project/native overrides). Soft if absent.
|
|
24
|
+
# shellcheck source=../../../toolu/hooks/lib/quality-config.sh
|
|
25
|
+
[ -f "$TOOLU_LIB_DIR/quality-config.sh" ] && . "$TOOLU_LIB_DIR/quality-config.sh"
|
|
26
|
+
# Multi-slot gate writer (entries keyed by file — one hook's failure no longer
|
|
27
|
+
# clobbers another's). Soft if absent: fallbacks below keep the legacy
|
|
28
|
+
# single-slot behavior when the toolu lib predates gate-file.sh.
|
|
29
|
+
# shellcheck source=../../../toolu/hooks/lib/gate-file.sh
|
|
30
|
+
[ -f "$TOOLU_LIB_DIR/gate-file.sh" ] && . "$TOOLU_LIB_DIR/gate-file.sh"
|
|
31
|
+
command -v gate_record_failure >/dev/null 2>&1 || gate_record_failure() {
|
|
32
|
+
jq -n --arg reason "$4" --arg source "$3" --arg file "$2" --arg violations "$5" \
|
|
33
|
+
--arg updatedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
34
|
+
'{status: "failing", reason: $reason, source: $source, file: $file,
|
|
35
|
+
violations: $violations, updatedAt: $updatedAt}' > "$1"
|
|
36
|
+
}
|
|
37
|
+
command -v gate_clear_file >/dev/null 2>&1 || gate_clear_file() {
|
|
38
|
+
[ -f "$1" ] || return 0
|
|
39
|
+
local _src _file
|
|
40
|
+
_src=$(jq -r '.source // ""' "$1" 2>/dev/null || echo "")
|
|
41
|
+
_file=$(jq -r '.file // ""' "$1" 2>/dev/null || echo "")
|
|
42
|
+
if [ "$_src" = "$3" ] && [ "$_file" = "$2" ]; then
|
|
43
|
+
jq -n --arg source "$3" --arg updatedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
44
|
+
'{status: "passing", source: $source, updatedAt: $updatedAt}' > "$1"
|
|
45
|
+
fi
|
|
46
|
+
}
|
|
47
|
+
command -v rust_max_file_lines >/dev/null 2>&1 || rust_max_file_lines() { echo "${DEFAULT_RUST_MAX_FILE_LINES:-500}"; }
|
|
48
|
+
command -v rust_max_fn_lines >/dev/null 2>&1 || rust_max_fn_lines() { echo "${DEFAULT_RUST_MAX_FN_LINES:-50}"; }
|
|
49
|
+
command -v rust_max_impl_lines >/dev/null 2>&1 || rust_max_impl_lines() { echo "${DEFAULT_RUST_MAX_IMPL_LINES:-200}"; }
|
|
50
|
+
# Boolean flag reader (e.g. noMocks) — falls back to the caller's default when
|
|
51
|
+
# quality-config.sh predates this reader.
|
|
52
|
+
command -v quality_flag >/dev/null 2>&1 || quality_flag() { printf '%s' "$3"; }
|
|
53
|
+
# count_code_lines comes from detect.sh (sourced above) — no fallback needed.
|
|
54
|
+
|
|
55
|
+
# Load the merged config ONCE so TOOLU_CFG_LOADED sticks for the threshold
|
|
56
|
+
# lookups below — each runs in a $(...) subshell that inherits it and skips
|
|
57
|
+
# re-merging (otherwise every wrapper re-spawns the jq merge).
|
|
58
|
+
command -v toolu_load_config >/dev/null 2>&1 && toolu_load_config 2>/dev/null || true
|
|
59
|
+
|
|
60
|
+
[ "$(detect_rust)" = "rust" ] || exit 0
|
|
61
|
+
command -v cargo >/dev/null 2>&1 || exit 0
|
|
62
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
63
|
+
|
|
64
|
+
TOOLU_SETTINGS_DIR=$(toolu_settings_dir)
|
|
65
|
+
EXEMPTIONS_FILE="$TOOLU_SETTINGS_DIR/rust-unsafe-exemptions.txt"
|
|
66
|
+
|
|
67
|
+
# read_list is sourced from lib/detect.sh.
|
|
68
|
+
|
|
69
|
+
fp_from_input=""
|
|
70
|
+
if [[ "$tool_name" == "Write" || "$tool_name" == "Edit" || "$tool_name" == "MultiEdit" ]]; then
|
|
71
|
+
fp_from_input=$(echo "$input" | jq -r '.tool_input.path // .tool_input.file_path // .tool_input.target_file // empty' 2>/dev/null || echo "")
|
|
72
|
+
fi
|
|
73
|
+
FILE_PATH="${CLAUDE_FILE_PATHS:-$fp_from_input}"
|
|
74
|
+
|
|
75
|
+
# Deleted files and move sources cannot be linted after apply_patch completes,
|
|
76
|
+
# but their path-owned quality failure must not remain active forever.
|
|
77
|
+
EDIT_OPERATION="${TOOLU_EDIT_OPERATION:-$(echo "$input" | jq -r '.tool_input.toolu_edit_operation // ""' 2>/dev/null || echo "")}"
|
|
78
|
+
EDIT_MOVED_TO="${TOOLU_EDIT_MOVED_TO:-$(echo "$input" | jq -r '.tool_input.toolu_edit_moved_to // ""' 2>/dev/null || echo "")}"
|
|
79
|
+
if [[ "$EDIT_OPERATION" == delete || -n "$EDIT_MOVED_TO" ]]; then
|
|
80
|
+
if [[ -n "$FILE_PATH" && "$FILE_PATH" =~ \.rs$ ]]; then
|
|
81
|
+
GATE_FILE="$(toolu_project_state_root "$PROJECT_ROOT")/quality-gate-status.json"
|
|
82
|
+
gate_clear_file "$GATE_FILE" "$FILE_PATH" "rust-quality-hook"
|
|
83
|
+
fi
|
|
84
|
+
exit 0
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
[[ -z "$FILE_PATH" || ! -f "$FILE_PATH" ]] && exit 0
|
|
88
|
+
[[ ! "$FILE_PATH" =~ \.rs$ ]] && exit 0
|
|
89
|
+
|
|
90
|
+
MESSAGES=""
|
|
91
|
+
add_error() {
|
|
92
|
+
MESSAGES="${MESSAGES}${1}"$'\n'
|
|
93
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
RUST_MAX_FILE=$(rust_max_file_lines)
|
|
2
|
+
LINE_COUNT=$(count_code_lines "$FILE_PATH")
|
|
3
|
+
if [[ "$LINE_COUNT" -gt "$RUST_MAX_FILE" ]]; then
|
|
4
|
+
_split_hint="split into submodules"
|
|
5
|
+
[ -n "$(detect_clippy)" ] && _split_hint="$_split_hint (clippy enforces complexity here)"
|
|
6
|
+
_approx=""
|
|
7
|
+
has_unterminated_block "$FILE_PATH" && _approx=" (size approximated — an unterminated /* or a string containing /* may be affecting the count)"
|
|
8
|
+
add_error "File exceeds ${RUST_MAX_FILE}-line limit: $FILE_PATH ($LINE_COUNT code lines, blanks/comments excluded)${_approx} — $_split_hint"
|
|
9
|
+
fi
|
|
10
|
+
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
_has_inline_cfg_test=0
|
|
2
|
+
# Match bare `#[cfg(test)]` and `test` as the FIRST predicate of an all()/any()
|
|
3
|
+
# combinator (`#[cfg(all(test, feature = "x"))]`, `#[cfg(any(test, ...))]`).
|
|
4
|
+
# Deliberately NOT a loose `cfg(.*\btest\b`: that would false-positive on
|
|
5
|
+
# `#[cfg(not(test))]` (the opposite gate) and `#[cfg(feature = "test-utils")]`
|
|
6
|
+
# (test inside a string). A non-leading `test` predicate (`all(feature, test)`)
|
|
7
|
+
# is the rare miss we accept to keep zero false positives.
|
|
8
|
+
#
|
|
9
|
+
# EXEMPTION: a bare `#[cfg(test)]` that wires a BODYLESS `mod <name>;` decl —
|
|
10
|
+
# single-line (`#[cfg(test)] mod tests;`) or rustfmt's attribute-on-its-own-line
|
|
11
|
+
# two-line form (`#[cfg(test)]` then `mod tests;`) — is a pointer to an external
|
|
12
|
+
# module-sibling tests/ dir, not inline test code, and is not flagged. Further
|
|
13
|
+
# OUTER attributes may sit between the two: `#[path = "tests/stats.rs"]` is the
|
|
14
|
+
# only wiring a crate that bans mod.rs has for a module-sibling test file, so
|
|
15
|
+
# the decl is looked for past any attribute run, bounded so it cannot scan the
|
|
16
|
+
# whole file. Inner attributes (`#![...]`) are deliberately not skipped — they
|
|
17
|
+
# cannot precede a declaration, and neither is an attribute split across lines
|
|
18
|
+
# (`#[path =` / `"x.rs"]`) — same line-based limitation as 30-suppression.sh,
|
|
19
|
+
# and it fails closed: the file is reported rather than silently exempted.
|
|
20
|
+
# Only the BARE cfg(test) form earns this: the
|
|
21
|
+
# all()/any() combinator stays blocked unconditionally (even ahead of a bodyless
|
|
22
|
+
# decl), and any body (`mod tests {`, any line form) stays blocked. grep is
|
|
23
|
+
# line-based, so the multi-line pairing needs awk (getline the next lines); no
|
|
24
|
+
# \b — BSD awk has none, use a negated [[:alnum:]_] class (or line end) for the
|
|
25
|
+
# boundary instead.
|
|
26
|
+
if [[ "$FILE_PATH" == */src/* ]] \
|
|
27
|
+
&& awk '
|
|
28
|
+
/^[[:space:]]*#\[cfg\((all|any)\(test([^[:alnum:]_]|$)/ { found=1; exit }
|
|
29
|
+
/^[[:space:]]*#\[cfg\(test\)\][[:space:]]*(pub[[:space:]]+)?mod[[:space:]]+[A-Za-z_][A-Za-z0-9_]*[[:space:]]*;[[:space:]]*(\/\/.*)?$/ { next }
|
|
30
|
+
/^[[:space:]]*#\[cfg\(test\)\][[:space:]]*$/ {
|
|
31
|
+
nxt=""
|
|
32
|
+
# 8 attributes plus the decl line itself — each pass reads one line, so
|
|
33
|
+
# the bound has to leave room for the decl read or a decl behind the
|
|
34
|
+
# full 8 attributes is reported.
|
|
35
|
+
for (i = 0; i < 9; i++) {
|
|
36
|
+
if ((getline nxt) <= 0) { found=1; exit }
|
|
37
|
+
if (nxt !~ /^[[:space:]]*#\[[^!]/) break
|
|
38
|
+
}
|
|
39
|
+
if (nxt ~ /^[[:space:]]*(pub[[:space:]]+)?mod[[:space:]]+[A-Za-z_][A-Za-z0-9_]*[[:space:]]*;[[:space:]]*(\/\/.*)?$/) { next }
|
|
40
|
+
found=1; exit
|
|
41
|
+
}
|
|
42
|
+
/^[[:space:]]*#\[cfg\(test\)\]/ { found=1; exit }
|
|
43
|
+
END { exit (found ? 0 : 1) }
|
|
44
|
+
' "$FILE_PATH" 2>/dev/null; then
|
|
45
|
+
add_error "Inline #[cfg(test)] in $FILE_PATH — unit tests belong in a module-sibling tests/ dir wired by a bodyless #[cfg(test)] mod tests; decl; crate-root tests/ is for integration tests"
|
|
46
|
+
_has_inline_cfg_test=1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Test placement: a test-bearing .rs file must live under a tests/ dir, kept
|
|
50
|
+
# flat (only fixtures/helpers/common subdirs allowed — common/mod.rs is the
|
|
51
|
+
# cargo idiom for shared test helpers). Skip when the inline-#[cfg(test)] rule
|
|
52
|
+
# already fired: that's the canonical `src/lib.rs` with `#[cfg(test)] mod tests`
|
|
53
|
+
# pattern, where "move the file to tests/" is wrong (it would orphan the pub
|
|
54
|
+
# items) — the cfg(test) message already says to extract the tests.
|
|
55
|
+
_is_rust_test=0
|
|
56
|
+
case "$(basename "$FILE_PATH")" in
|
|
57
|
+
*_test.rs|*_tests.rs) _is_rust_test=1 ;;
|
|
58
|
+
esac
|
|
59
|
+
# #[bench] and #[wasm_bindgen_test] are deliberately NOT in the alternation:
|
|
60
|
+
# benches belong in benches/ (cargo convention) and wasm-bindgen tests have no
|
|
61
|
+
# single canonical home — "move to tests/" would be a false positive for both.
|
|
62
|
+
# Generalized over `#[<any::path>::test]` rather than a hardcoded runtime list,
|
|
63
|
+
# so test_log::test, trace_test::test, actix_web::test, etc. all trip the rule;
|
|
64
|
+
# `test_case` (the test_case crate's generator) is added explicitly. `#[rstest]`
|
|
65
|
+
# is its own alternation (the macro name is not `test`). `#[cfg(test)]` is NOT
|
|
66
|
+
# matched here — no `test`/`test_case` token follows `#[` — and stays owned by
|
|
67
|
+
# the inline-cfg(test) rule above.
|
|
68
|
+
if [[ "$_is_rust_test" -eq 0 ]] \
|
|
69
|
+
&& grep -qE '^[[:space:]]*#\[([A-Za-z_][A-Za-z0-9_]*::)*(test|test_case)\b|^[[:space:]]*#\[rstest\b' "$FILE_PATH" 2>/dev/null; then
|
|
70
|
+
_is_rust_test=1
|
|
71
|
+
fi
|
|
72
|
+
if [[ "$_is_rust_test" -eq 1 && "$_has_inline_cfg_test" -eq 0 ]]; then
|
|
73
|
+
if [[ "$FILE_PATH" != */tests/* ]]; then
|
|
74
|
+
add_error "Rust test file outside tests/: $FILE_PATH — move to a sibling tests/ directory"
|
|
75
|
+
else
|
|
76
|
+
_after_tests="${FILE_PATH##*/tests/}"
|
|
77
|
+
if [[ "$_after_tests" == */* ]]; then
|
|
78
|
+
_subdir="${_after_tests%%/*}"
|
|
79
|
+
if [[ "$_subdir" != "fixtures" && "$_subdir" != "helpers" && "$_subdir" != "common" ]]; then
|
|
80
|
+
add_error "Rust test nested in tests/ subdirectory: $FILE_PATH — keep tests/ flat (only fixtures/helpers/common subdirs allowed)"
|
|
81
|
+
fi
|
|
82
|
+
fi
|
|
83
|
+
fi
|
|
84
|
+
fi
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
|
|
2
|
+
# Forbidden lint suppression: #[allow(...)] / #![allow(...)] / #[expect(...)]
|
|
3
|
+
# and the #[cfg_attr(..., allow(...))] / cfg_attr(..., expect(...)) back door.
|
|
4
|
+
# Known limitation: grep is line-based, so an attribute split across multiple
|
|
5
|
+
# lines (e.g. `#[cfg_attr(\n test,\n allow(...))]`) escapes detection. Rare;
|
|
6
|
+
# multi-line attribute parsing isn't worth a full tokenizer here.
|
|
7
|
+
#
|
|
8
|
+
# EXEMPTION (tests/ only): the file-level INNER header — `#![allow(...)]` and
|
|
9
|
+
# the `#![cfg_attr(...)]` form of it — is not flagged in a test file. The lints
|
|
10
|
+
# a test suppresses there (unwrap_used, expect_used, panic, float_cmp,
|
|
11
|
+
# too_many_lines) are ones test code violates on purpose: a test that must
|
|
12
|
+
# panic to fail is not a defect being hidden, and the toolu-conventions Rust
|
|
13
|
+
# template prescribes exactly this header. Only the INNER form earns it —
|
|
14
|
+
# a per-item `#[allow(...)]` inside a test file silences one real warning on
|
|
15
|
+
# one real item and stays banned, as everywhere else. Same shape as
|
|
16
|
+
# ts-quality's narrower @ts-expect-error exemption for .test/.spec files.
|
|
17
|
+
_rs_suppress_re='^[[:space:]]*#!?\[(allow|expect)\(|^[[:space:]]*#!?\[cfg_attr\([^]]*\b(allow|expect)\b'
|
|
18
|
+
_rs_suppress_hint=""
|
|
19
|
+
if [[ "$FILE_PATH" == */tests/* ]]; then
|
|
20
|
+
_rs_suppress_re='^[[:space:]]*#\[(allow|expect)\(|^[[:space:]]*#\[cfg_attr\([^]]*\b(allow|expect)\b'
|
|
21
|
+
_rs_suppress_hint=" In a test file only the file-level #![allow(...)] header is accepted."
|
|
22
|
+
fi
|
|
23
|
+
if grep -qE "$_rs_suppress_re" "$FILE_PATH" 2>/dev/null; then
|
|
24
|
+
add_error "Forbidden lint suppression (#[allow]/#[expect]/cfg_attr allow) in $FILE_PATH — remove it and fix the underlying warning in code. For unsafe_code, override in Cargo.toml [lints.rust].${_rs_suppress_hint}"
|
|
25
|
+
fi
|
|
26
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Forbidden unsafe blocks/functions — except for crates listed in the
|
|
2
|
+
# exemptions file (FFI crates, sandboxes, etc).
|
|
3
|
+
exempt=0
|
|
4
|
+
if [ -f "$EXEMPTIONS_FILE" ]; then
|
|
5
|
+
while IFS= read -r crate; do
|
|
6
|
+
[ -z "$crate" ] && continue
|
|
7
|
+
if [[ "$FILE_PATH" == *"$crate"* ]]; then
|
|
8
|
+
exempt=1
|
|
9
|
+
break
|
|
10
|
+
fi
|
|
11
|
+
done <<< "$(read_list "$EXEMPTIONS_FILE")"
|
|
12
|
+
fi
|
|
13
|
+
if [ "$exempt" -eq 0 ]; then
|
|
14
|
+
# Strip // line comments and /* */ blocks (incl. multi-line) before matching,
|
|
15
|
+
# so a commented-out or doc-mentioned `unsafe {` does not false-positive. The
|
|
16
|
+
# block-comment state machine mirrors count_code_lines in detect.sh. String
|
|
17
|
+
# literals containing the pattern are not handled — same heuristic limit as the
|
|
18
|
+
# other comment-stripping passes here. BSD awk (macOS) has no \b, so explicit
|
|
19
|
+
# boundaries are used instead.
|
|
20
|
+
if awk '
|
|
21
|
+
{ line=$0
|
|
22
|
+
if (inblock) {
|
|
23
|
+
idx=index(line,"*/")
|
|
24
|
+
if (idx>0) { line=substr(line, idx+2); inblock=0 } else next
|
|
25
|
+
}
|
|
26
|
+
while ((s=index(line,"/*"))>0) {
|
|
27
|
+
rest=substr(line, s+2); e=index(rest,"*/")
|
|
28
|
+
if (e>0) { line=substr(line,1,s-1) substr(rest, e+2) }
|
|
29
|
+
else { line=substr(line,1,s-1); inblock=1; break }
|
|
30
|
+
}
|
|
31
|
+
sub(/\/\/.*$/, "", line) # // line comment
|
|
32
|
+
if (line ~ /(^|[^A-Za-z0-9_])unsafe[ \t]*(\{|fn )/) { found=1; exit }
|
|
33
|
+
}
|
|
34
|
+
END { exit(found ? 0 : 1) }
|
|
35
|
+
' "$FILE_PATH" 2>/dev/null; then
|
|
36
|
+
add_error "Forbidden unsafe code in $FILE_PATH — refactor to safe alternative. Add crate to settings/rust-unsafe-exemptions.txt if it legitimately needs unsafe (FFI, sandboxing)."
|
|
37
|
+
fi
|
|
38
|
+
fi
|
|
39
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
RUST_MAX_FN=$(rust_max_fn_lines)
|
|
2
|
+
# Match fn with any leading visibility/qualifier combo: pub, pub(crate)/pub(super),
|
|
3
|
+
# async, const, unsafe, extern "C", and combinations thereof.
|
|
4
|
+
# The fn end is found with a brace-depth counter from the fn's own line, so a
|
|
5
|
+
# method inside an impl/mod is measured to ITS close (not the impl's), and an
|
|
6
|
+
# inner if/match/loop close can't end the range early. Body-less trait decls
|
|
7
|
+
# (`fn f();`) are released on the `;`. Single-line "..." strings and the
|
|
8
|
+
# '{'/'}' char literals are stripped before counting so a lone brace inside
|
|
9
|
+
# them cannot skew the depth. Known limitation: a brace inside a MULTI-LINE
|
|
10
|
+
# string (incl. raw strings) still leaks into the count — an unbalanced one
|
|
11
|
+
# leaves the fn unmeasured (fail-open), never falsely flagged short.
|
|
12
|
+
LONG_RS_FUNCS=$(awk -v max="$RUST_MAX_FN" -v q="'" '
|
|
13
|
+
!infn && /^[[:space:]]*(pub(\([^)]+\))?[[:space:]]+)?((async|const|unsafe|extern)([[:space:]]+"[^"]*")?[[:space:]]+)*fn / {
|
|
14
|
+
infn=1; start=NR; name=$0; depth=0; opened=0
|
|
15
|
+
}
|
|
16
|
+
infn {
|
|
17
|
+
line=$0
|
|
18
|
+
gsub(/\\"/, "", line) # escaped quotes would derail the span strip
|
|
19
|
+
gsub(/"[^"]*"/, "", line) # single-line string contents
|
|
20
|
+
gsub(q "[{}]" q, "", line) # brace char literals (lifetimes never contain braces)
|
|
21
|
+
no=gsub(/\{/, "{", line); nc=gsub(/\}/, "}", line)
|
|
22
|
+
depth += no - nc
|
|
23
|
+
if (no > 0) opened=1
|
|
24
|
+
if (opened && depth <= 0) {
|
|
25
|
+
len=NR-start
|
|
26
|
+
if (len > max) printf "%s:%d (%d lines)\n", name, start, len
|
|
27
|
+
infn=0
|
|
28
|
+
}
|
|
29
|
+
if (!opened && $0 ~ /;[[:space:]]*$/) infn=0
|
|
30
|
+
}
|
|
31
|
+
' "$FILE_PATH" 2>/dev/null)
|
|
32
|
+
# Test files are exempt: a table-driven test is one long fn by nature, and the
|
|
33
|
+
# toolu-conventions Rust test header prescribes `clippy::too_many_lines` for
|
|
34
|
+
# exactly that reason — flagging it here would contradict the template.
|
|
35
|
+
if [[ -n "$LONG_RS_FUNCS" && "$_is_rust_test" -ne 1 ]]; then
|
|
36
|
+
add_error "Function too long in $FILE_PATH (>${RUST_MAX_FN} lines) — extract helpers."
|
|
37
|
+
fi
|
|
38
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
RUST_MAX_IMPL=$(rust_max_impl_lines)
|
|
2
|
+
# Brace-depth counter (same technique + string/char strip as the fn check):
|
|
3
|
+
# a bare `^\}` close-marker would be tripped early by a column-0 `}` from
|
|
4
|
+
# macro-generated or wide-rustfmt code inside the impl, undercounting it.
|
|
5
|
+
LONG_IMPL=$(awk -v max="$RUST_MAX_IMPL" -v q="'" '
|
|
6
|
+
!inimpl && /^[[:space:]]*(unsafe[[:space:]]+)?impl[[:space:]<]/ {
|
|
7
|
+
inimpl=1; start=NR; name=$0; depth=0; opened=0
|
|
8
|
+
}
|
|
9
|
+
inimpl {
|
|
10
|
+
line=$0
|
|
11
|
+
gsub(/\\"/, "", line)
|
|
12
|
+
gsub(/"[^"]*"/, "", line)
|
|
13
|
+
gsub(q "[{}]" q, "", line)
|
|
14
|
+
no=gsub(/\{/, "{", line); nc=gsub(/\}/, "}", line)
|
|
15
|
+
depth += no - nc
|
|
16
|
+
if (no > 0) opened=1
|
|
17
|
+
if (opened && depth <= 0) {
|
|
18
|
+
len=NR-start
|
|
19
|
+
if (len > max) printf "%s:%d (%d lines)\n", name, start, len
|
|
20
|
+
inimpl=0
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
' "$FILE_PATH" 2>/dev/null)
|
|
24
|
+
# Exempt in test files, for the same reason as the fn-length rule above.
|
|
25
|
+
if [[ -n "$LONG_IMPL" && "$_is_rust_test" -ne 1 ]]; then
|
|
26
|
+
add_error "Impl block too large in $FILE_PATH (>${RUST_MAX_IMPL} lines) — split into trait impls or modules."
|
|
27
|
+
fi
|
|
28
|
+
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# shellcheck shell=bash
|
|
2
|
+
# --- Error-handling rules (zero tolerance) ---
|
|
3
|
+
# src/ is production code, so any panic-on-error pattern here is a prod panic
|
|
4
|
+
# — EXCEPT in a test file. "tests must live in tests/" is true of the crate-root
|
|
5
|
+
# suite but not of a module-sibling one: a crate that bans mod.rs wires its
|
|
6
|
+
# colocated tests as src/<mod>/tests/<name>.rs, which is under src/ and is still
|
|
7
|
+
# test code. `.unwrap()` there is how a test reports a failed assertion, not a
|
|
8
|
+
# production panic. Same `_is_rust_test` gate 90-docs.sh already uses (set in
|
|
9
|
+
# 20-tests.sh: *_test.rs/*_tests.rs filename, or a #[test]/#[rstest]/#[test_case]
|
|
10
|
+
# attribute in the body).
|
|
11
|
+
if [[ "$FILE_PATH" == */src/* && "$_is_rust_test" -ne 1 ]] && command -v ast-grep >/dev/null 2>&1; then
|
|
12
|
+
# ONE ast-grep process for all six rules. `ast-grep run -p` re-parses the file
|
|
13
|
+
# per pattern; `scan --inline-rules` parses once and returns every rule's hits
|
|
14
|
+
# as JSON. The excerpt lines rebuilt from .file/.range/.lines below are
|
|
15
|
+
# byte-identical to what `run -p` printed. Mirrors ts-quality/78-error-ast.sh.
|
|
16
|
+
#
|
|
17
|
+
# severity is `warning` on purpose: `scan` exits non-zero when an ERROR-level
|
|
18
|
+
# rule matches, which would be indistinguishable from a crashed tool. At
|
|
19
|
+
# warning level both a clean and a matching run exit 0, so a non-zero exit (or
|
|
20
|
+
# any stderr) still means the tool genuinely broke.
|
|
21
|
+
ast_err_file="$(mktemp)"
|
|
22
|
+
ast_rules=$(cat <<'RS_AST_RULES'
|
|
23
|
+
id: unwrap
|
|
24
|
+
language: rust
|
|
25
|
+
severity: warning
|
|
26
|
+
message: unwrap on Result/Option
|
|
27
|
+
rule:
|
|
28
|
+
pattern: '$E.unwrap()'
|
|
29
|
+
---
|
|
30
|
+
id: expect
|
|
31
|
+
language: rust
|
|
32
|
+
severity: warning
|
|
33
|
+
message: expect on Result/Option
|
|
34
|
+
rule:
|
|
35
|
+
pattern: '$E.expect($M)'
|
|
36
|
+
---
|
|
37
|
+
id: panic
|
|
38
|
+
language: rust
|
|
39
|
+
severity: warning
|
|
40
|
+
message: panic!
|
|
41
|
+
rule:
|
|
42
|
+
pattern: 'panic!($$$)'
|
|
43
|
+
---
|
|
44
|
+
id: todo
|
|
45
|
+
language: rust
|
|
46
|
+
severity: warning
|
|
47
|
+
message: todo!
|
|
48
|
+
rule:
|
|
49
|
+
pattern: 'todo!($$$)'
|
|
50
|
+
---
|
|
51
|
+
id: unimplemented
|
|
52
|
+
language: rust
|
|
53
|
+
severity: warning
|
|
54
|
+
message: unimplemented!
|
|
55
|
+
rule:
|
|
56
|
+
pattern: 'unimplemented!($$$)'
|
|
57
|
+
---
|
|
58
|
+
id: unreachable
|
|
59
|
+
language: rust
|
|
60
|
+
severity: warning
|
|
61
|
+
message: unreachable!
|
|
62
|
+
rule:
|
|
63
|
+
pattern: 'unreachable!($$$)'
|
|
64
|
+
RS_AST_RULES
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
ast_grep_failed=0
|
|
68
|
+
ast_grep_fail_detail=""
|
|
69
|
+
ast_lines=""
|
|
70
|
+
ast_json=$(ast-grep scan --inline-rules "$ast_rules" --json "$FILE_PATH" 2>"$ast_err_file")
|
|
71
|
+
ast_rc=$?
|
|
72
|
+
# Flatten every hit to "<ruleId>\t<file>:<line>:<source line>", one output line
|
|
73
|
+
# per matched source line.
|
|
74
|
+
#
|
|
75
|
+
# Two distinct failure modes, reported distinctly: ast-grep itself broke, or it
|
|
76
|
+
# exited 0 and returned something that is not the documented JSON array. Both
|
|
77
|
+
# mean the rules could not be verified, but collapsing them into one message
|
|
78
|
+
# sends the reader to the wrong tool.
|
|
79
|
+
ast_fail_stage=""
|
|
80
|
+
if [[ "$ast_rc" -ne 0 || -s "$ast_err_file" ]]; then
|
|
81
|
+
ast_grep_failed=1
|
|
82
|
+
ast_fail_stage="ast-grep"
|
|
83
|
+
elif ! ast_lines=$(printf '%s' "$ast_json" | jq -r '
|
|
84
|
+
.[] | . as $m
|
|
85
|
+
| ((($m.lines // "") | split("\n")) | to_entries[])
|
|
86
|
+
| "\($m.ruleId)\t\($m.file):\($m.range.start.line + 1 + .key):\(.value)"
|
|
87
|
+
' 2>/dev/null); then
|
|
88
|
+
ast_grep_failed=1
|
|
89
|
+
ast_fail_stage="jq"
|
|
90
|
+
ast_lines=""
|
|
91
|
+
fi
|
|
92
|
+
if [[ "$ast_grep_failed" -ne 0 ]]; then
|
|
93
|
+
# Exit code plus the first stderr line (capped at ~200 chars) so the surfaced
|
|
94
|
+
# message tells the agent WHAT broke, not just THAT it broke.
|
|
95
|
+
ast_stderr_first=$(head -n 1 "$ast_err_file" 2>/dev/null | cut -c1-200)
|
|
96
|
+
if [[ "$ast_fail_stage" == "jq" ]]; then
|
|
97
|
+
ast_grep_fail_detail="ast-grep exited 0 but its output did not parse as the documented JSON array"
|
|
98
|
+
else
|
|
99
|
+
ast_grep_fail_detail="ast-grep exit ${ast_rc}${ast_stderr_first:+: $ast_stderr_first}"
|
|
100
|
+
fi
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# join_hits BLOCK... — concatenate non-empty hit blocks into $JOINED with one
|
|
104
|
+
# newline between them. Each block comes from a $(...) capture, which strips the
|
|
105
|
+
# trailing newline, so plain concatenation glued one block's last line onto the
|
|
106
|
+
# next block's first line. Sets a variable instead of printing to avoid a fork
|
|
107
|
+
# on a path that runs after every edit.
|
|
108
|
+
join_hits() {
|
|
109
|
+
local _part
|
|
110
|
+
JOINED=""
|
|
111
|
+
for _part in "$@"; do
|
|
112
|
+
[ -z "$_part" ] && continue
|
|
113
|
+
[ -n "$JOINED" ] && JOINED="${JOINED}"$'\n'
|
|
114
|
+
JOINED="${JOINED}${_part}"
|
|
115
|
+
done
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# ast_hits RULE_ID LIMIT — this rule's excerpt lines, capped like before.
|
|
119
|
+
ast_hits() {
|
|
120
|
+
local _id="$1" _limit="$2" _row _rest
|
|
121
|
+
[ -n "$ast_lines" ] || return 0
|
|
122
|
+
while IFS=$'\t' read -r _row _rest; do
|
|
123
|
+
[ "$_row" = "$_id" ] && printf '%s\n' "$_rest"
|
|
124
|
+
done <<< "$ast_lines" | head -n "$_limit"
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
UNWRAP_HITS=$(ast_hits unwrap 5)
|
|
128
|
+
if [[ -n "$UNWRAP_HITS" ]]; then
|
|
129
|
+
add_error ".unwrap() in $FILE_PATH — use ? or match on Result/Option"$'\n'"${UNWRAP_HITS}"
|
|
130
|
+
fi
|
|
131
|
+
EXPECT_HITS=$(ast_hits expect 5)
|
|
132
|
+
if [[ -n "$EXPECT_HITS" ]]; then
|
|
133
|
+
add_error ".expect() in $FILE_PATH — use ? or match on Result/Option"$'\n'"${EXPECT_HITS}"
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
PANIC_HITS=$(ast_hits panic 3)
|
|
137
|
+
TODO_HITS=$(ast_hits todo 3)
|
|
138
|
+
UNIMPL_HITS=$(ast_hits unimplemented 3)
|
|
139
|
+
UNREACH_HITS=$(ast_hits unreachable 3)
|
|
140
|
+
if [[ -n "$PANIC_HITS" || -n "$TODO_HITS" || -n "$UNIMPL_HITS" || -n "$UNREACH_HITS" ]]; then
|
|
141
|
+
join_hits "$PANIC_HITS" "$TODO_HITS" "$UNIMPL_HITS" "$UNREACH_HITS"
|
|
142
|
+
add_error "panic!/todo!/unimplemented!/unreachable! in $FILE_PATH — return a Result instead"$'\n'"${JOINED}"
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
rm -f "$ast_err_file"
|
|
146
|
+
if [[ "$ast_grep_failed" -ne 0 ]]; then
|
|
147
|
+
add_error "ast-grep failed while scanning $FILE_PATH — ${ast_grep_fail_detail:-unknown error}; error-handling rules could not be verified. Fix the tool/file and re-edit"
|
|
148
|
+
fi
|
|
149
|
+
fi
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# --- No-mocks (zero tolerance): forbid mock frameworks — real data/fixtures only ---
|
|
2
|
+
# Split scope, because mock *definitions* live in src/ while mock *imports* are
|
|
3
|
+
# pulled into test files:
|
|
4
|
+
# - src/**/*.rs: #[automock], #[cfg_attr(..., automock)], mock! { ... }
|
|
5
|
+
# (also the mock!(...) and mock![...] delimiter forms — mock! is a macro,
|
|
6
|
+
# so all three bracket kinds are valid Rust).
|
|
7
|
+
# - test files (tests/ placement, or *_test.rs / *_tests.rs basenames — the
|
|
8
|
+
# same path predicate 20-tests.sh uses; its attribute-based detection
|
|
9
|
+
# (#[test], #[rstest], ...) is not reused here since mock imports are `use`
|
|
10
|
+
# statements, not attribute-marked items): use mockall::...;, use faux::...;.
|
|
11
|
+
# Same ast-grep-scan pattern as 60-error-handling.sh: ONE process, severity
|
|
12
|
+
# warning (a matching rule and a clean run both exit 0 — only a genuine crash
|
|
13
|
+
# is distinguishable from a clean pass), jq flattens every hit to one excerpt
|
|
14
|
+
# line, add_error on a match or on tool breakage.
|
|
15
|
+
#
|
|
16
|
+
# Opt-out: lang.rust.noMocks (default true) via quality_flag.
|
|
17
|
+
if [[ "$(quality_flag rust noMocks true)" == "true" ]] && command -v ast-grep >/dev/null 2>&1; then
|
|
18
|
+
_nm_in_src=0
|
|
19
|
+
[[ "$FILE_PATH" == */src/* ]] && _nm_in_src=1
|
|
20
|
+
|
|
21
|
+
_nm_is_test_file=0
|
|
22
|
+
[[ "$FILE_PATH" == */tests/* ]] && _nm_is_test_file=1
|
|
23
|
+
case "$(basename "$FILE_PATH")" in
|
|
24
|
+
*_test.rs|*_tests.rs) _nm_is_test_file=1 ;;
|
|
25
|
+
esac
|
|
26
|
+
|
|
27
|
+
NM_RULES=""
|
|
28
|
+
if [[ "$_nm_in_src" -eq 1 ]]; then
|
|
29
|
+
NM_RULES=$(cat <<'RS_NOMOCK_SRC'
|
|
30
|
+
id: automock-attr
|
|
31
|
+
language: rust
|
|
32
|
+
severity: warning
|
|
33
|
+
message: '#[automock] mock definition'
|
|
34
|
+
rule:
|
|
35
|
+
pattern: '#[automock]'
|
|
36
|
+
---
|
|
37
|
+
id: automock-cfg-attr
|
|
38
|
+
language: rust
|
|
39
|
+
severity: warning
|
|
40
|
+
message: cfg_attr(..., automock) mock definition
|
|
41
|
+
rule:
|
|
42
|
+
pattern: '#[cfg_attr($$$, automock)]'
|
|
43
|
+
---
|
|
44
|
+
id: mock-macro
|
|
45
|
+
language: rust
|
|
46
|
+
severity: warning
|
|
47
|
+
message: 'mock! { ... } mock definition'
|
|
48
|
+
rule:
|
|
49
|
+
any:
|
|
50
|
+
- pattern: 'mock! { $$$ }'
|
|
51
|
+
- pattern: 'mock!($$$)'
|
|
52
|
+
- pattern: 'mock![$$$]'
|
|
53
|
+
RS_NOMOCK_SRC
|
|
54
|
+
)
|
|
55
|
+
fi
|
|
56
|
+
if [[ "$_nm_is_test_file" -eq 1 ]]; then
|
|
57
|
+
NM_TEST_RULES=$(cat <<'RS_NOMOCK_TEST'
|
|
58
|
+
id: mockall-import
|
|
59
|
+
language: rust
|
|
60
|
+
severity: warning
|
|
61
|
+
message: 'use mockall::...; mock import'
|
|
62
|
+
rule:
|
|
63
|
+
kind: use_declaration
|
|
64
|
+
regex: 'mockall::'
|
|
65
|
+
---
|
|
66
|
+
id: faux-import
|
|
67
|
+
language: rust
|
|
68
|
+
severity: warning
|
|
69
|
+
message: 'use faux::...; mock import'
|
|
70
|
+
rule:
|
|
71
|
+
kind: use_declaration
|
|
72
|
+
regex: 'faux::'
|
|
73
|
+
RS_NOMOCK_TEST
|
|
74
|
+
)
|
|
75
|
+
if [[ -n "$NM_RULES" ]]; then
|
|
76
|
+
NM_RULES="${NM_RULES}"$'\n---\n'"${NM_TEST_RULES}"
|
|
77
|
+
else
|
|
78
|
+
NM_RULES="$NM_TEST_RULES"
|
|
79
|
+
fi
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
if [[ -n "$NM_RULES" ]]; then
|
|
83
|
+
nm_err_file="$(mktemp)"
|
|
84
|
+
nm_grep_failed=0
|
|
85
|
+
nm_fail_detail=""
|
|
86
|
+
nm_lines=""
|
|
87
|
+
nm_json=$(ast-grep scan --inline-rules "$NM_RULES" --json "$FILE_PATH" 2>"$nm_err_file")
|
|
88
|
+
nm_rc=$?
|
|
89
|
+
# Two distinct failure modes, reported distinctly: ast-grep itself broke, or
|
|
90
|
+
# it exited 0 and returned something that is not the documented JSON array.
|
|
91
|
+
if [[ "$nm_rc" -ne 0 || -s "$nm_err_file" ]]; then
|
|
92
|
+
nm_grep_failed=1
|
|
93
|
+
nm_stderr_first=$(head -n 1 "$nm_err_file" 2>/dev/null | cut -c1-200)
|
|
94
|
+
nm_fail_detail="ast-grep exit ${nm_rc}${nm_stderr_first:+: $nm_stderr_first}"
|
|
95
|
+
elif [[ -z "$(printf '%s' "$nm_json" | tr -d '[:space:]')" ]]; then
|
|
96
|
+
# ast-grep exited 0 with genuinely empty stdout — jq also exits 0 on
|
|
97
|
+
# empty input with empty output, so left unchecked this would be
|
|
98
|
+
# indistinguishable from a clean "[]" scan and silently swallow a real
|
|
99
|
+
# breakage as "no hits". A well-behaved scan always emits at least "[]".
|
|
100
|
+
nm_grep_failed=1
|
|
101
|
+
nm_fail_detail="ast-grep exited 0 with empty output (expected at least the JSON array \"[]\")"
|
|
102
|
+
elif ! nm_lines=$(printf '%s' "$nm_json" | jq -r '
|
|
103
|
+
.[] | . as $m
|
|
104
|
+
| ((($m.lines // "") | split("\n")) | to_entries[])
|
|
105
|
+
| "\($m.ruleId)\t\($m.file):\($m.range.start.line + 1 + .key):\(.value)"
|
|
106
|
+
' 2>/dev/null); then
|
|
107
|
+
nm_grep_failed=1
|
|
108
|
+
nm_fail_detail="ast-grep exited 0 but its output did not parse as the documented JSON array"
|
|
109
|
+
nm_lines=""
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# nm_hits RULE_ID LIMIT — this rule's excerpt lines, capped.
|
|
113
|
+
nm_hits() {
|
|
114
|
+
local _id="$1" _limit="$2" _row _rest
|
|
115
|
+
[ -n "$nm_lines" ] || return 0
|
|
116
|
+
while IFS=$'\t' read -r _row _rest; do
|
|
117
|
+
[ "$_row" = "$_id" ] && printf '%s\n' "$_rest"
|
|
118
|
+
done <<< "$nm_lines" | head -n "$_limit"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
AUTOMOCK_HITS=$(nm_hits automock-attr 5; nm_hits automock-cfg-attr 5)
|
|
122
|
+
if [[ -n "$AUTOMOCK_HITS" ]]; then
|
|
123
|
+
add_error "no-mocks: #[automock] mock definition in $FILE_PATH — write against real data/fixtures instead"$'\n'"${AUTOMOCK_HITS}"
|
|
124
|
+
fi
|
|
125
|
+
MOCK_MACRO_HITS=$(nm_hits mock-macro 5)
|
|
126
|
+
if [[ -n "$MOCK_MACRO_HITS" ]]; then
|
|
127
|
+
add_error "no-mocks: mock! { ... } mock definition in $FILE_PATH — write against real data/fixtures instead"$'\n'"${MOCK_MACRO_HITS}"
|
|
128
|
+
fi
|
|
129
|
+
MOCK_IMPORT_HITS=$(nm_hits mockall-import 5; nm_hits faux-import 5)
|
|
130
|
+
if [[ -n "$MOCK_IMPORT_HITS" ]]; then
|
|
131
|
+
add_error "no-mocks: mockall/faux import in $FILE_PATH — write against real data/fixtures instead"$'\n'"${MOCK_IMPORT_HITS}"
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
rm -f "$nm_err_file"
|
|
135
|
+
if [[ "$nm_grep_failed" -ne 0 ]]; then
|
|
136
|
+
add_error "ast-grep failed while scanning $FILE_PATH — ${nm_fail_detail:-unknown error}; no-mocks rules could not be verified. Fix the tool/file and re-edit"
|
|
137
|
+
fi
|
|
138
|
+
fi
|
|
139
|
+
fi
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# --- Docs (soft advisory, never blocks) ---
|
|
2
|
+
# A public API item in src/ should carry a concise /// doc comment. Advisory
|
|
3
|
+
# only — collected separately from MESSAGES so it never sets the failing gate.
|
|
4
|
+
DOC_ADVISORY=""
|
|
5
|
+
if [[ "$FILE_PATH" == */src/* && ! "$_is_rust_test" -eq 1 ]]; then
|
|
6
|
+
_undoc=$(awk '
|
|
7
|
+
/^[[:space:]]*$/ { next } # blanks do not reset the doc context
|
|
8
|
+
/^[[:space:]]*#\[/ { next } # attributes sit between doc and item
|
|
9
|
+
{
|
|
10
|
+
if ($0 ~ /^[[:space:]]*pub(\([^)]+\))?[[:space:]]+(fn|struct|enum|trait|const|static|type|mod)[[:space:]]/) {
|
|
11
|
+
if (prev !~ /^[[:space:]]*(\/\/\/|\/\/!)/) printf "%d: %s\n", NR, $0
|
|
12
|
+
}
|
|
13
|
+
prev=$0
|
|
14
|
+
}
|
|
15
|
+
' "$FILE_PATH" 2>/dev/null | head -3)
|
|
16
|
+
if [[ -n "$_undoc" ]]; then
|
|
17
|
+
DOC_ADVISORY="Public items missing a /// doc comment in $FILE_PATH — add a concise one-line doc:"$'\n'"${_undoc}"
|
|
18
|
+
fi
|
|
19
|
+
# Concise cap: flag doc-comment runs that have grown long.
|
|
20
|
+
_verbose_doc=$(awk '
|
|
21
|
+
/^[[:space:]]*\/\/[\/!]/ { if (run==0) start=NR; run++; next }
|
|
22
|
+
{ if (run>12) printf "%d: doc block is %d lines — trim to the essentials\n", start, run; run=0 }
|
|
23
|
+
END { if (run>12) printf "%d: doc block is %d lines — trim to the essentials\n", start, run }
|
|
24
|
+
' "$FILE_PATH" 2>/dev/null | head -2)
|
|
25
|
+
if [[ -n "$_verbose_doc" ]]; then
|
|
26
|
+
# Separator appended on its own line: ANSI-C quoting is NOT honored inside a
|
|
27
|
+
# ${var:+word} expansion, so it cannot be inlined there.
|
|
28
|
+
[[ -n "$DOC_ADVISORY" ]] && DOC_ADVISORY="${DOC_ADVISORY}"$'\n'
|
|
29
|
+
DOC_ADVISORY="${DOC_ADVISORY}Verbose doc comment in $FILE_PATH — docs must be present but concise:"$'\n'"${_verbose_doc}"
|
|
30
|
+
fi
|
|
31
|
+
fi
|
|
32
|
+
|