rbs_activemodel 1.3.1 → 1.4.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e4dc4dbc7e78453a1e28c02b840515cd7842128f8a811be4186e7d142694bc5
4
- data.tar.gz: 360bca3add7f52c45aecb98748d993d2233b7e3ac02d09dd0c8c6e7d73a4b5b0
3
+ metadata.gz: 1e505f32754d95ee6e0b0588c1c4e29eb1886f7815541a3cff9316e5425ffafe
4
+ data.tar.gz: bf675d5783c2983cac9a59f448ea2ec9bcab0e99a4da8451f9305bc3a4b5d5e7
5
5
  SHA512:
6
- metadata.gz: a8f2bfa44a7646f724db738cba0e8aab2f4cc1c07c8841151a3cf70e64b520a20e885b022b5dda4d7d9998c878ea0d60a7eac3354ba4ca5d219ac370dbd7ecc5
7
- data.tar.gz: 1fe675d397625b1570d1de917a9f6f32733447e4c54d36265d3645d825199d4799c9032d0860e835c88bb27c7e293090a80781a21492f130f3925a6630aba807
6
+ metadata.gz: 4eacf6b6a9c5fa22698a011d14d91db2767fdb07ea5a0c85e6ca8c65759ac6bb861f7c47bf1325512d043cca5271baac132c9d92a90abb04c5220106a8f38934
7
+ data.tar.gz: 75660cc87d4927e3cfa514760689e1c992690404b1e4465bec2063ea1487da905dff590f6f83f3bb5f50215198c8417b8bfea5366bb5b71d798c25c300e3c942
@@ -0,0 +1,14 @@
1
+ #!/bin/bash
2
+ set -eu
3
+
4
+ if [ "${CLAUDE_CODE_REMOTE:-}" = "true" ]; then
5
+ eval "$(rbenv init - bash)"
6
+
7
+ # Set the latest installed Ruby version as the global default
8
+ rbenv global `rbenv versions --bare | sort -rV | head -1`
9
+
10
+ echo 'eval "$(rbenv init - bash)"' >> "$CLAUDE_ENV_FILE"
11
+ echo 'export RUBYOPT="-rcgi"' >> "$CLAUDE_ENV_FILE"
12
+ RUBYOPT="-rcgi" bundle install
13
+ bundle exec rbs collection install --frozen
14
+ fi
@@ -0,0 +1,73 @@
1
+ #!/bin/bash
2
+
3
+ # Hook input is JSON from stdin
4
+ input=$(cat)
5
+ tool_name=$(echo "$input" | jq -r '.tool_name')
6
+ command=$(echo "$input" | jq -r '.tool_input.command // ""')
7
+
8
+ # Only run for git commit commands
9
+ if [[ "$tool_name" != "Bash" ]] || [[ ! "$command" =~ git[[:space:]]+(commit|cherry-pick|merge|rebase) ]]; then
10
+ exit 0
11
+ fi
12
+
13
+ cd "$CLAUDE_PROJECT_DIR" || exit 1
14
+
15
+ if [ "${CLAUDE_CODE_REMOTE:-}" = "true" ]; then
16
+ eval "$(rbenv init - bash)"
17
+ fi
18
+
19
+ echo "Running pre-commit checks..." >&2
20
+
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
+ echo "Error: RBS generation failed" >&2
24
+ exit 2
25
+ fi
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
+
67
+ if ! bundle exec rake >&2; then
68
+ echo "Error: rake checks failed" >&2
69
+ exit 2
70
+ fi
71
+
72
+ echo "All checks passed!" >&2
73
+ exit 0
@@ -0,0 +1,45 @@
1
+ #!/bin/bash
2
+ # Prevents direct editing of auto-generated RBS type definition files.
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.
5
+
6
+ # Hook input is JSON from stdin
7
+ input=$(cat)
8
+ tool_name=$(echo "$input" | jq -r '.tool_name // ""')
9
+
10
+ # Block Write/Edit operations on sig/ files
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
28
+ fi
29
+ fi
30
+
31
+ # Block manual rbs-inline execution
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
42
+ fi
43
+ fi
44
+
45
+ exit 0
@@ -0,0 +1,95 @@
1
+ #!/bin/bash
2
+
3
+ # Hook input is JSON from stdin
4
+ input=$(cat)
5
+ tool_name=$(echo "$input" | jq -r '.tool_name')
6
+
7
+ cd "$CLAUDE_PROJECT_DIR" || exit 1
8
+
9
+ if [ "${CLAUDE_CODE_REMOTE:-}" = "true" ]; then
10
+ eval "$(rbenv init - bash)"
11
+ fi
12
+
13
+ # Handle Edit or Write tools
14
+ if [[ "$tool_name" == "Edit" || "$tool_name" == "Write" ]]; then
15
+ file_path=$(echo "$input" | jq -r '.tool_input.file_path // ""')
16
+
17
+ if [[ ! "$file_path" =~ \.rb$ ]]; then
18
+ exit 0
19
+ fi
20
+
21
+ if [[ ! "$file_path" =~ /lib/ ]]; then
22
+ exit 0
23
+ fi
24
+
25
+ echo "Running rbs-inline for $file_path..." >&2
26
+
27
+ if ! bundle exec rbs-inline --opt-out --output=sig/ "$file_path" >&2; then
28
+ echo "Warning: RBS generation failed for $file_path" >&2
29
+ exit 0
30
+ fi
31
+
32
+ echo "RBS generation completed." >&2
33
+ exit 0
34
+ fi
35
+
36
+ # Handle Bash tool for mv command
37
+ if [[ "$tool_name" == "Bash" ]]; then
38
+ command=$(echo "$input" | jq -r '.tool_input.command // ""')
39
+
40
+ # Check if this is a mv command
41
+ if [[ ! "$command" =~ ^[[:space:]]*(mv|git\ mv)[[:space:]] ]]; then
42
+ exit 0
43
+ fi
44
+
45
+ # Extract source and destination paths from mv command
46
+ paths=$(echo "$command" | sed -E 's/^[[:space:]]*(git[[:space:]]+)?mv[[:space:]]+//')
47
+
48
+ source_path=$(echo "$paths" | awk '{print $1}')
49
+ dest_path=$(echo "$paths" | awk '{print $2}')
50
+
51
+ # Check if source was a .rb file in lib/
52
+ if [[ ! "$source_path" =~ \.rb$ ]] || [[ ! "$source_path" =~ lib/ ]]; then
53
+ exit 0
54
+ fi
55
+
56
+ echo "Detected mv of Ruby file in lib/: $source_path -> $dest_path" >&2
57
+
58
+ # Calculate the corresponding .rbs file path for the source
59
+ source_rbs=$(echo "$source_path" | sed -E 's|^(.*/)?lib/|sig/|; s|\.rb$|.rbs|')
60
+
61
+ # Remove the old .rbs file if it exists
62
+ if [[ -f "$source_rbs" ]]; then
63
+ echo "Removing old RBS file: $source_rbs" >&2
64
+ rm -f "$source_rbs"
65
+ fi
66
+
67
+ # Determine the new .rb file path
68
+ if [[ -d "$dest_path" ]]; then
69
+ dest_file="$dest_path/$(basename "$source_path")"
70
+ else
71
+ dest_file="$dest_path"
72
+ fi
73
+
74
+ # Check if the destination is in lib/
75
+ if [[ ! "$dest_file" =~ lib/ ]]; then
76
+ echo "Destination is not in lib/, skipping RBS generation" >&2
77
+ exit 0
78
+ fi
79
+
80
+ # Generate RBS for the new file
81
+ if [[ -f "$dest_file" ]]; then
82
+ echo "Running rbs-inline for $dest_file..." >&2
83
+
84
+ if ! bundle exec rbs-inline --opt-out --output=sig/ "$dest_file" >&2; then
85
+ echo "Warning: RBS generation failed for $dest_file" >&2
86
+ exit 0
87
+ fi
88
+
89
+ echo "RBS generation completed." >&2
90
+ fi
91
+
92
+ exit 0
93
+ fi
94
+
95
+ exit 0
@@ -0,0 +1,93 @@
1
+ #!/bin/bash
2
+ #
3
+ # PreToolUse hook: review code before `git commit`.
4
+ #
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.
10
+ #
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.
15
+ #
16
+
17
+ set -euo pipefail
18
+
19
+ # Read JSON input from stdin
20
+ input=$(cat)
21
+
22
+ tool_name=$(echo "$input" | jq -r '.tool_name // empty')
23
+ command=$(echo "$input" | jq -r '.tool_input.command // empty')
24
+
25
+ # Only gate the Bash tool running a git commit.
26
+ if [[ "$tool_name" != "Bash" ]]; then
27
+ exit 0
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
46
+
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
58
+ exit 0
59
+ fi
60
+
61
+ hash=$(printf '%s' "$changeset" | git hash-object --stdin)
62
+ state_file="$git_dir/self-review-reviewed"
63
+
64
+ # Already surfaced for review → allow the commit.
65
+ if [[ -f "$state_file" ]] && grep -qxF "$hash" "$state_file"; then
66
+ exit 0
67
+ fi
68
+
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:
74
+
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.
77
+
78
+ Judge each finding the review returns rather than fixing it on sight. Weigh what the review
79
+ attached to it — how it graded the point, the basis it gave — and decide whether acting on it
80
+ is worth it. Where you skip a finding, say so; do not drop it silently. Where the choice needs
81
+ a requirement or an intent you do not have, leave it to the user. Once every fix you judged
82
+ necessary is in, review again; when nothing you judged worth fixing is left, run the same git
83
+ commit command again to proceed — it will be allowed through.'
84
+
85
+ jq -n --arg reason "$review_prompt" '{
86
+ "hookSpecificOutput": {
87
+ "hookEventName": "PreToolUse",
88
+ "permissionDecision": "deny",
89
+ "permissionDecisionReason": $reason
90
+ }
91
+ }'
92
+
93
+ exit 0
@@ -0,0 +1,58 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "Edit|Write|Bash",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/protect-sig-files.sh",
10
+ "timeout": 5000
11
+ }
12
+ ]
13
+ },
14
+ {
15
+ "matcher": "Bash",
16
+ "hooks": [
17
+ {
18
+ "type": "command",
19
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/pre-commit-check.sh",
20
+ "timeout": 300000
21
+ }
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
+ ]
33
+ }
34
+ ],
35
+ "SessionStart": [
36
+ {
37
+ "hooks": [
38
+ {
39
+ "type": "command",
40
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/claude-code-web-session-start.sh"
41
+ }
42
+ ]
43
+ }
44
+ ],
45
+ "PostToolUse": [
46
+ {
47
+ "matcher": "Edit|Write|Bash",
48
+ "hooks": [
49
+ {
50
+ "type": "command",
51
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/rbs-inline.sh",
52
+ "timeout": 60000
53
+ }
54
+ ]
55
+ }
56
+ ]
57
+ }
58
+ }
data/.rubocop.yml CHANGED
@@ -1,5 +1,28 @@
1
+ # NOTE: Rules are sorted in ASCII order.
2
+
1
3
  AllCops:
2
- TargetRubyVersion: 2.7
4
+ NewCops: enable
5
+ TargetRubyVersion: 3.3
6
+
7
+ plugins:
8
+ - rubocop-instance_variable_access
9
+ - rubocop-numbered-params
10
+ - rubocop-rake
11
+ - rubocop-rbs_inline
12
+ - rubocop-rspec
13
+ - rubocop-rspec-unused-let
14
+
15
+ # Layout
16
+ Layout/LeadingCommentSpace:
17
+ AllowRBSInlineAnnotation: true
18
+ AllowSteepAnnotation: true
19
+
20
+ Layout/LineLength:
21
+ Max: 120
22
+
23
+ # Metrics
24
+ Metrics/AbcSize:
25
+ Max: 20
3
26
 
4
27
  Metrics/BlockLength:
5
28
  Exclude:
@@ -7,17 +30,55 @@ Metrics/BlockLength:
7
30
  - "spec/**/*.rb"
8
31
 
9
32
  Metrics/ClassLength:
10
- Max: 150
33
+ Max: 200
11
34
 
12
35
  Metrics/CyclomaticComplexity:
13
36
  Max: 10
14
37
 
15
38
  Metrics/MethodLength:
16
- Max: 20
39
+ Max: 30
40
+
41
+ # RSpec
42
+ RSpec/ExampleLength:
43
+ Enabled: false
44
+
45
+ RSpec/MultipleExpectations:
46
+ Enabled: false
47
+
48
+ RSpec/MultipleMemoizedHelpers:
49
+ Max: 10
50
+
51
+ RSpec/NamedSubject:
52
+ Enabled: false
53
+
54
+ RSpec/NestedGroups:
55
+ Max: 5
56
+
57
+ # Style
58
+ Style/AccessorGrouping:
59
+ Enabled: false
60
+
61
+ Style/CommentedKeyword:
62
+ Enabled: false
17
63
 
18
64
  Style/Documentation:
19
65
  Enabled: false
20
66
 
67
+ Style/HashSyntax:
68
+ EnforcedShorthandSyntax: always
69
+
70
+ Style/RbsInline:
71
+ Mode: opt_out
72
+
73
+ Style/RbsInline/MissingTypeAnnotation:
74
+ EnforcedStyle: doc_style_and_return_annotation
75
+
76
+ Style/RbsInline/RedundantTypeAnnotation:
77
+ EnforcedStyle: doc_style_and_return_annotation
78
+
79
+ Style/RedundantFormat:
80
+ Enabled: false
81
+
21
82
  Style/StringLiterals:
22
83
  EnforcedStyle: double_quotes
23
84
 
@@ -2,5 +2,24 @@
2
2
  "[ruby]": {
3
3
  "editor.defaultFormatter": "Shopify.ruby-lsp"
4
4
  },
5
- "rbs-helper.signature-directory": "sig"
5
+ "rbs-helper.signature-directory": "sig",
6
+ "cSpell.words": [
7
+ "Activemodel",
8
+ "activerecord",
9
+ "bigdecimal",
10
+ "bindir",
11
+ "klass",
12
+ "KOMIYA",
13
+ "mkpath",
14
+ "popen",
15
+ "raketask",
16
+ "readlines",
17
+ "rmtree",
18
+ "rubygems",
19
+ "Takeshi",
20
+ "tasklib",
21
+ "tk0miya"
22
+ ],
23
+ "rbs-helper.rbs-inline-on-save": true,
24
+ "rbs-helper.rbs-inline-signature-directory": "sig/"
6
25
  }
data/README.md CHANGED
@@ -6,7 +6,7 @@ rbs_activemodel is a RBS generator for activemodel gem.
6
6
 
7
7
  Add a new entry to your Gemfile and run `bundle install`:
8
8
 
9
- group :deveopment do
9
+ group :development do
10
10
  gem 'rbs_activemodel', require: false
11
11
  end
12
12
 
data/Rakefile CHANGED
@@ -1,8 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
4
5
  require "rubocop/rake_task"
5
6
 
6
7
  RuboCop::RakeTask.new
8
+ RSpec::Core::RakeTask.new(:spec)
7
9
 
8
- task default: :rubocop
10
+ task default: :ci
11
+
12
+ task ci: %i[rubocop spec steep rbs:validate]
13
+
14
+ namespace :rbs do
15
+ # sig/gems holds hand-written types for external gems, so it is never regenerated
16
+ kept = ->(path) { path == "sig/gems" || path.start_with?("sig/gems/") }
17
+
18
+ desc "Install RBS collection"
19
+ task :install do
20
+ sh "bundle exec rbs collection install --frozen"
21
+ end
22
+
23
+ desc "Regenerate all RBS files under sig/ from scratch (run manually after editing lib/)"
24
+ task :regenerate do
25
+ Dir.glob("sig/**/*.rbs").reject(&kept).each { rm _1 }
26
+
27
+ sh "bundle exec rbs-inline --opt-out --output=sig lib"
28
+
29
+ # Drop the directories the deletion above left empty, deepest first
30
+ dirs = Dir.glob("sig/**/*").select { File.directory?(_1) }
31
+ dirs.reject(&kept).sort.reverse_each { rmdir _1 if Dir.empty?(_1) }
32
+ end
33
+
34
+ desc "Validate RBS files"
35
+ task validate: "rbs:install" do
36
+ sh "bundle exec rbs -Isig validate"
37
+ end
38
+ end
39
+
40
+ desc "Run steep type check"
41
+ task steep: "rbs:install" do
42
+ sh "bundle exec steep check"
43
+ end
@@ -4,7 +4,7 @@ require "rails"
4
4
 
5
5
  module RbsActivemodel
6
6
  class InstallGenerator < Rails::Generators::Base
7
- def create_raketask
7
+ def create_raketask #: void
8
8
  create_file "lib/tasks/rbs_activemodel.rake", <<~RUBY
9
9
  # frozen_string_literal: true
10
10