rubocop-rspec-unused-let 1.1.0 → 1.2.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.
- checksums.yaml +4 -4
- data/.claude/hooks/pre-commit-check.sh +42 -2
- data/.claude/hooks/protect-sig-files.sh +32 -17
- data/.claude/hooks/self-review.sh +66 -35
- data/.claude/settings.json +10 -10
- data/CHANGELOG.md +43 -0
- data/README.md +326 -28
- data/Rakefile +13 -4
- data/config/default.yml +7 -1
- data/lib/rubocop/cop/rspec/unused_let/external_definitions.rb +120 -0
- data/lib/rubocop/cop/rspec/unused_let/matchers.rb +211 -0
- data/lib/rubocop/cop/rspec/unused_let/references.rb +48 -0
- data/lib/rubocop/cop/rspec/unused_let/scope.rb +126 -0
- data/lib/rubocop/cop/rspec/unused_let/scope_builder.rb +267 -0
- data/lib/rubocop/cop/rspec/unused_let/scope_stack.rb +116 -0
- data/lib/rubocop/cop/rspec/unused_let/shared_example_registry.rb +265 -0
- data/lib/rubocop/cop/rspec/unused_let.rb +194 -123
- data/lib/rubocop/rspec/unused/let/version.rb +1 -1
- data/rbs_collection.lock.yaml +20 -16
- data/rbs_collection.yaml +3 -0
- data/sig/gems/rubocop_rspec_stubs.rbs +70 -0
- data/sig/rubocop/cop/rspec/unused_let/external_definitions.rbs +78 -0
- data/sig/rubocop/cop/rspec/unused_let/matchers.rbs +129 -0
- data/sig/rubocop/cop/rspec/unused_let/references.rbs +30 -0
- data/sig/rubocop/cop/rspec/unused_let/scope.rbs +100 -0
- data/sig/rubocop/cop/rspec/unused_let/scope_builder.rbs +162 -0
- data/sig/rubocop/cop/rspec/unused_let/scope_stack.rbs +84 -0
- data/sig/rubocop/cop/rspec/unused_let/shared_example_registry.rbs +171 -0
- data/sig/rubocop/cop/rspec/unused_let.rbs +134 -61
- metadata +23 -6
- data/sig/rubocop_rspec_stubs.rbs +0 -30
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 724c612abc3f71f86027ff074ddd0c543b58200c7ddc8bfbb717e57308bc6a6d
|
|
4
|
+
data.tar.gz: a70d24faab3cd03e5c85ea53896b784b19a3527ce440809de887696fba2107b0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0eac33bde343620b5648ac00bc61cfb8e48a95692b16fe6f135d679646bb8f07c6f51ab6271da0aacf90b3d2fecd087763ea1bf1c4133cc172a4bff3b2d426fe
|
|
7
|
+
data.tar.gz: 69545def1558284cd78eccd6933edf5fbb1a890065b4b5f959d1736e693f7bac37ee83e70fe65a169f3a2f1bb01e456dd9293cd19b6ff7e49326b4043996fa3e
|
|
@@ -18,12 +18,52 @@ fi
|
|
|
18
18
|
|
|
19
19
|
echo "Running pre-commit checks..." >&2
|
|
20
20
|
|
|
21
|
-
#
|
|
22
|
-
if ! bundle exec rbs
|
|
21
|
+
# Regenerate RBS from scratch so that files orphaned by deleted lib/ sources are dropped too
|
|
22
|
+
if ! bundle exec rake rbs:regenerate >&2; then
|
|
23
23
|
echo "Error: RBS generation failed" >&2
|
|
24
24
|
exit 2
|
|
25
25
|
fi
|
|
26
26
|
|
|
27
|
+
# True when $command stages the whole of sig/ itself, which the check below cannot see because
|
|
28
|
+
# this hook runs first. Staging only part of sig/ does not count - the rest still drops out.
|
|
29
|
+
stages_sig() {
|
|
30
|
+
local segment tokens token stages_everything pathspec_given
|
|
31
|
+
|
|
32
|
+
while IFS= read -r segment; do
|
|
33
|
+
segment=${segment//[\"\']/}
|
|
34
|
+
read -ra tokens <<<"${segment#*add}"
|
|
35
|
+
stages_everything=0
|
|
36
|
+
pathspec_given=0
|
|
37
|
+
|
|
38
|
+
for token in "${tokens[@]}"; do
|
|
39
|
+
case "$token" in
|
|
40
|
+
--) ;;
|
|
41
|
+
-*A*|--all) stages_everything=1 ;;
|
|
42
|
+
-*) ;;
|
|
43
|
+
.|./|sig|sig/|./sig|./sig/) return 0 ;;
|
|
44
|
+
*) pathspec_given=1 ;;
|
|
45
|
+
esac
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
# -A alone stages every path; given a pathspec it stages that path only
|
|
49
|
+
[ "$stages_everything" = 1 ] && [ "$pathspec_given" = 0 ] && return 0
|
|
50
|
+
done < <(grep -oE '(^|[;&|(])[[:space:]]*git[[:space:]]+add[^;&|(]*' <<<"$command")
|
|
51
|
+
|
|
52
|
+
return 1
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Stop rather than let the RBS just regenerated above drop out of the commit unnoticed
|
|
56
|
+
if [[ "$command" =~ git[[:space:]]+commit([[:space:]]|$) ]] && ! stages_sig; then
|
|
57
|
+
unstaged_sig=$(git status --porcelain --untracked-files=all -- sig/ | awk 'substr($0, 2, 1) != " "')
|
|
58
|
+
|
|
59
|
+
if [ -n "$unstaged_sig" ]; then
|
|
60
|
+
echo "Error: sig/ has changes that are not staged:" >&2
|
|
61
|
+
echo "$unstaged_sig" >&2
|
|
62
|
+
echo "Run 'git add sig/' and commit again." >&2
|
|
63
|
+
exit 2
|
|
64
|
+
fi
|
|
65
|
+
fi
|
|
66
|
+
|
|
27
67
|
if ! bundle exec rake >&2; then
|
|
28
68
|
echo "Error: rake checks failed" >&2
|
|
29
69
|
exit 2
|
|
@@ -1,29 +1,44 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# .claude/hooks/protect-sig-files.sh
|
|
3
|
-
#
|
|
4
2
|
# Prevents direct editing of auto-generated RBS type definition files.
|
|
5
|
-
# Type definitions in
|
|
3
|
+
# Type definitions in the project's own sig/ are managed by PostToolUse hooks.
|
|
4
|
+
# Hand-written stubs for third-party gems under sig/gems/ are exempt.
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
# Hook input is JSON from stdin
|
|
7
|
+
input=$(cat)
|
|
8
|
+
tool_name=$(echo "$input" | jq -r '.tool_name // ""')
|
|
10
9
|
|
|
11
10
|
# Block Write/Edit operations on sig/ files
|
|
12
|
-
if [[ "$
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
if [[ "$tool_name" == "Write" || "$tool_name" == "Edit" ]]; then
|
|
12
|
+
file_path=$(echo "$input" | jq -r '.tool_input.file_path // ""')
|
|
13
|
+
|
|
14
|
+
# Judge the path relative to the project root: only the sig/ there is
|
|
15
|
+
# generated. A sig/ nested elsewhere is hand-written and stays editable.
|
|
16
|
+
project_root="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
17
|
+
rel_path="${file_path#"${project_root}/"}"
|
|
18
|
+
|
|
19
|
+
if [[ "$rel_path" == sig/gems/* ]]; then
|
|
20
|
+
exit 0
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
if [[ "$rel_path" == sig/* ]]; then
|
|
24
|
+
echo "❌ ERROR: Direct editing of generated sig/ files is not allowed." >&2
|
|
25
|
+
echo "📝 This repository's own types are generated: fix the inline RBS comments in the .rb file." >&2
|
|
26
|
+
echo "💡 Hand-written files are allowed only under sig/gems/ (stubs for third-party gems)." >&2
|
|
27
|
+
exit 2
|
|
18
28
|
fi
|
|
19
29
|
fi
|
|
20
30
|
|
|
21
31
|
# Block manual rbs-inline execution
|
|
22
|
-
if [[ "$
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
if [[ "$tool_name" == "Bash" ]]; then
|
|
33
|
+
command=$(echo "$input" | jq -r '.tool_input.command // ""')
|
|
34
|
+
|
|
35
|
+
# Match rbs-inline only at a command position, not as an argument.
|
|
36
|
+
rbs_inline_re=$'(^|[;&|(\n])[[:space:]]*(bundle[[:space:]]+exec[[:space:]]+)?([^[:space:]]*/)?rbs-inline([[:space:]]|[;&|)]|$)'
|
|
37
|
+
|
|
38
|
+
if [[ "$command" =~ $rbs_inline_re ]]; then
|
|
39
|
+
echo "❌ ERROR: Manual rbs-inline execution is not allowed." >&2
|
|
40
|
+
echo "🔄 Type definitions are auto-generated by hooks." >&2
|
|
41
|
+
exit 2
|
|
27
42
|
fi
|
|
28
43
|
fi
|
|
29
44
|
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
#
|
|
3
|
-
#
|
|
3
|
+
# PreToolUse hook: review code before `git commit`.
|
|
4
4
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# Fires before a Bash `git commit` command and blocks the first commit attempt
|
|
6
|
+
# for a given change-set, asking Claude to review the code before committing.
|
|
7
|
+
# The review uses the `self-review` skill when available, and falls back to the
|
|
8
|
+
# bundled `code-review` skill otherwise — so the hook works even in projects
|
|
9
|
+
# that don't ship the custom self-review skill.
|
|
8
10
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
11
|
+
# A per-change-set marker (keyed by a hash of the diff, stored under .git/)
|
|
12
|
+
# prevents an infinite block loop: once a change-set has been surfaced for
|
|
13
|
+
# review, the matching commit is allowed through. If the review leads to fixes,
|
|
14
|
+
# the diff changes, so the new state is reviewed once more before it commits.
|
|
13
15
|
#
|
|
14
16
|
|
|
15
17
|
set -euo pipefail
|
|
@@ -20,49 +22,78 @@ input=$(cat)
|
|
|
20
22
|
tool_name=$(echo "$input" | jq -r '.tool_name // empty')
|
|
21
23
|
command=$(echo "$input" | jq -r '.tool_input.command // empty')
|
|
22
24
|
|
|
23
|
-
# Only
|
|
25
|
+
# Only gate the Bash tool running a git commit.
|
|
24
26
|
if [[ "$tool_name" != "Bash" ]]; then
|
|
25
27
|
exit 0
|
|
26
28
|
fi
|
|
29
|
+
# Match `git commit`, or `git -C <path> commit`.
|
|
30
|
+
commit_re='git[[:space:]]+(-C[[:space:]]+[^[:space:]]+[[:space:]]+)?commit'
|
|
31
|
+
if [[ ! "$command" =~ $commit_re ]]; then
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# If the command targets a specific repo via `git -C <path>`, operate there too.
|
|
36
|
+
work_dir="."
|
|
37
|
+
if [[ "$command" =~ git[[:space:]]+-C[[:space:]]+([^[:space:]]+) ]]; then
|
|
38
|
+
work_dir="${BASH_REMATCH[1]}"
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Not a git repo → nothing to do.
|
|
42
|
+
git_dir=$(git -C "$work_dir" rev-parse --absolute-git-dir 2>/dev/null || true)
|
|
43
|
+
if [[ -z "$git_dir" ]]; then
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
27
46
|
|
|
28
|
-
#
|
|
29
|
-
|
|
47
|
+
# Hash what will be committed. `git status --porcelain` is included so a commit
|
|
48
|
+
# that only adds untracked files isn't mistaken for empty; `git diff HEAD` adds
|
|
49
|
+
# tracked content. On the first commit there is no HEAD, so use the working state.
|
|
50
|
+
if git -C "$work_dir" rev-parse --verify HEAD >/dev/null 2>&1; then
|
|
51
|
+
changeset=$(git -C "$work_dir" status --porcelain; git -C "$work_dir" diff HEAD)
|
|
52
|
+
else
|
|
53
|
+
changeset=$(git -C "$work_dir" status --porcelain; git -C "$work_dir" diff; git -C "$work_dir" diff --cached)
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Nothing to review (e.g. a message-only `--amend`) → let it proceed.
|
|
57
|
+
if [[ -z "$changeset" ]]; then
|
|
30
58
|
exit 0
|
|
31
59
|
fi
|
|
32
60
|
|
|
33
|
-
|
|
34
|
-
|
|
61
|
+
hash=$(printf '%s' "$changeset" | git hash-object --stdin)
|
|
62
|
+
state_file="$git_dir/self-review-reviewed"
|
|
35
63
|
|
|
36
|
-
|
|
37
|
-
|
|
64
|
+
# Already surfaced for review → allow the commit.
|
|
65
|
+
if [[ -f "$state_file" ]] && grep -qxF "$hash" "$state_file"; then
|
|
38
66
|
exit 0
|
|
39
67
|
fi
|
|
40
68
|
|
|
41
|
-
#
|
|
42
|
-
|
|
69
|
+
# Record this change-set and block once to force a review first.
|
|
70
|
+
echo "$hash" >> "$state_file"
|
|
71
|
+
|
|
72
|
+
review_prompt='SELF-REVIEW REQUIRED before committing. Review the changes that are
|
|
73
|
+
about to be committed BEFORE running git commit again:
|
|
43
74
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
`git diff origin/main...HEAD` to examine all changes from the branch point.
|
|
47
|
-
Check for:
|
|
48
|
-
1. Correctness: requirements met, logic errors, edge cases
|
|
49
|
-
2. Code Quality: naming, readability, follows project conventions
|
|
50
|
-
3. Testing: test coverage for new functionality
|
|
51
|
-
4. Security: vulnerabilities, input validation
|
|
52
|
-
5. Commit Quality: each commit should be a meaningful unit of work.
|
|
53
|
-
If there are minor fix commits (typos, forgotten changes, careless mistakes),
|
|
54
|
-
suggest squashing them into the relevant commit before merging.
|
|
75
|
+
- If the `self-review` skill is available, invoke it (Skill tool, skill "self-review").
|
|
76
|
+
- Otherwise, run the bundled `/code-review` skill on the working diff.
|
|
55
77
|
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
If the review finds issues, fix them and review again. Once it is clean, run the same
|
|
79
|
+
git commit command again to proceed — it will be allowed through.
|
|
58
80
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
The review runs in an isolated subagent with no memory of earlier rounds, so it cannot
|
|
82
|
+
tell on its own whether it is repeating itself — only you, the caller, have that history.
|
|
83
|
+
Before fixing a finding, check your own conversation: if it asks for the same change as
|
|
84
|
+
a finding you already attempted to satisfy twice before (worded differently still counts),
|
|
85
|
+
and it is still not resolved, do not attempt a third fix. Instead stop and ask the user
|
|
86
|
+
(AskUserQuestion): what the finding wants, how the two prior attempts addressed it, why it
|
|
87
|
+
still was not accepted, and the options (try a different fix, skip this finding and
|
|
88
|
+
commit, or decide the review point does not apply here). Do not run git commit again until
|
|
89
|
+
the user answers — another round of fixes would only repeat the same mismatch.'
|
|
62
90
|
|
|
63
91
|
jq -n --arg reason "$review_prompt" '{
|
|
64
|
-
"
|
|
65
|
-
|
|
92
|
+
"hookSpecificOutput": {
|
|
93
|
+
"hookEventName": "PreToolUse",
|
|
94
|
+
"permissionDecision": "deny",
|
|
95
|
+
"permissionDecisionReason": $reason
|
|
96
|
+
}
|
|
66
97
|
}'
|
|
67
98
|
|
|
68
99
|
exit 0
|
data/.claude/settings.json
CHANGED
|
@@ -20,6 +20,16 @@
|
|
|
20
20
|
"timeout": 300000
|
|
21
21
|
}
|
|
22
22
|
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"matcher": "Bash",
|
|
26
|
+
"hooks": [
|
|
27
|
+
{
|
|
28
|
+
"type": "command",
|
|
29
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/self-review.sh",
|
|
30
|
+
"timeout": 10000
|
|
31
|
+
}
|
|
32
|
+
]
|
|
23
33
|
}
|
|
24
34
|
],
|
|
25
35
|
"SessionStart": [
|
|
@@ -42,16 +52,6 @@
|
|
|
42
52
|
"timeout": 60000
|
|
43
53
|
}
|
|
44
54
|
]
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
"matcher": "Bash",
|
|
48
|
-
"hooks": [
|
|
49
|
-
{
|
|
50
|
-
"type": "command",
|
|
51
|
-
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/self-review.sh",
|
|
52
|
-
"timeout": 10000
|
|
53
|
-
}
|
|
54
|
-
]
|
|
55
55
|
}
|
|
56
56
|
]
|
|
57
57
|
}
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.2.0] - 2026-08-08
|
|
4
|
+
|
|
5
|
+
- `RSpec/UnusedLet` now also flags `subject`/`subject!` definitions that
|
|
6
|
+
nothing references. A subject answers to two names: its own, when it has
|
|
7
|
+
one, and the implicit `subject`, which RSpec's one-liner syntax
|
|
8
|
+
(`is_expected`, `are_expected`, `should`, `should_not`) and rspec-its' `its`
|
|
9
|
+
reach. A reference by either name counts, in a shared example block as much
|
|
10
|
+
as in an example group. `CheckLetBang` governs `subject!` alongside `let!`,
|
|
11
|
+
and setting the new `CheckSubject` (default `true`) to `false` turns the
|
|
12
|
+
check off entirely.
|
|
13
|
+
- `RSpec/UnusedLet` now also flags helper methods (`def`) written at an
|
|
14
|
+
example group's level whose name is never referenced. Such a method
|
|
15
|
+
becomes an instance method on the group's example class, so it is checked
|
|
16
|
+
with the same rules as a `let`, and autocorrect removes it.
|
|
17
|
+
- `RSpec/UnusedLet` now understands `shared_examples` / `shared_context`.
|
|
18
|
+
Where it used to silence every `let` an inclusion could reach, it now works
|
|
19
|
+
out which ones the shared block really uses, falling back to the old
|
|
20
|
+
behavior only for an inclusion it cannot resolve, and it no longer flags a
|
|
21
|
+
`let` that overrides a name an inline inclusion (`include_examples` /
|
|
22
|
+
`include_context`) injects and the shared block itself references. It also
|
|
23
|
+
checks the `let`s defined inside a shared block that carries examples —
|
|
24
|
+
expect new offenses on upgrade. `SharedExamplePaths` lists the files,
|
|
25
|
+
besides the one under inspection, whose top-level blocks are resolved, and
|
|
26
|
+
defaults to `spec/support/**/*.rb`; `CheckSharedExamples: false` leaves the
|
|
27
|
+
`let`s inside shared blocks unchecked, as before.
|
|
28
|
+
- `RSpec/UnusedLet` now skips helper specs (rspec-rails `type: :helper`, or
|
|
29
|
+
files under `spec/helpers`) by default, since the auto-included module may
|
|
30
|
+
reference any `let` unseen. Set `CheckHelperSpecs: true` to check them.
|
|
31
|
+
- `RSpec/UnusedLet` now recognizes `let` definitions consumed by
|
|
32
|
+
well-known gems' shared contexts and treats them as used. Currently
|
|
33
|
+
supports [rspec-validator_spec_helper](https://github.com/izumin5210/rspec-validator_spec_helper):
|
|
34
|
+
groups tagged with `type: :validator` may override `value`,
|
|
35
|
+
`attribute_names`, `options` (and the helper's other overridable
|
|
36
|
+
lets) without being flagged.
|
|
37
|
+
- Fix a false positive in `RSpec/UnusedLet` when a `let` defined in a
|
|
38
|
+
nested group was only referenced from an ancestor group's `let`,
|
|
39
|
+
`subject`, or hook block. Those ancestor blocks run in the example's
|
|
40
|
+
scope, so their references resolve to the nested definition.
|
|
41
|
+
- Fix a false positive in `RSpec/UnusedLet` when a `let` was only
|
|
42
|
+
referenced from a plain `def` helper method defined in an ancestor
|
|
43
|
+
example group. Such helpers become instance methods on the example
|
|
44
|
+
class and can reference `let` names visible at the example.
|
|
45
|
+
|
|
3
46
|
## [1.1.0] - 2026-07-15
|
|
4
47
|
|
|
5
48
|
- `RSpec/UnusedLet` now supports autocorrect. The correction is marked
|