litclaude-ai 0.2.2
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/CHANGELOG.md +155 -0
- package/LICENSE +21 -0
- package/README.md +369 -0
- package/README_ko-KR.md +374 -0
- package/RELEASE_CHECKLIST.md +165 -0
- package/bin/litclaude-ai.js +643 -0
- package/cover.png +0 -0
- package/docs/agents.md +67 -0
- package/docs/hooks.md +134 -0
- package/docs/lsp.md +40 -0
- package/docs/migration.md +209 -0
- package/docs/workflow-compatibility-audit.md +119 -0
- package/generate_cover.py +123 -0
- package/package.json +48 -0
- package/plugins/litclaude/.claude-plugin/plugin.json +25 -0
- package/plugins/litclaude/.lsp.json +13 -0
- package/plugins/litclaude/.mcp.json +9 -0
- package/plugins/litclaude/agents/boulder-executor.md +12 -0
- package/plugins/litclaude/agents/librarian-researcher.md +15 -0
- package/plugins/litclaude/agents/oracle-verifier.md +16 -0
- package/plugins/litclaude/agents/prometheus-planner.md +13 -0
- package/plugins/litclaude/agents/qa-runner.md +16 -0
- package/plugins/litclaude/agents/quality-reviewer.md +17 -0
- package/plugins/litclaude/bin/litclaude-hook.js +110 -0
- package/plugins/litclaude/bin/litclaude-hud.js +271 -0
- package/plugins/litclaude/bin/litclaude-lsp-doctor.js +15 -0
- package/plugins/litclaude/bin/litclaude-mcp.js +70 -0
- package/plugins/litclaude/commands/deep-interview.md +21 -0
- package/plugins/litclaude/commands/dynamic-workflow.md +36 -0
- package/plugins/litclaude/commands/lit-loop.md +40 -0
- package/plugins/litclaude/commands/lit-plan.md +35 -0
- package/plugins/litclaude/commands/litgoal.md +30 -0
- package/plugins/litclaude/commands/review-work.md +35 -0
- package/plugins/litclaude/commands/start-work.md +36 -0
- package/plugins/litclaude/hooks/hooks.json +54 -0
- package/plugins/litclaude/lib/context-pressure.mjs +25 -0
- package/plugins/litclaude/lib/hud-accent-palette.mjs +58 -0
- package/plugins/litclaude/lib/litgoal/cli.mjs +266 -0
- package/plugins/litclaude/lib/litgoal/ledger.mjs +16 -0
- package/plugins/litclaude/lib/litgoal/paths.mjs +7 -0
- package/plugins/litclaude/lib/litgoal/state.mjs +67 -0
- package/plugins/litclaude/lib/mutated-file-paths.mjs +63 -0
- package/plugins/litclaude/lib/start-work-continuation.mjs +99 -0
- package/plugins/litclaude/lib/workflow-check.mjs +83 -0
- package/plugins/litclaude/skills/ai-slop-remover/SKILL.md +142 -0
- package/plugins/litclaude/skills/comment-checker/SKILL.md +55 -0
- package/plugins/litclaude/skills/debugging/SKILL.md +70 -0
- package/plugins/litclaude/skills/debugging/references/methodology/00-setup.md +108 -0
- package/plugins/litclaude/skills/debugging/references/methodology/02-investigate.md +126 -0
- package/plugins/litclaude/skills/debugging/references/methodology/04-oracle-triple.md +106 -0
- package/plugins/litclaude/skills/debugging/references/methodology/05-escalate.md +69 -0
- package/plugins/litclaude/skills/debugging/references/methodology/06-fix.md +116 -0
- package/plugins/litclaude/skills/debugging/references/methodology/08-qa.md +94 -0
- package/plugins/litclaude/skills/debugging/references/methodology/09-cleanup.md +164 -0
- package/plugins/litclaude/skills/debugging/references/methodology/partial-runtime-evidence.md +228 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/bundled-js-binary.md +415 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/go.md +252 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/native-binary.md +484 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/node.md +260 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/python.md +248 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/rust.md +234 -0
- package/plugins/litclaude/skills/debugging/references/tools/ghidra.md +212 -0
- package/plugins/litclaude/skills/debugging/references/tools/playwright-cli.md +194 -0
- package/plugins/litclaude/skills/debugging/references/tools/pwndbg.md +263 -0
- package/plugins/litclaude/skills/debugging/references/tools/pwntools.md +265 -0
- package/plugins/litclaude/skills/deep-interview/SKILL.md +323 -0
- package/plugins/litclaude/skills/deep-interview/scripts/render_progress.py +193 -0
- package/plugins/litclaude/skills/frontend-ui-ux/SKILL.md +62 -0
- package/plugins/litclaude/skills/lit-loop/SKILL.md +144 -0
- package/plugins/litclaude/skills/lit-plan/SKILL.md +125 -0
- package/plugins/litclaude/skills/litgoal/SKILL.md +219 -0
- package/plugins/litclaude/skills/lsp/SKILL.md +63 -0
- package/plugins/litclaude/skills/programming/SKILL.md +106 -0
- package/plugins/litclaude/skills/programming/references/go/README.md +90 -0
- package/plugins/litclaude/skills/programming/references/go/backend-stack.md +641 -0
- package/plugins/litclaude/skills/programming/references/go/bootstrap.md +328 -0
- package/plugins/litclaude/skills/programming/references/go/bubbletea-v2.md +360 -0
- package/plugins/litclaude/skills/programming/references/go/cobra-stack.md +468 -0
- package/plugins/litclaude/skills/programming/references/go/concurrency.md +362 -0
- package/plugins/litclaude/skills/programming/references/go/data-modeling.md +329 -0
- package/plugins/litclaude/skills/programming/references/go/error-handling.md +359 -0
- package/plugins/litclaude/skills/programming/references/go/golangci-strict.md +236 -0
- package/plugins/litclaude/skills/programming/references/go/grpc-connect.md +375 -0
- package/plugins/litclaude/skills/programming/references/go/libraries.md +337 -0
- package/plugins/litclaude/skills/programming/references/go/one-liners.md +202 -0
- package/plugins/litclaude/skills/programming/references/go/sqlc-pgx.md +471 -0
- package/plugins/litclaude/skills/programming/references/go/testing.md +467 -0
- package/plugins/litclaude/skills/programming/references/go/type-patterns.md +298 -0
- package/plugins/litclaude/skills/programming/references/python/README.md +314 -0
- package/plugins/litclaude/skills/programming/references/python/async-anyio.md +442 -0
- package/plugins/litclaude/skills/programming/references/python/data-modeling.md +233 -0
- package/plugins/litclaude/skills/programming/references/python/data-processing.md +133 -0
- package/plugins/litclaude/skills/programming/references/python/error-handling.md +218 -0
- package/plugins/litclaude/skills/programming/references/python/fastapi-stack.md +316 -0
- package/plugins/litclaude/skills/programming/references/python/httpx2-optimization.md +360 -0
- package/plugins/litclaude/skills/programming/references/python/libraries.md +307 -0
- package/plugins/litclaude/skills/programming/references/python/one-liners.md +268 -0
- package/plugins/litclaude/skills/programming/references/python/orjson-stack.md +378 -0
- package/plugins/litclaude/skills/programming/references/python/pydantic-ai.md +285 -0
- package/plugins/litclaude/skills/programming/references/python/pyproject-strict.md +232 -0
- package/plugins/litclaude/skills/programming/references/python/textual-tui.md +201 -0
- package/plugins/litclaude/skills/programming/references/python/type-patterns.md +176 -0
- package/plugins/litclaude/skills/programming/references/rust/README.md +317 -0
- package/plugins/litclaude/skills/programming/references/rust/async-tokio.md +299 -0
- package/plugins/litclaude/skills/programming/references/rust/axum-stack.md +467 -0
- package/plugins/litclaude/skills/programming/references/rust/cargo-strict.md +317 -0
- package/plugins/litclaude/skills/programming/references/rust/clap-stack.md +409 -0
- package/plugins/litclaude/skills/programming/references/rust/concurrency.md +375 -0
- package/plugins/litclaude/skills/programming/references/rust/libraries.md +439 -0
- package/plugins/litclaude/skills/programming/references/rust/one-liners.md +291 -0
- package/plugins/litclaude/skills/programming/references/rust/proptest-insta.md +429 -0
- package/plugins/litclaude/skills/programming/references/rust/type-state.md +354 -0
- package/plugins/litclaude/skills/programming/references/rust/unsafe-discipline.md +250 -0
- package/plugins/litclaude/skills/programming/references/rust/zero-cost-safety.md +527 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/README.md +289 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/miri-sanitizers-loom.md +411 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/ub-taxonomy.md +269 -0
- package/plugins/litclaude/skills/programming/references/typescript/README.md +195 -0
- package/plugins/litclaude/skills/programming/references/typescript/backend-hono.md +672 -0
- package/plugins/litclaude/skills/programming/references/typescript/bootstrap.md +199 -0
- package/plugins/litclaude/skills/programming/references/typescript/data-modeling.md +202 -0
- package/plugins/litclaude/skills/programming/references/typescript/error-handling.md +169 -0
- package/plugins/litclaude/skills/programming/references/typescript/tsconfig-strict.md +152 -0
- package/plugins/litclaude/skills/programming/references/typescript/type-patterns.md +196 -0
- package/plugins/litclaude/skills/programming/scripts/go/check-no-excuse-rules.sh +173 -0
- package/plugins/litclaude/skills/programming/scripts/go/new-project.py +138 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/.editorconfig +13 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/.golangci.yml +95 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/AGENTS.md.tmpl +24 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/README.md.tmpl +12 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/Taskfile.yml +40 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/ci.yml +37 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/config.go +24 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/gitignore +15 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/main.go.tmpl +22 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/run.go +15 -0
- package/plugins/litclaude/skills/programming/scripts/python/check-no-excuse-rules.py +687 -0
- package/plugins/litclaude/skills/programming/scripts/python/new-project.py +172 -0
- package/plugins/litclaude/skills/programming/scripts/python/new-script.py +116 -0
- package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.py +296 -0
- package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.sh +158 -0
- package/plugins/litclaude/skills/programming/scripts/rust/new-project.py +175 -0
- package/plugins/litclaude/skills/programming/scripts/typescript/check-no-excuse-rules.ts +282 -0
- package/plugins/litclaude/skills/programming/scripts/typescript/new-project.ts +177 -0
- package/plugins/litclaude/skills/refactor/SKILL.md +73 -0
- package/plugins/litclaude/skills/remove-ai-slops/SKILL.md +52 -0
- package/plugins/litclaude/skills/review-work/SKILL.md +331 -0
- package/plugins/litclaude/skills/rules/SKILL.md +66 -0
- package/plugins/litclaude/skills/start-work/SKILL.md +132 -0
- package/scripts/audit-plan-checkboxes.mjs +37 -0
- package/scripts/doctor.mjs +41 -0
- package/scripts/inspect-agent-tools.mjs +27 -0
- package/scripts/postinstall.mjs +50 -0
- package/scripts/qa-claude-plugin-smoke.sh +60 -0
- package/scripts/qa-portable-install.sh +136 -0
- package/scripts/validate-plugin.mjs +72 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# No-excuse rule checker for Rust files.
|
|
3
|
+
# Mirrors the philosophy of python-programmer / typescript-programmer scripts:
|
|
4
|
+
# only rules that can be enforced via pure text matching live here.
|
|
5
|
+
# Everything semantic is on clippy + miri + nextest.
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
if [ $# -eq 0 ]; then
|
|
10
|
+
echo "Usage: $0 <file.rs> [file.rs ...]" >&2
|
|
11
|
+
exit 2
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
violations=0
|
|
15
|
+
report() {
|
|
16
|
+
local file="$1"
|
|
17
|
+
local line="$2"
|
|
18
|
+
local rule="$3"
|
|
19
|
+
local detail="$4"
|
|
20
|
+
echo "::error file=${file},line=${line}::[${rule}] ${detail}" >&2
|
|
21
|
+
violations=$((violations + 1))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
is_test_path() {
|
|
25
|
+
local path="$1"
|
|
26
|
+
case "$path" in
|
|
27
|
+
*/tests/*|*/benches/*|*/examples/*|*/build.rs|*_test.rs|tests/*|benches/*|examples/*) return 0 ;;
|
|
28
|
+
esac
|
|
29
|
+
# In-file #[cfg(test)] modules are handled per-line below.
|
|
30
|
+
return 1
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for file in "$@"; do
|
|
34
|
+
[ -f "$file" ] || continue
|
|
35
|
+
case "$file" in
|
|
36
|
+
*.rs) ;;
|
|
37
|
+
*) continue ;;
|
|
38
|
+
esac
|
|
39
|
+
|
|
40
|
+
if is_test_path "$file"; then
|
|
41
|
+
# Test files are exempt from unwrap/expect/todo rules.
|
|
42
|
+
# Still enforce unsafe-comment, allow-comment, panic-in-lib rules below
|
|
43
|
+
# by setting a marker - keeping the loop unified.
|
|
44
|
+
in_test_file=1
|
|
45
|
+
else
|
|
46
|
+
in_test_file=0
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Track #[cfg(test)] regions for per-line exemptions.
|
|
50
|
+
in_cfg_test=0
|
|
51
|
+
cfg_test_brace_depth=0
|
|
52
|
+
line_no=0
|
|
53
|
+
|
|
54
|
+
while IFS= read -r raw_line || [ -n "$raw_line" ]; do
|
|
55
|
+
line_no=$((line_no + 1))
|
|
56
|
+
line="$raw_line"
|
|
57
|
+
|
|
58
|
+
# Crude #[cfg(test)] region tracker: when we see #[cfg(test)] on a
|
|
59
|
+
# line followed by a mod with `{`, count braces until depth returns
|
|
60
|
+
# to zero. This is approximate but matches typical formatting.
|
|
61
|
+
if [[ "$line" =~ \#\[cfg\(test\)\] ]]; then
|
|
62
|
+
in_cfg_test=1
|
|
63
|
+
cfg_test_brace_depth=0
|
|
64
|
+
fi
|
|
65
|
+
if [ "$in_cfg_test" -eq 1 ]; then
|
|
66
|
+
opens=$(printf '%s' "$line" | tr -cd '{' | wc -c)
|
|
67
|
+
closes=$(printf '%s' "$line" | tr -cd '}' | wc -c)
|
|
68
|
+
cfg_test_brace_depth=$((cfg_test_brace_depth + opens - closes))
|
|
69
|
+
if [ "$cfg_test_brace_depth" -le 0 ] && [[ ! "$line" =~ \#\[cfg\(test\)\] ]]; then
|
|
70
|
+
in_cfg_test=0
|
|
71
|
+
fi
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
exempt=0
|
|
75
|
+
[ "$in_test_file" -eq 1 ] && exempt=1
|
|
76
|
+
[ "$in_cfg_test" -eq 1 ] && exempt=1
|
|
77
|
+
|
|
78
|
+
# Strip line comments before pattern checks - so doc comments and
|
|
79
|
+
# explanatory prose do not trip the regexes.
|
|
80
|
+
code_only="${line%%//*}"
|
|
81
|
+
|
|
82
|
+
if [ "$exempt" -eq 0 ]; then
|
|
83
|
+
# .unwrap()
|
|
84
|
+
if [[ "$code_only" =~ \.unwrap\(\) ]]; then
|
|
85
|
+
# Allow if previous line had // SAFE-UNWRAP: comment
|
|
86
|
+
prev_line=$(sed -n "$((line_no - 1))p" "$file" 2>/dev/null || true)
|
|
87
|
+
if [[ ! "$prev_line" =~ //[[:space:]]*SAFE-UNWRAP: ]]; then
|
|
88
|
+
report "$file" "$line_no" "unwrap" ".unwrap() outside tests - use ? / ok_or / pattern match or annotate previous line with // SAFE-UNWRAP: <reason>"
|
|
89
|
+
fi
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# .expect("...")
|
|
93
|
+
if [[ "$code_only" =~ \.expect\( ]]; then
|
|
94
|
+
prev_line=$(sed -n "$((line_no - 1))p" "$file" 2>/dev/null || true)
|
|
95
|
+
if [[ ! "$prev_line" =~ //[[:space:]]*SAFE-EXPECT: ]]; then
|
|
96
|
+
report "$file" "$line_no" "expect" ".expect() outside tests - use ? or annotate previous line with // SAFE-EXPECT: <reason>"
|
|
97
|
+
fi
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# todo!() / unimplemented!() / unreachable!()
|
|
101
|
+
if [[ "$code_only" =~ (todo!|unimplemented!|unreachable!|unreachable_unchecked!) ]]; then
|
|
102
|
+
report "$file" "$line_no" "placeholder-macro" "todo!/unimplemented!/unreachable! in committed code"
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# Box<dyn Error
|
|
106
|
+
if [[ "$code_only" =~ Box\<dyn[[:space:]]+Error ]]; then
|
|
107
|
+
report "$file" "$line_no" "box-dyn-error" "Box<dyn Error> in non-test code - use anyhow::Error (apps) or thiserror enum (libs)"
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# panic!( in lib
|
|
111
|
+
if [[ "$file" == */src/lib.rs || "$file" == */src/*/mod.rs || ( "$file" == */src/*.rs && "$file" != */src/main.rs && "$file" != */src/bin/* ) ]]; then
|
|
112
|
+
if [[ "$code_only" =~ panic!\( ]]; then
|
|
113
|
+
report "$file" "$line_no" "lib-panic" "panic!() in library code - return Result"
|
|
114
|
+
fi
|
|
115
|
+
fi
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
# unsafe { without preceding // SAFETY: in the last 5 lines (always enforced)
|
|
119
|
+
if [[ "$code_only" =~ unsafe[[:space:]]*\{ ]]; then
|
|
120
|
+
start=$((line_no > 5 ? line_no - 5 : 1))
|
|
121
|
+
window=$(sed -n "${start},${line_no}p" "$file" 2>/dev/null || true)
|
|
122
|
+
if [[ ! "$window" =~ //[[:space:]]*SAFETY: ]]; then
|
|
123
|
+
report "$file" "$line_no" "unsafe-no-safety-comment" "unsafe block without // SAFETY: comment in preceding 5 lines"
|
|
124
|
+
fi
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
# #[allow(clippy::...)] without preceding // CLIPPY-ALLOW: justification
|
|
128
|
+
if [[ "$code_only" =~ \#\[allow\(clippy:: ]]; then
|
|
129
|
+
prev_line=$(sed -n "$((line_no - 1))p" "$file" 2>/dev/null || true)
|
|
130
|
+
if [[ ! "$prev_line" =~ //[[:space:]]*CLIPPY-ALLOW: ]]; then
|
|
131
|
+
report "$file" "$line_no" "unjustified-clippy-allow" "#[allow(clippy::...)] without // CLIPPY-ALLOW: <reason> on previous line"
|
|
132
|
+
fi
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
# Narrowing numeric `as` casts - heuristic flag for human review.
|
|
136
|
+
# Catches the common shapes; precise type analysis belongs to clippy::cast_possible_truncation.
|
|
137
|
+
if [[ "$code_only" =~ as[[:space:]]+(u8|u16|u32|i8|i16|i32) ]] && \
|
|
138
|
+
[[ "$code_only" =~ (u16|u32|u64|u128|usize|i16|i32|i64|i128|isize)[[:space:]]+as[[:space:]]+(u8|u16|u32|i8|i16|i32) ]]; then
|
|
139
|
+
report "$file" "$line_no" "narrowing-as-cast" "possible narrowing 'as' cast - use TryFrom / try_into() for fallible conversion"
|
|
140
|
+
fi
|
|
141
|
+
done < "$file"
|
|
142
|
+
done
|
|
143
|
+
|
|
144
|
+
if [ "$violations" -gt 0 ]; then
|
|
145
|
+
echo "" >&2
|
|
146
|
+
echo "rust-programmer: ${violations} violation(s). Fix before declaring work done." >&2
|
|
147
|
+
echo "" >&2
|
|
148
|
+
echo "Then run the full toolchain gate:" >&2
|
|
149
|
+
echo " cargo +stable fmt --all -- --check" >&2
|
|
150
|
+
echo " cargo +stable clippy --all-targets --all-features -- -D warnings" >&2
|
|
151
|
+
echo " cargo nextest run --all-targets --all-features" >&2
|
|
152
|
+
echo " cargo +nightly miri nextest run --all-features # if unsafe touched" >&2
|
|
153
|
+
echo " cargo machete" >&2
|
|
154
|
+
echo " cargo deny check" >&2
|
|
155
|
+
exit 1
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
echo "rust-programmer: no-excuse rules passed for $# file(s)."
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run --script
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# dependencies = [
|
|
5
|
+
# "typer",
|
|
6
|
+
# "rich",
|
|
7
|
+
# ]
|
|
8
|
+
# ///
|
|
9
|
+
|
|
10
|
+
# ─── How to run ───
|
|
11
|
+
# 1. Install uv (if not installed):
|
|
12
|
+
# curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
13
|
+
# 2. Run:
|
|
14
|
+
# uv run new-project.py myproject
|
|
15
|
+
# uv run new-project.py myproject --path ./workspace
|
|
16
|
+
# ──────────────────
|
|
17
|
+
#
|
|
18
|
+
# Creates a new Rust project with strict lints, deny.toml, rustfmt.toml,
|
|
19
|
+
# rust-toolchain.toml, and .cargo/config.toml pre-configured.
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
import typer
|
|
28
|
+
from rich.console import Console
|
|
29
|
+
|
|
30
|
+
console = Console(stderr=True)
|
|
31
|
+
|
|
32
|
+
# ── Embedded config contents ─────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
RUST_TOOLCHAIN_TOML = """\
|
|
35
|
+
[toolchain]
|
|
36
|
+
channel = "stable"
|
|
37
|
+
components = ["rustfmt", "clippy", "rust-src"]
|
|
38
|
+
profile = "default"
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
CARGO_TOML_LINTS = """
|
|
42
|
+
[lints.rust]
|
|
43
|
+
unsafe_op_in_unsafe_fn = "deny"
|
|
44
|
+
missing_docs = "warn"
|
|
45
|
+
missing_debug_implementations = "warn"
|
|
46
|
+
unreachable_pub = "warn"
|
|
47
|
+
unused_must_use = "deny"
|
|
48
|
+
elided_lifetimes_in_paths = "warn"
|
|
49
|
+
non_ascii_idents = "deny"
|
|
50
|
+
trivial_numeric_casts = "warn"
|
|
51
|
+
unused_lifetimes = "warn"
|
|
52
|
+
single_use_lifetimes = "warn"
|
|
53
|
+
|
|
54
|
+
[lints.clippy]
|
|
55
|
+
all = { level = "deny", priority = -1 }
|
|
56
|
+
pedantic = { level = "warn", priority = -1 }
|
|
57
|
+
nursery = { level = "warn", priority = -1 }
|
|
58
|
+
cargo = { level = "warn", priority = -1 }
|
|
59
|
+
undocumented_unsafe_blocks = "deny"
|
|
60
|
+
multiple_unsafe_ops_per_block = "deny"
|
|
61
|
+
unwrap_used = "deny"
|
|
62
|
+
expect_used = "deny"
|
|
63
|
+
panic = "deny"
|
|
64
|
+
todo = "deny"
|
|
65
|
+
unimplemented = "deny"
|
|
66
|
+
dbg_macro = "deny"
|
|
67
|
+
print_stdout = "warn"
|
|
68
|
+
print_stderr = "warn"
|
|
69
|
+
module_name_repetitions = { level = "allow" }
|
|
70
|
+
must_use_candidate = { level = "allow" }
|
|
71
|
+
missing_errors_doc = { level = "allow" }
|
|
72
|
+
missing_panics_doc = { level = "allow" }
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
CARGO_CONFIG_TOML = """\
|
|
76
|
+
[build]
|
|
77
|
+
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
|
|
78
|
+
|
|
79
|
+
[target.x86_64-unknown-linux-gnu]
|
|
80
|
+
linker = "clang"
|
|
81
|
+
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
|
|
82
|
+
|
|
83
|
+
[target.aarch64-apple-darwin]
|
|
84
|
+
rustflags = []
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
DENY_TOML = """\
|
|
88
|
+
[advisories]
|
|
89
|
+
vulnerability = "deny"
|
|
90
|
+
unmaintained = "warn"
|
|
91
|
+
yanked = "deny"
|
|
92
|
+
|
|
93
|
+
[licenses]
|
|
94
|
+
unlicensed = "deny"
|
|
95
|
+
allow = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", "Unicode-3.0", "Zlib"]
|
|
96
|
+
|
|
97
|
+
[bans]
|
|
98
|
+
multiple-versions = "warn"
|
|
99
|
+
wildcards = "deny"
|
|
100
|
+
|
|
101
|
+
[sources]
|
|
102
|
+
unknown-registry = "deny"
|
|
103
|
+
unknown-git = "deny"
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
RUSTFMT_TOML = """\
|
|
107
|
+
edition = "2024"
|
|
108
|
+
max_width = 100
|
|
109
|
+
use_field_init_shorthand = true
|
|
110
|
+
use_try_shorthand = true
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
# ── Main ─────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
app = typer.Typer(add_completion=False)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@app.command()
|
|
119
|
+
def main(
|
|
120
|
+
name: str = typer.Argument(help="Name of the new Rust project"),
|
|
121
|
+
path: Path = typer.Option(
|
|
122
|
+
Path.cwd(),
|
|
123
|
+
"--path",
|
|
124
|
+
"-p",
|
|
125
|
+
help="Parent directory where the project folder is created",
|
|
126
|
+
),
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Scaffold a new Rust project with strict lints and tooling configs."""
|
|
129
|
+
project_dir = path / name
|
|
130
|
+
|
|
131
|
+
# ── cargo init ───────────────────────────────────────────────────
|
|
132
|
+
console.print(f"[bold green]Creating[/] project [cyan]{name}[/] at [dim]{project_dir}[/]")
|
|
133
|
+
try:
|
|
134
|
+
subprocess.run(
|
|
135
|
+
["cargo", "init", str(project_dir), "--name", name],
|
|
136
|
+
check=True,
|
|
137
|
+
capture_output=True,
|
|
138
|
+
text=True,
|
|
139
|
+
)
|
|
140
|
+
except FileNotFoundError:
|
|
141
|
+
console.print("[bold red]Error:[/] cargo not found. Install Rust via https://rustup.rs")
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
except subprocess.CalledProcessError as exc:
|
|
144
|
+
console.print(f"[bold red]cargo init failed:[/]\n{exc.stderr}")
|
|
145
|
+
sys.exit(1)
|
|
146
|
+
|
|
147
|
+
# ── rust-toolchain.toml ──────────────────────────────────────────
|
|
148
|
+
(project_dir / "rust-toolchain.toml").write_text(RUST_TOOLCHAIN_TOML)
|
|
149
|
+
console.print(" [dim]wrote[/] rust-toolchain.toml")
|
|
150
|
+
|
|
151
|
+
# ── Append [lints] to Cargo.toml ─────────────────────────────────
|
|
152
|
+
cargo_toml = project_dir / "Cargo.toml"
|
|
153
|
+
with cargo_toml.open("a") as f:
|
|
154
|
+
f.write(CARGO_TOML_LINTS)
|
|
155
|
+
console.print(" [dim]appended[/] [lints] to Cargo.toml")
|
|
156
|
+
|
|
157
|
+
# ── .cargo/config.toml ───────────────────────────────────────────
|
|
158
|
+
cargo_config_dir = project_dir / ".cargo"
|
|
159
|
+
cargo_config_dir.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
(cargo_config_dir / "config.toml").write_text(CARGO_CONFIG_TOML)
|
|
161
|
+
console.print(" [dim]wrote[/] .cargo/config.toml")
|
|
162
|
+
|
|
163
|
+
# ── deny.toml ────────────────────────────────────────────────────
|
|
164
|
+
(project_dir / "deny.toml").write_text(DENY_TOML)
|
|
165
|
+
console.print(" [dim]wrote[/] deny.toml")
|
|
166
|
+
|
|
167
|
+
# ── rustfmt.toml ─────────────────────────────────────────────────
|
|
168
|
+
(project_dir / "rustfmt.toml").write_text(RUSTFMT_TOML)
|
|
169
|
+
console.print(" [dim]wrote[/] rustfmt.toml")
|
|
170
|
+
|
|
171
|
+
console.print(f"\n[bold green]Done![/] cd {project_dir} && cargo check")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
if __name__ == "__main__":
|
|
175
|
+
app()
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Check TypeScript files for no-excuse violations.
|
|
4
|
+
*
|
|
5
|
+
* Rules:
|
|
6
|
+
* no-any-assertion - `as any`
|
|
7
|
+
* no-unknown-assertion - `as unknown`
|
|
8
|
+
* no-ts-ignore - `@ts-ignore` comments
|
|
9
|
+
* no-ts-expect-error - `@ts-expect-error` comments
|
|
10
|
+
* no-enum - `enum` declarations
|
|
11
|
+
* no-non-null-assertion - `x!` postfix operator
|
|
12
|
+
* no-throw-literal - `throw "string"` / `throw 123`
|
|
13
|
+
* no-mutable-export - `export let` / `export var`
|
|
14
|
+
* no-any-annotation - `: any` in annotations (opt out: `// no-excuse-ok: any`)
|
|
15
|
+
* no-explicit-any-return - `(): any` return types (opt out: `// no-excuse-ok: any`)
|
|
16
|
+
* empty-catch - `catch { }` or `catch (e) { }` with empty body
|
|
17
|
+
* catch-without-narrowing - catch block that uses error without instanceof narrowing
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* bun run scripts/check-no-excuse-rules.ts <file-or-dir>...
|
|
21
|
+
*
|
|
22
|
+
* Exit codes:
|
|
23
|
+
* 0 - no violations
|
|
24
|
+
* 1 - violations found
|
|
25
|
+
* 2 - input error
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import fs from "node:fs"
|
|
29
|
+
import path from "node:path"
|
|
30
|
+
import process from "node:process"
|
|
31
|
+
import ts from "typescript"
|
|
32
|
+
|
|
33
|
+
type RuleId =
|
|
34
|
+
| "no-any-assertion"
|
|
35
|
+
| "no-unknown-assertion"
|
|
36
|
+
| "no-ts-ignore"
|
|
37
|
+
| "no-ts-expect-error"
|
|
38
|
+
| "no-enum"
|
|
39
|
+
| "no-non-null-assertion"
|
|
40
|
+
| "no-throw-literal"
|
|
41
|
+
| "no-mutable-export"
|
|
42
|
+
| "no-any-annotation"
|
|
43
|
+
| "no-explicit-any-return"
|
|
44
|
+
| "empty-catch"
|
|
45
|
+
| "catch-without-narrowing"
|
|
46
|
+
|
|
47
|
+
type Violation = {
|
|
48
|
+
readonly ruleId: RuleId
|
|
49
|
+
readonly filePath: string
|
|
50
|
+
readonly line: number
|
|
51
|
+
readonly column: number
|
|
52
|
+
readonly message: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const INCLUDED_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts"])
|
|
56
|
+
const IGNORED_DIRECTORIES = new Set([
|
|
57
|
+
".git", ".next", ".nuxt", ".turbo", ".yarn",
|
|
58
|
+
"coverage", "dist", "build", "node_modules",
|
|
59
|
+
])
|
|
60
|
+
|
|
61
|
+
const OPT_OUT_RE = /\/\/\s*no-excuse-ok:\s*any/
|
|
62
|
+
const CATCH_OK_RE = /\/\/\s*no-excuse-ok:\s*catch/
|
|
63
|
+
|
|
64
|
+
function isIncludedFile(filePath: string): boolean {
|
|
65
|
+
return INCLUDED_EXTENSIONS.has(path.extname(filePath).toLowerCase())
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isDeclarationFile(filePath: string): boolean {
|
|
69
|
+
return filePath.endsWith(".d.ts") || filePath.endsWith(".d.mts") || filePath.endsWith(".d.cts")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function discoverFiles(inputs: string[]): string[] {
|
|
73
|
+
const files: string[] = []
|
|
74
|
+
for (const input of inputs) {
|
|
75
|
+
const resolved = path.resolve(input)
|
|
76
|
+
if (!fs.existsSync(resolved)) {
|
|
77
|
+
console.error(`Path does not exist: ${resolved}`)
|
|
78
|
+
process.exit(2)
|
|
79
|
+
}
|
|
80
|
+
if (fs.statSync(resolved).isFile()) {
|
|
81
|
+
if (isIncludedFile(resolved) && !isDeclarationFile(resolved)) files.push(resolved)
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
const walk = (dir: string): void => {
|
|
85
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
86
|
+
if (entry.isDirectory()) {
|
|
87
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) walk(path.join(dir, entry.name))
|
|
88
|
+
} else if (isIncludedFile(entry.name) && !isDeclarationFile(entry.name)) {
|
|
89
|
+
files.push(path.join(dir, entry.name))
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
walk(resolved)
|
|
94
|
+
}
|
|
95
|
+
return files
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getLineText(sourceFile: ts.SourceFile, line: number): string {
|
|
99
|
+
const lineStarts = sourceFile.getLineStarts()
|
|
100
|
+
const start = lineStarts[line]
|
|
101
|
+
const end = line + 1 < lineStarts.length ? lineStarts[line + 1] : sourceFile.getEnd()
|
|
102
|
+
return sourceFile.text.slice(start, end)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function analyzeFile(filePath: string): Violation[] {
|
|
106
|
+
const source = fs.readFileSync(filePath, "utf-8")
|
|
107
|
+
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true)
|
|
108
|
+
const violations: Violation[] = []
|
|
109
|
+
|
|
110
|
+
function pos(node: ts.Node): { line: number; column: number } {
|
|
111
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
|
|
112
|
+
return { line: line + 1, column: character + 1 }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function lineHasOptOut(node: ts.Node): boolean {
|
|
116
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
|
|
117
|
+
return OPT_OUT_RE.test(getLineText(sourceFile, line))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function visit(node: ts.Node): void {
|
|
121
|
+
// ── as any / as unknown ──
|
|
122
|
+
if (ts.isAsExpression(node)) {
|
|
123
|
+
const typeText = node.type.getText(sourceFile)
|
|
124
|
+
if (typeText === "any") {
|
|
125
|
+
const p = pos(node)
|
|
126
|
+
violations.push({ ruleId: "no-any-assertion", filePath, ...p, message: "`as any` — narrow with type guards or redesign the types" })
|
|
127
|
+
}
|
|
128
|
+
if (typeText === "unknown") {
|
|
129
|
+
const p = pos(node)
|
|
130
|
+
violations.push({ ruleId: "no-unknown-assertion", filePath, ...p, message: "`as unknown` — redesign the types" })
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── enum ──
|
|
135
|
+
if (ts.isEnumDeclaration(node)) {
|
|
136
|
+
const p = pos(node)
|
|
137
|
+
violations.push({ ruleId: "no-enum", filePath, ...p, message: "`enum` — use `as const` object + literal union type" })
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── x! non-null assertion ──
|
|
141
|
+
if (ts.isNonNullExpression(node)) {
|
|
142
|
+
const p = pos(node)
|
|
143
|
+
violations.push({ ruleId: "no-non-null-assertion", filePath, ...p, message: "`x!` — use narrowing or optional chaining" })
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── throw "literal" ──
|
|
147
|
+
if (ts.isThrowStatement(node) && node.expression) {
|
|
148
|
+
const expr = node.expression
|
|
149
|
+
if (ts.isStringLiteral(expr) || ts.isNumericLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
|
|
150
|
+
const p = pos(node)
|
|
151
|
+
violations.push({ ruleId: "no-throw-literal", filePath, ...p, message: "`throw literal` — throw an Error subclass" })
|
|
152
|
+
}
|
|
153
|
+
if (ts.isTemplateExpression(expr)) {
|
|
154
|
+
const p = pos(node)
|
|
155
|
+
violations.push({ ruleId: "no-throw-literal", filePath, ...p, message: "`throw template` — throw an Error subclass" })
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── export let / export var ──
|
|
160
|
+
if (ts.isVariableStatement(node)) {
|
|
161
|
+
const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
|
|
162
|
+
if (hasExport) {
|
|
163
|
+
const flags = node.declarationList.flags
|
|
164
|
+
if (!(flags & ts.NodeFlags.Const)) {
|
|
165
|
+
const p = pos(node)
|
|
166
|
+
violations.push({ ruleId: "no-mutable-export", filePath, ...p, message: "`export let/var` — use `export const`" })
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── : any in annotations ──
|
|
172
|
+
if (ts.isTypeReferenceNode(node) || node.kind === ts.SyntaxKind.AnyKeyword) {
|
|
173
|
+
if (node.kind === ts.SyntaxKind.AnyKeyword && !lineHasOptOut(node)) {
|
|
174
|
+
const parent = node.parent
|
|
175
|
+
// Skip `as any` — already caught by no-any-assertion
|
|
176
|
+
if (parent && ts.isAsExpression(parent)) {
|
|
177
|
+
// already handled
|
|
178
|
+
} else if (parent && (
|
|
179
|
+
ts.isParameter(parent) ||
|
|
180
|
+
ts.isVariableDeclaration(parent) ||
|
|
181
|
+
ts.isPropertyDeclaration(parent) ||
|
|
182
|
+
ts.isPropertySignature(parent)
|
|
183
|
+
)) {
|
|
184
|
+
const p = pos(node)
|
|
185
|
+
violations.push({ ruleId: "no-any-annotation", filePath, ...p, message: "`: any` annotation — use `unknown` and narrow" })
|
|
186
|
+
} else if (parent && (
|
|
187
|
+
ts.isFunctionDeclaration(parent) ||
|
|
188
|
+
ts.isMethodDeclaration(parent) ||
|
|
189
|
+
ts.isArrowFunction(parent) ||
|
|
190
|
+
ts.isFunctionExpression(parent)
|
|
191
|
+
)) {
|
|
192
|
+
const p = pos(node)
|
|
193
|
+
violations.push({ ruleId: "no-explicit-any-return", filePath, ...p, message: "`(): any` return — use a specific type" })
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── empty catch / catch without narrowing ──
|
|
199
|
+
if (ts.isCatchClause(node)) {
|
|
200
|
+
const catchLine = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line
|
|
201
|
+
const catchLineText = getLineText(sourceFile, catchLine)
|
|
202
|
+
if (!CATCH_OK_RE.test(catchLineText)) {
|
|
203
|
+
const body = node.block
|
|
204
|
+
const stmts = body.statements
|
|
205
|
+
|
|
206
|
+
if (stmts.length === 0) {
|
|
207
|
+
// Empty catch — swallows everything silently
|
|
208
|
+
const p = pos(node)
|
|
209
|
+
violations.push({ ruleId: "empty-catch", filePath, ...p, message: "empty `catch` block — handle, re-throw, or remove the try/catch" })
|
|
210
|
+
} else if (node.variableDeclaration) {
|
|
211
|
+
// Has a bound variable — check if it's narrowed with instanceof
|
|
212
|
+
const varName = node.variableDeclaration.name.getText(sourceFile)
|
|
213
|
+
const blockText = body.getText(sourceFile)
|
|
214
|
+
const hasInstanceof = blockText.includes(`instanceof`)
|
|
215
|
+
const hasRethrow = blockText.includes(`throw ${varName}`) || blockText.includes(`throw new`)
|
|
216
|
+
if (!hasInstanceof && !hasRethrow) {
|
|
217
|
+
const p = pos(node)
|
|
218
|
+
violations.push({ ruleId: "catch-without-narrowing", filePath, ...p, message: "`catch` without `instanceof` narrowing or re-throw — narrow the error type or re-throw" })
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
ts.forEachChild(node, visit)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
visit(sourceFile)
|
|
228
|
+
|
|
229
|
+
// ── @ts-ignore / @ts-expect-error in comments ──
|
|
230
|
+
const commentRanges = [
|
|
231
|
+
...(ts.getLeadingCommentRanges(source, 0) ?? []),
|
|
232
|
+
]
|
|
233
|
+
// Scan all comments via regex for reliability
|
|
234
|
+
const commentRegex = /\/\/\s*@ts-(ignore|expect-error)/g
|
|
235
|
+
let match: RegExpExecArray | null
|
|
236
|
+
while ((match = commentRegex.exec(source)) !== null) {
|
|
237
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(match.index)
|
|
238
|
+
const kind = match[1]
|
|
239
|
+
violations.push({
|
|
240
|
+
ruleId: kind === "ignore" ? "no-ts-ignore" : "no-ts-expect-error",
|
|
241
|
+
filePath,
|
|
242
|
+
line: line + 1,
|
|
243
|
+
column: character + 1,
|
|
244
|
+
message: `\`@ts-${kind}\` — fix the underlying type`,
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return violations
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function formatViolation(v: Violation): string {
|
|
252
|
+
return `${v.filePath}:${v.line}:${v.column}: [${v.ruleId}] ${v.message}`
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function main(): void {
|
|
256
|
+
const args = process.argv.slice(2)
|
|
257
|
+
if (args.length === 0) {
|
|
258
|
+
console.error("usage: check-no-excuse-rules.ts <file-or-dir>...")
|
|
259
|
+
process.exit(2)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const files = discoverFiles(args)
|
|
263
|
+
if (files.length === 0) {
|
|
264
|
+
console.error("No TypeScript files found.")
|
|
265
|
+
process.exit(2)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const violations = files.flatMap((f) => analyzeFile(f))
|
|
269
|
+
|
|
270
|
+
if (violations.length === 0) {
|
|
271
|
+
console.log(`No violations in ${files.length} file(s).`)
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
for (const v of violations) {
|
|
276
|
+
console.error(formatViolation(v))
|
|
277
|
+
}
|
|
278
|
+
console.error(`\n${violations.length} violation(s) in ${files.length} file(s).`)
|
|
279
|
+
process.exit(1)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
main()
|