rbs_activerecord 1.2.1 → 1.4.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/hooks/claude-code-web-session-start.sh +14 -0
  3. data/.claude/hooks/pre-commit-check.sh +73 -0
  4. data/.claude/hooks/protect-sig-files.sh +45 -0
  5. data/.claude/hooks/rbs-inline.sh +95 -0
  6. data/.claude/hooks/self-review.sh +93 -0
  7. data/.claude/settings.json +58 -0
  8. data/.rubocop.yml +61 -1
  9. data/.vscode/settings.json +16 -1
  10. data/README.md +7 -0
  11. data/Rakefile +32 -3
  12. data/lib/generators/rbs_activerecord/install_generator.rb +5 -2
  13. data/lib/rbs_activerecord/generator/active_storage/instance_methods.rb +3 -2
  14. data/lib/rbs_activerecord/generator/active_storage/scopes.rb +1 -1
  15. data/lib/rbs_activerecord/generator/associations.rb +4 -4
  16. data/lib/rbs_activerecord/generator/attributes.rb +30 -5
  17. data/lib/rbs_activerecord/generator/delegated_type/instance_methods.rb +5 -5
  18. data/lib/rbs_activerecord/generator/delegated_type/scopes.rb +3 -3
  19. data/lib/rbs_activerecord/generator/enum/base.rb +2 -2
  20. data/lib/rbs_activerecord/generator/enum/instance_methods.rb +3 -3
  21. data/lib/rbs_activerecord/generator/enum/mappings.rb +2 -2
  22. data/lib/rbs_activerecord/generator/enum/scopes.rb +2 -2
  23. data/lib/rbs_activerecord/generator/scopes.rb +2 -2
  24. data/lib/rbs_activerecord/generator/secure_password.rb +1 -1
  25. data/lib/rbs_activerecord/generator.rb +6 -3
  26. data/lib/rbs_activerecord/model.rb +2 -1
  27. data/lib/rbs_activerecord/parser/evaluator.rb +3 -12
  28. data/lib/rbs_activerecord/parser/include_expander/module.rb +7 -5
  29. data/lib/rbs_activerecord/parser/include_expander.rb +4 -4
  30. data/lib/rbs_activerecord/parser/visitor.rb +1 -1
  31. data/lib/rbs_activerecord/parser.rb +2 -1
  32. data/lib/rbs_activerecord/rake_task.rb +6 -3
  33. data/lib/rbs_activerecord/utils.rb +8 -8
  34. data/lib/rbs_activerecord/version.rb +1 -1
  35. data/rbs_collection.lock.yaml +136 -64
  36. data/rbs_collection.yaml +1 -0
  37. data/sig/generators/rbs_activerecord/install_generator.rbs +1 -1
  38. data/sig/rbs_activerecord/generator/active_storage/instance_methods.rbs +1 -0
  39. data/sig/rbs_activerecord/generator/attributes.rbs +14 -1
  40. data/sig/rbs_activerecord/generator.rbs +4 -1
  41. data/sig/rbs_activerecord/parser.rbs +2 -1
  42. data/sig/rbs_activerecord/rake_task.rbs +4 -2
  43. data/sig/rbs_activerecord/utils.rbs +2 -2
  44. metadata +11 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a901f8b1ee98a3837e7ee4182ffb3d247466793f367ac6b764487271f2088445
4
- data.tar.gz: 749b7ec1f3dc34235087df037f7f94418e09729cff3229717e1620a08d6abbaa
3
+ metadata.gz: 78a25f0014350c5542d7b2ed8eeb226c736c8f3416432261413e26f84b7d5877
4
+ data.tar.gz: 92684a1a6ac24afc2bcf1fe160ab6bf391f0ada8bd75ae337360c01d36891114
5
5
  SHA512:
6
- metadata.gz: 6c5e46b70af5fb36fbfacf1beea9f949ad18b31a78ca13bfe37578f0fb8d4c5c88738bbc9039f09adde10ce49dc7f43a49d60664ac9130f4614178a8781259bf
7
- data.tar.gz: 16cdca38fc3d9d72ba97ab9c076996aea8c58864bdcb41476a0bf8d9f86123a1042fe305a4e3ff77022ad86febeba50c6d7c632fc2bf8f3512af8b9cd58c4858
6
+ metadata.gz: 680a2e88d185d9a7c26d124ed5b6f0240233f4199006f4eb85e1639b25abb2925fa6ac6dd623c4d25f7777eea947a137357c05e274f3d8ac02c88ee9a01e2acc
7
+ data.tar.gz: db219887c854a4f412e0d5c1ba7713dc2425c45d985e9bb9fd5872424d51a84c17d8b843db400273fe7c47baa35e690fc2fe8ca2d45bfce2cdfeaf3e0aaf13b7
@@ -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,20 +1,80 @@
1
+ # NOTE: Rules are sorted in ASCII order.
2
+
1
3
  AllCops:
2
- TargetRubyVersion: 3.1
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
3
14
 
15
+ # Layout
4
16
  Layout/LeadingCommentSpace:
5
17
  AllowRBSInlineAnnotation: true
6
18
  AllowSteepAnnotation: true
7
19
 
20
+ Layout/LineLength:
21
+ Max: 120
22
+
23
+ # Metrics
24
+ Metrics/AbcSize:
25
+ Max: 20
26
+
8
27
  Metrics/BlockLength:
9
28
  Exclude:
10
29
  - "spec/**/*.rb"
11
30
 
31
+ Metrics/ClassLength:
32
+ Max: 200
33
+
12
34
  Metrics/MethodLength:
13
35
  Max: 30
14
36
 
37
+ # RSpec
38
+ RSpec/ExampleLength:
39
+ Enabled: false
40
+
41
+ RSpec/MultipleExpectations:
42
+ Enabled: false
43
+
44
+ RSpec/MultipleMemoizedHelpers:
45
+ Max: 10
46
+
47
+ RSpec/NamedSubject:
48
+ Enabled: false
49
+
50
+ RSpec/NestedGroups:
51
+ Max: 5
52
+
53
+ # Style
54
+ Style/AccessorGrouping:
55
+ Enabled: false
56
+
57
+ Style/CommentedKeyword:
58
+ Enabled: false
59
+
15
60
  Style/Documentation:
16
61
  Enabled: false
17
62
 
63
+ Style/HashSyntax:
64
+ EnforcedShorthandSyntax: always
65
+
66
+ Style/RbsInline:
67
+ Mode: opt_out
68
+
69
+ Style/RbsInline/MissingTypeAnnotation:
70
+ EnforcedStyle: doc_style_and_return_annotation
71
+
72
+ Style/RbsInline/RedundantTypeAnnotation:
73
+ EnforcedStyle: doc_style_and_return_annotation
74
+
75
+ Style/RedundantFormat:
76
+ Enabled: false
77
+
18
78
  Style/StringLiterals:
19
79
  EnforcedStyle: double_quotes
20
80
 
@@ -6,13 +6,28 @@
6
6
  "rbs-helper.rbs-inline-on-save": true,
7
7
  "cSpell.words": [
8
8
  "activerecord",
9
+ "activestorage",
10
+ "bindir",
9
11
  "codebases",
10
12
  "Cyclomatic",
13
+ "entryable",
14
+ "foos",
11
15
  "kaminari",
16
+ "KOMIYA",
17
+ "kwargs",
12
18
  "mkpath",
13
19
  "modname",
20
+ "noargs",
21
+ "popen",
22
+ "posargs",
23
+ "raketask",
24
+ "readlines",
14
25
  "requireds",
15
26
  "rmtree",
16
- "tasklib"
27
+ "rubocop",
28
+ "rubygems",
29
+ "Takeshi",
30
+ "tasklib",
31
+ "tk0miya"
17
32
  ]
18
33
  }
data/README.md CHANGED
@@ -23,6 +23,12 @@ Run `rbs:activerecord:setup` task:
23
23
  Then rbs_activerecord will scan your code and generate RBS files into
24
24
  `sig/activerecord` directory.
25
25
 
26
+ ## Configurations
27
+
28
+ You can change the behavior of the generator via rbs_activerecord.rake:
29
+
30
+ * pure_accessors: Make accessor methods for columns pure. It helps [Type narrowing](https://github.com/soutaro/steep/wiki/Release-Note-1.1#type-narrowing-on-method-calls-590).
31
+
26
32
  ## The goal of rbs_activerecord
27
33
 
28
34
  rbs_activerecord is an experimental project to enhance the type generation for ActiveRecord models.
@@ -43,6 +49,7 @@ Main differences between rbs_activerecord and rbs_rails are:
43
49
  * Support extension ActiveRecord from 3rd party gems ([#254](https://github.com/pocke/rbs_rails/pull/254))
44
50
  * ex. [kaminari-activerecord](https://github.com/ruby/gem_rbs_collection/pull/725)
45
51
  * Support composite primary keys ([#275](https://github.com/pocke/rbs_rails/pull/275))
52
+ * Support pure_accessors ([#302](https://github.com/pocke/rbs_rails/issues/302))
46
53
  * Extend return types of #pluck ([#273](https://github.com/pocke/rbs_rails/pull/273))
47
54
  * Override types of CollectionProxy ([#289](https://github.com/pocke/rbs_rails/pull/289))
48
55
  * Bug fixes
data/Rakefile CHANGED
@@ -1,14 +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]
9
13
 
10
14
  namespace :rbs do
11
- task :generate do
12
- sh "rbs-inline", "--opt-out", "--output=sig", "lib"
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"
13
37
  end
14
38
  end
39
+
40
+ desc "Run steep type check"
41
+ task steep: "rbs:install" do
42
+ sh "bundle exec steep check"
43
+ end
@@ -4,14 +4,17 @@ require "rails"
4
4
 
5
5
  module RbsActiverecord
6
6
  class InstallGenerator < Rails::Generators::Base
7
- def create_raketask
7
+ def create_raketask #: void
8
8
  create_file "lib/tasks/rbs_activerecord.rake", <<~RUBY
9
9
  # frozen_string_literal: true
10
10
 
11
11
  begin
12
12
  require "rbs_activerecord/rake_task"
13
13
 
14
- RbsActiverecord::RakeTask.new
14
+ RbsActiverecord::RakeTask.new do |task|
15
+ # Make accessor methods for columns pure.
16
+ # task.pure_accessors = true
17
+ end
15
18
  rescue LoadError
16
19
  # failed to load rbs_activerecord. Skip to load rbs_activerecord tasks.
17
20
  end
@@ -23,13 +23,14 @@ module RbsActiverecord
23
23
 
24
24
  def attachments #: Hash[String, untyped]
25
25
  if model.klass.respond_to?(:attachment_reflections)
26
- model.klass.attachment_reflections # steep:ignore
26
+ model.klass.attachment_reflections
27
27
  else
28
28
  {}
29
29
  end
30
30
  end
31
31
 
32
32
  # @rbs name: String
33
+ # @rbs reflection: untyped
33
34
  def attachment(name, reflection) #: String
34
35
  case reflection.macro
35
36
  when :has_one_attached
@@ -44,7 +45,7 @@ module RbsActiverecord
44
45
  RBS
45
46
  when :has_many_attached
46
47
  <<~RBS
47
- def #{name}: () -> ::ActiveStroage::Attached::Many
48
+ def #{name}: () -> ::ActiveStorage::Attached::Many
48
49
  def #{name}=: (untyped) -> untyped
49
50
  RBS
50
51
  else
@@ -23,7 +23,7 @@ module RbsActiverecord
23
23
 
24
24
  def attachments #: Hash[String, untyped]
25
25
  if model.klass.respond_to?(:attachment_reflections)
26
- model.klass.attachment_reflections # steep:ignore
26
+ model.klass.attachment_reflections
27
27
  else
28
28
  {}
29
29
  end
@@ -25,7 +25,7 @@ module RbsActiverecord
25
25
 
26
26
  private
27
27
 
28
- def has_many #: String # rubocop:disable Naming/PredicateName
28
+ def has_many #: String # rubocop:disable Naming/PredicatePrefix
29
29
  model.reflect_on_all_associations(:has_many).map do |assoc|
30
30
  assoc_name = assoc.name.to_s
31
31
  klass_name = "::#{assoc.klass.name}"
@@ -41,7 +41,7 @@ module RbsActiverecord
41
41
  end.join("\n")
42
42
  end
43
43
 
44
- def has_one #: String # rubocop:disable Naming/PredicateName
44
+ def has_one #: String # rubocop:disable Naming/PredicatePrefix
45
45
  model.reflect_on_all_associations(:has_one).map do |assoc|
46
46
  type = "::#{assoc.klass.name}"
47
47
  optional = "#{type}?"
@@ -85,7 +85,7 @@ module RbsActiverecord
85
85
  def polymorphic_owner_types(assoc) #: String # rubocop:disable Metrics/AbcSize
86
86
  table_name = model.klass.name.to_s.tableize.to_sym
87
87
  owners = ActiveRecord::Base.descendants.select do |klass|
88
- klass.reflect_on_all_associations.any? { |a| a.name == table_name && a.options[:as] == assoc.name }
88
+ klass.reflect_on_all_associations.any? { _1.name == table_name && _1.options[:as] == assoc.name }
89
89
  end
90
90
 
91
91
  if owners.empty?
@@ -98,7 +98,7 @@ module RbsActiverecord
98
98
  end
99
99
  end
100
100
 
101
- def has_and_belongs_to_many #: String # rubocop:disable Naming/PredicateName
101
+ def has_and_belongs_to_many #: String # rubocop:disable Naming/PredicatePrefix
102
102
  model.reflect_on_all_associations(:has_and_belongs_to_many).map do |assoc|
103
103
  assoc_name = assoc.name.to_s
104
104
  klass_name = assoc.klass.name