rubocop-claude 0.1.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 (37) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +25 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +267 -0
  5. data/config/default.yml +202 -0
  6. data/exe/rubocop-claude +7 -0
  7. data/lib/rubocop/cop/claude/explicit_visibility.rb +139 -0
  8. data/lib/rubocop/cop/claude/mystery_regex.rb +46 -0
  9. data/lib/rubocop/cop/claude/no_backwards_compat_hacks.rb +140 -0
  10. data/lib/rubocop/cop/claude/no_commented_code.rb +182 -0
  11. data/lib/rubocop/cop/claude/no_fancy_unicode.rb +173 -0
  12. data/lib/rubocop/cop/claude/no_hardcoded_line_numbers.rb +142 -0
  13. data/lib/rubocop/cop/claude/no_overly_defensive_code.rb +160 -0
  14. data/lib/rubocop/cop/claude/tagged_comments.rb +78 -0
  15. data/lib/rubocop-claude.rb +19 -0
  16. data/lib/rubocop_claude/cli.rb +246 -0
  17. data/lib/rubocop_claude/generator.rb +90 -0
  18. data/lib/rubocop_claude/init_wizard/hooks_installer.rb +127 -0
  19. data/lib/rubocop_claude/init_wizard/linter_configurer.rb +88 -0
  20. data/lib/rubocop_claude/init_wizard/preferences_gatherer.rb +94 -0
  21. data/lib/rubocop_claude/plugin.rb +34 -0
  22. data/lib/rubocop_claude/version.rb +5 -0
  23. data/rubocop-claude.gemspec +41 -0
  24. data/templates/cops/class-structure.md +58 -0
  25. data/templates/cops/disable-cops-directive.md +33 -0
  26. data/templates/cops/explicit-visibility.md +52 -0
  27. data/templates/cops/metrics.md +73 -0
  28. data/templates/cops/mystery-regex.md +54 -0
  29. data/templates/cops/no-backwards-compat-hacks.md +101 -0
  30. data/templates/cops/no-commented-code.md +74 -0
  31. data/templates/cops/no-fancy-unicode.md +72 -0
  32. data/templates/cops/no-hardcoded-line-numbers.md +70 -0
  33. data/templates/cops/no-overly-defensive-code.md +117 -0
  34. data/templates/cops/tagged-comments.md +74 -0
  35. data/templates/hooks/settings.local.json +15 -0
  36. data/templates/linting.md +81 -0
  37. metadata +183 -0
@@ -0,0 +1,101 @@
1
+ # Claude/NoBackwardsCompatHacks
2
+
3
+ **What it catches:** Self-documented compatibility hacks - code with comments like "for backwards compatibility" or markers like `# removed:`.
4
+
5
+ **Why it matters:** Dead code should be deleted, not preserved "helpfully."
6
+
7
+ ## The Principle
8
+
9
+ When removing or replacing code, **delete it completely**. Don't:
10
+ - Leave commented tombstones (`# removed: old_method`)
11
+ - Create "compatibility shims" (`OldName = NewName`)
12
+ - Silence linters with underscore prefixes (`_unused = old_value`)
13
+ - Add wrapper methods that just delegate to new ones
14
+ - Keep empty methods "for backwards compatibility"
15
+
16
+ Version control preserves history. Callers should be updated, not shimmed.
17
+
18
+ ## What This Cop Detects
19
+
20
+ This cop catches **self-documented** hacks - patterns where you've helpfully commented your intent:
21
+
22
+ ```ruby
23
+ # CAUGHT - has compat comment
24
+ OldName = NewName # for backwards compatibility
25
+
26
+ # CAUGHT - dead code marker
27
+ # removed: def old_method; end
28
+ # deprecated: use new_method instead
29
+
30
+ # NOT CAUGHT - no comment (silent hack)
31
+ OldName = NewName
32
+ ```
33
+
34
+ ### Detection Philosophy
35
+
36
+ We only catch ~30% of compat hacks (the self-documented ones). This is intentional.
37
+
38
+ **The goal is teaching, not comprehensive detection.**
39
+
40
+ When this cop fires, you're reading this guidance and learning the principle. Even if we can't catch every case, the teaching moments shape better habits over time.
41
+
42
+ ## How to Fix
43
+
44
+ ### Dead Code Markers
45
+
46
+ ```ruby
47
+ # BAD
48
+ # removed: def old_method; end
49
+ # deprecated: use new_method instead
50
+ # legacy: keeping for backwards compat
51
+
52
+ # GOOD - just delete the comment entirely
53
+ ```
54
+
55
+ ### Constant Re-exports
56
+
57
+ ```ruby
58
+ # BAD
59
+ OldClassName = NewClassName # for backwards compatibility
60
+
61
+ # GOOD - delete the alias, update all callers
62
+ # (yes, even if there are many callers)
63
+ ```
64
+
65
+ ### Underscore Assignments (optional check)
66
+
67
+ ```ruby
68
+ # BAD (if CheckUnderscoreAssignments: true)
69
+ _old_value = calculate_something
70
+
71
+ # GOOD - delete the line if the value isn't needed
72
+ ```
73
+
74
+ ## For AI Assistants
75
+
76
+ **When you're about to add a compat shim, stop.** Ask yourself:
77
+
78
+ 1. "Am I preserving dead code?" → Delete it instead
79
+ 2. "Will callers break?" → Update the callers
80
+ 3. "There are too many callers" → Update them anyway, or ask the human
81
+ 4. "What if someone needs the old behavior?" → They can use git history
82
+
83
+ **Never:**
84
+ - Add `# for backwards compatibility` comments
85
+ - Create `OldName = NewName` aliases
86
+ - Use `_unused = value` to silence linters
87
+ - Leave `# removed:` or `# deprecated:` markers
88
+ - Create wrapper methods that just delegate
89
+
90
+ **Instead:**
91
+ - Delete the old code completely
92
+ - Update all callers to use the new code
93
+ - If unsure, ask: "Should I update all callers or is there a reason to keep the old interface?"
94
+
95
+ ## Configuration
96
+
97
+ ```yaml
98
+ Claude/NoBackwardsCompatHacks:
99
+ Enabled: true
100
+ CheckUnderscoreAssignments: false # Optional, off by default
101
+ ```
@@ -0,0 +1,74 @@
1
+ # Claude/NoCommentedCode
2
+
3
+ **What it catches:** Commented-out code (even single lines by default).
4
+
5
+ **Why it matters:** Commented code is technical debt. Version control preserves history - just delete it.
6
+
7
+ **Autocorrectable:** Yes (unsafe) - deletes the commented code. Run `rubocop -A` to auto-fix.
8
+
9
+ ## How to Fix
10
+
11
+ ```ruby
12
+ # BAD
13
+ # def old_method
14
+ # do_something
15
+ # end
16
+
17
+ # BAD - even single lines
18
+ # user.update!(name: "test")
19
+
20
+ # GOOD - just delete the commented code entirely
21
+ ```
22
+
23
+ ## The KEEP Exception
24
+
25
+ Sometimes you genuinely need to preserve commented code temporarily. Use a `KEEP` comment with attribution:
26
+
27
+ ```ruby
28
+ # GOOD - explicit, attributed, time-boxed
29
+ # KEEP [@username]: Rollback path during migration, remove after 2025-06
30
+ # def legacy_method
31
+ # old_implementation
32
+ # end
33
+
34
+ # BAD - KEEP without attribution doesn't work
35
+ # KEEP: I might need this later
36
+ # def old_method
37
+ # do_something
38
+ # end
39
+ ```
40
+
41
+ ### KEEP Rules
42
+
43
+ 1. **Must have attribution** - `[@handle]` format (same as TaggedComments)
44
+ 2. **Must have justification** - explain why it's kept
45
+ 3. **Should be time-boxed** - include a removal date when possible
46
+ 4. **Only protects the immediately following block** - prose comments break the protection
47
+
48
+ ## For AI Assistants
49
+
50
+ **Default behavior: delete commented code.** Don't ask "should I keep this?" - the answer is delete.
51
+
52
+ If you encounter a `# KEEP` comment with valid attribution, leave it alone. The human made an explicit decision to preserve that code.
53
+
54
+ **Never add KEEP comments yourself** unless the human explicitly asks you to preserve specific code temporarily.
55
+
56
+ ## What's NOT Commented Code
57
+
58
+ These are fine and won't be flagged:
59
+
60
+ - Explanatory prose comments
61
+ - Documentation (YARD/RDoc)
62
+ - TODO/FIXME/NOTE annotations
63
+ - `@example` blocks in documentation
64
+ - RuboCop directives
65
+
66
+ ## Configuration
67
+
68
+ ```yaml
69
+ Claude/NoCommentedCode:
70
+ MinLines: 1 # Flag even single lines (default)
71
+ AllowKeep: true # Honor KEEP comments with attribution (default)
72
+ ```
73
+
74
+ Set `MinLines: 2` if single-line detection is too noisy for your codebase.
@@ -0,0 +1,72 @@
1
+ # Claude/NoFancyUnicode
2
+
3
+ **What it catches:** Non-standard Unicode characters outside the allowed set (letters, numbers, ASCII symbols).
4
+
5
+ **Why it matters:** Fancy Unicode causes subtle bugs. Curly quotes `""` break string matching. Mathematical symbols `≠` look like `!=` but aren't. Em-dashes `—` aren't hyphens. Emoji reduce professionalism.
6
+
7
+ ## How to Fix
8
+
9
+ ```ruby
10
+ # BAD - curly quotes
11
+ puts "Hello world"
12
+
13
+ # BAD - em-dash
14
+ # Section 3 — Details
15
+
16
+ # BAD - mathematical symbol
17
+ puts "x ≠ y"
18
+
19
+ # BAD - emoji
20
+ puts "Success! 🎉"
21
+ status = :done_✅
22
+
23
+ # GOOD - ASCII equivalents
24
+ puts "Hello world"
25
+
26
+ # GOOD - double hyphen or just hyphen
27
+ # Section 3 -- Details
28
+
29
+ # GOOD - ASCII operators
30
+ puts "x != y"
31
+
32
+ # GOOD - no emoji
33
+ puts "Success!"
34
+ status = :done
35
+ ```
36
+
37
+ ## Allowed Characters
38
+
39
+ - **Letters** - Any script: Latin, Chinese, Japanese, Cyrillic, Arabic, etc.
40
+ - **Numbers** - Any script
41
+ - **Combining marks** - Accents, diacritics (café, José)
42
+ - **ASCII printable** - All standard keyboard symbols (0x20-0x7E)
43
+ - **Whitespace** - Tabs, newlines
44
+
45
+ ## Configuration Options
46
+
47
+ ```yaml
48
+ Claude/NoFancyUnicode:
49
+ AllowedUnicode: ['→', '←', '•'] # Specific chars to permit
50
+ AllowInStrings: false # Skip checking strings
51
+ AllowInComments: false # Skip checking comments
52
+ ```
53
+
54
+ ## Common Replacements
55
+
56
+ | Fancy | ASCII | Description |
57
+ |-------|-------|-------------|
58
+ | `"` `"` | `"` | Curly quotes to straight quotes |
59
+ | `'` `'` | `'` | Curly apostrophes to straight |
60
+ | `—` | `--` | Em-dash to double hyphen |
61
+ | `–` | `-` | En-dash to hyphen |
62
+ | `≠` | `!=` | Not equal |
63
+ | `≤` `≥` | `<=` `>=` | Comparison operators |
64
+ | `→` `←` | `->` `<-` | Arrows |
65
+ | `•` | `*` or `-` | Bullet |
66
+
67
+ ## When to Allow
68
+
69
+ Add to `AllowedUnicode` if the character is:
70
+ - Required by external API or data format
71
+ - Part of user-facing content where typography matters
72
+ - In comments explaining Unicode behavior
@@ -0,0 +1,70 @@
1
+ # Claude/NoHardcodedLineNumbers
2
+
3
+ **What it catches:** Hardcoded line numbers in comments and strings that become stale when code shifts.
4
+
5
+ **Why it matters:** References like "see line 42" or "foo.rb:123" break silently as code evolves. Use stable references like method names, class names, or descriptive comments instead.
6
+
7
+ ## How to Fix
8
+
9
+ ```ruby
10
+ # BAD - line numbers shift when code changes
11
+ # see line 42 for details
12
+ # Error defined at foo.rb:123
13
+ raise "error at line 42"
14
+
15
+ # GOOD - use stable references
16
+ # see #validate_input for details
17
+ # Error defined in FooError class
18
+ raise "error in validate_input"
19
+
20
+ # GOOD - use descriptive comments
21
+ # The validation logic below handles edge cases
22
+ # See the ErrorHandler module for error definitions
23
+ ```
24
+
25
+ ## Patterns Detected
26
+
27
+ | Pattern | Example | Description |
28
+ |---------|---------|-------------|
29
+ | `line N` | `# see line 42` | Natural language reference |
30
+ | `lines N` | `# lines 42-50` | Plural form |
31
+ | `LN` | `# see L42` | GitHub-style reference |
32
+ | `.rb:N` | `# foo.rb:123` | Ruby file:line format |
33
+ | `.erb:N` | `# app.erb:10` | ERB file:line format |
34
+ | `.rake:N` | `# tasks.rake:5` | Rake file:line format |
35
+
36
+ ## Patterns NOT Flagged
37
+
38
+ These are explicitly ignored:
39
+
40
+ - Version strings: `Ruby 3.1`, `v1.2.3`, `1.2.3`
41
+ - Port numbers: `port 8080`
42
+ - IDs: `id: 42`, `pid: 1234`
43
+ - Time durations: `30 seconds`, `100ms`
44
+ - Byte sizes: `100 bytes`, `5mb`
45
+ - Percentages: `50%`
46
+ - Dollar amounts: `$42`
47
+ - Issue references: `#42`
48
+
49
+ ## Configuration
50
+
51
+ ```yaml
52
+ Claude/NoHardcodedLineNumbers:
53
+ Enabled: true
54
+ CheckComments: true # Check comments for line refs (default: true)
55
+ CheckStrings: true # Check string literals (default: true)
56
+ MinLineNumber: 1 # Only flag line numbers >= this (default: 1)
57
+ ```
58
+
59
+ ## Better Alternatives
60
+
61
+ | Instead of... | Use... |
62
+ |---------------|--------|
63
+ | `# see line 42` | `# see #method_name` |
64
+ | `# foo.rb:123` | `# see FooClass#method` |
65
+ | `"error at line 42"` | `"error in validate_input"` |
66
+ | `# L42 handles this` | `# The validation block handles this` |
67
+
68
+ ## Edge Cases
69
+
70
+ The cop reports only the first line number per node to avoid noisy output. Heredocs are intentionally ignored since they often contain documentation or templates where line references may be intentional.
@@ -0,0 +1,117 @@
1
+ # Claude/NoOverlyDefensiveCode
2
+
3
+ **What it catches:**
4
+ 1. `rescue => e; nil` or `rescue nil` - swallowing errors
5
+ 2. 2+ chained `&.` operators - excessive safe navigation
6
+ 3. `a && a.foo` - defensive nil check before method call
7
+ 4. `a.present? && a.foo` - defensive presence check
8
+ 5. `foo.nil? ? default : foo` - verbose nil ternary
9
+ 6. `foo ? foo : default` - verbose identity ternary
10
+
11
+ **Why it matters:** Defensive code hides bugs and indicates distrust of the codebase. Internal code should be trusted; errors should propagate.
12
+
13
+ ## The Principle
14
+
15
+ Don't code defensively against your own codebase. When you add defensive patterns, you're saying "I don't trust this code." Either:
16
+ 1. Fix the code so it's trustworthy, or
17
+ 2. Handle the error/nil explicitly and meaningfully
18
+
19
+ ## Error Swallowing
20
+
21
+ ```ruby
22
+ # BAD - swallows all errors
23
+ begin
24
+ risky_operation
25
+ rescue => e
26
+ nil
27
+ end
28
+
29
+ # BAD - inline form
30
+ result = dangerous_call rescue nil
31
+
32
+ # GOOD - let errors propagate
33
+ result = risky_operation
34
+
35
+ # GOOD - specific exceptions with intentional ignore
36
+ begin
37
+ require 'optional_gem'
38
+ rescue LoadError
39
+ # Optional dependency not available
40
+ end
41
+ ```
42
+
43
+ ## Excessive Safe Navigation
44
+
45
+ ```ruby
46
+ # BAD - 2+ chained &. violates design principles
47
+ user&.profile&.settings
48
+
49
+ # GOOD - single &. at system boundary
50
+ user&.name
51
+
52
+ # GOOD - trust your data model
53
+ user.profile.settings.notifications
54
+ ```
55
+
56
+ ## Defensive Nil Checks
57
+
58
+ ```ruby
59
+ # BAD - pre-safe-navigation pattern
60
+ a && a.foo
61
+ user && user.name
62
+
63
+ # BAD - presence check before method call
64
+ user.present? && user.name
65
+
66
+ # GOOD (default: AddSafeNavigator: false) - trust the code
67
+ a.foo
68
+ user.name
69
+
70
+ # ALTERNATIVE (AddSafeNavigator: true) - if you really need nil safety
71
+ a&.foo
72
+ user&.name
73
+ ```
74
+
75
+ ## Verbose Ternaries
76
+
77
+ ```ruby
78
+ # BAD - verbose nil check
79
+ foo.nil? ? default : foo
80
+ value.blank? ? fallback : value
81
+
82
+ # BAD - verbose identity check
83
+ foo ? foo : default
84
+
85
+ # GOOD - use ||
86
+ foo || default
87
+ value || fallback
88
+ ```
89
+
90
+ ## For AI Assistants
91
+
92
+ **When you're about to add defensive code, stop.** Ask yourself:
93
+
94
+ 1. "Why don't I trust this code?" → Fix the trust issue instead
95
+ 2. "What error am I hiding?" → Let it propagate or handle it properly
96
+ 3. "Why might this be nil?" → Fix the data model or handle at the boundary
97
+
98
+ **The right response to uncertainty is not defensive code.** It's:
99
+ - Understanding why the uncertainty exists
100
+ - Fixing the root cause
101
+ - Or asking the human: "This could be nil/error here - how should I handle it?"
102
+
103
+ ## Configuration
104
+
105
+ ```yaml
106
+ Claude/NoOverlyDefensiveCode:
107
+ Enabled: true
108
+ MaxSafeNavigationChain: 1 # Flag 2+ chained &. operators
109
+ AddSafeNavigator: false # Autocorrect `a && a.foo` to `a.foo` (fail fast)
110
+ # Set to true for `a&.foo` (add safe nav)
111
+ ```
112
+
113
+ ## Related Cops
114
+
115
+ If using `rubocop-rails`, also enable:
116
+ - `Rails/Present` - catches `a && a.present?`
117
+ - `Rails/Blank` - catches `a.blank? && a.foo`
@@ -0,0 +1,74 @@
1
+ # Claude/TaggedComments
2
+
3
+ **What it catches:** TODO/FIXME/NOTE/HACK comments without attribution.
4
+
5
+ **Why it matters:** Anonymous TODOs lose context. Attribution tracks ownership and distinguishes human comments from AI-generated ones.
6
+
7
+ ## How to Fix
8
+
9
+ ```ruby
10
+ # BAD
11
+ # TODO: Refactor this method
12
+ # FIXME: Handle edge case
13
+
14
+ # GOOD - human attribution
15
+ # TODO [@username]: Refactor this method - it's doing too much
16
+ # FIXME [Alice - @alice]: Handle edge case where user is nil
17
+
18
+ # GOOD - AI attribution
19
+ # TODO [@claude]: Consider extracting to a service object
20
+ # NOTE [@claude]: This mirrors the pattern in user_factory.rb
21
+ ```
22
+
23
+ ## For AI Assistants
24
+
25
+ **Your handle is `@claude`.** When you write TODO/FIXME/NOTE/HACK comments, always use `[@claude]`:
26
+
27
+ ```ruby
28
+ # TODO [@claude]: This method could be simplified
29
+ # NOTE [@claude]: Intentionally duplicated from BaseController for isolation
30
+ ```
31
+
32
+ This makes it easy to find AI-generated comments later with `grep -r "@claude"`.
33
+
34
+ ## Attribution Format
35
+
36
+ Required format: `[@handle]` or `[Name - @handle]`
37
+
38
+ | Format | Example | Valid |
39
+ |--------|---------|-------|
40
+ | Handle only | `[@username]` | Yes |
41
+ | Name + handle | `[Alice - @alice]` | Yes |
42
+ | Full name + handle | `[Alice Smith - @alice]` | Yes |
43
+ | No @ symbol | `[username]` | No |
44
+ | No handle | `[Alice]` | No |
45
+ | Plain text | `[some text]` | No |
46
+
47
+ ## Placement
48
+
49
+ Attribution can appear anywhere in the comment:
50
+
51
+ ```ruby
52
+ # Both valid:
53
+ # TODO [@username]: Fix this later
54
+ # TODO: Fix this later [@username]
55
+ ```
56
+
57
+ ## Configuration Options
58
+
59
+ ```yaml
60
+ Claude/TaggedComments:
61
+ Keywords:
62
+ - TODO
63
+ - FIXME
64
+ - NOTE # Default keywords
65
+ - HACK
66
+ - OPTIMIZE
67
+ - REVIEW
68
+ ```
69
+
70
+ ## When Fixing Existing Comments
71
+
72
+ - **Your own new comments:** Use `[@claude]`
73
+ - **Existing anonymous comments:** Ask the human whose attribution to use
74
+ - **Don't guess:** If unsure, ask "Who should I attribute this TODO to?"
@@ -0,0 +1,15 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": "Write|Edit|MultiEdit",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/ruby-lint.sh"
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1,81 @@
1
+ # Linting
2
+
3
+ Run `bin/standardrb` (or `bin/rubocop`) before committing. Fix all errors.
4
+
5
+ ## Quick Reference
6
+
7
+ | Cop | Fix |
8
+ |-----|-----|
9
+ | **Claude/NoFancyUnicode** | Remove emoji and fancy Unicode. Use ASCII text. |
10
+ | **Claude/TaggedComments** | Add attribution: `# TODO: [@handle] description` |
11
+ | **Claude/NoCommentedCode** | Delete commented-out code. Use version control. |
12
+ | **Claude/NoBackwardsCompatHacks** | Delete dead code. Don't preserve for compatibility. |
13
+ | **Claude/NoOverlyDefensiveCode** | Trust internal code. Remove `rescue nil` and excessive `&.` chains. |
14
+ | **Claude/ExplicitVisibility** | Use consistent visibility style (grouped or modifier). |
15
+ | **Claude/MysteryRegex** | Extract long regexes to named constants. |
16
+
17
+ ## When to Ask
18
+
19
+ - If a cop seems wrong for this codebase, ask before disabling
20
+ - If you're unsure how to fix, ask rather than guessing
21
+ - Never add `# rubocop:disable` without discussing first
22
+
23
+ ## Common Patterns
24
+
25
+ ### Tagged Comments
26
+ ```ruby
27
+ # bad
28
+ # TODO fix this later
29
+
30
+ # good
31
+ # TODO: [@username] fix this later
32
+ ```
33
+
34
+ ### Commented Code
35
+ ```ruby
36
+ # bad - delete this, don't comment it out
37
+ # def old_method
38
+ # do_something
39
+ # end
40
+
41
+ # good - just delete it, git has history
42
+ ```
43
+
44
+ ### Defensive Code
45
+ ```ruby
46
+ # bad - swallowing errors
47
+ result = dangerous_call rescue nil
48
+
49
+ # bad - excessive safe navigation
50
+ user&.profile&.settings&.value
51
+
52
+ # bad - defensive nil check
53
+ user && user.name
54
+
55
+ # good - trust internal code
56
+ result = dangerous_call
57
+ user.profile.settings.value
58
+ user.name
59
+ ```
60
+
61
+ ### Visibility Style
62
+
63
+ Check `.rubocop.yml` for `EnforcedStyle` (grouped or modifier):
64
+
65
+ ```ruby
66
+ # grouped style (default)
67
+ class Foo
68
+ def public_method; end
69
+
70
+ private
71
+
72
+ def private_method; end
73
+ end
74
+
75
+ # modifier style
76
+ class Foo
77
+ def public_method; end
78
+
79
+ private def private_method; end
80
+ end
81
+ ```