ivar 0.2.0 → 0.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.augment-guidelines +5 -3
  3. data/.devcontainer/devcontainer.json +28 -20
  4. data/.devcontainer/post-create.sh +18 -0
  5. data/.editorconfig +35 -0
  6. data/.rubocop.yml +6 -0
  7. data/.standard.yml +1 -1
  8. data/.vscode/extensions.json +3 -1
  9. data/.vscode/launch.json +25 -0
  10. data/.vscode/settings.json +38 -2
  11. data/CHANGELOG.md +83 -1
  12. data/README.md +272 -207
  13. data/Rakefile +1 -1
  14. data/VERSION.md +44 -0
  15. data/examples/check_all_block_example.rb +84 -0
  16. data/examples/check_all_example.rb +42 -0
  17. data/examples/inheritance_with_kwarg_init.rb +156 -0
  18. data/examples/inheritance_with_positional_init.rb +142 -0
  19. data/examples/mixed_positional_and_kwarg_init.rb +125 -0
  20. data/examples/require_check_all_example.rb +23 -0
  21. data/examples/sandwich_inheritance.rb +1 -1
  22. data/examples/sandwich_with_accessors.rb +78 -0
  23. data/examples/sandwich_with_block_values.rb +54 -0
  24. data/examples/sandwich_with_checked.rb +0 -1
  25. data/examples/sandwich_with_checked_once.rb +0 -1
  26. data/examples/sandwich_with_initial_values.rb +52 -0
  27. data/examples/sandwich_with_ivar_block.rb +6 -9
  28. data/examples/sandwich_with_ivar_macro.rb +4 -4
  29. data/examples/sandwich_with_kwarg_init.rb +78 -0
  30. data/examples/sandwich_with_positional_init.rb +50 -0
  31. data/examples/sandwich_with_shared_values.rb +54 -0
  32. data/hooks/README.md +42 -0
  33. data/hooks/install.sh +12 -0
  34. data/hooks/pre-commit +54 -0
  35. data/ivar.gemspec +5 -4
  36. data/lib/ivar/check_all.rb +7 -0
  37. data/lib/ivar/check_all_manager.rb +72 -0
  38. data/lib/ivar/check_policy.rb +29 -0
  39. data/lib/ivar/checked/class_methods.rb +19 -0
  40. data/lib/ivar/checked/instance_methods.rb +35 -0
  41. data/lib/ivar/checked.rb +17 -24
  42. data/lib/ivar/declaration.rb +30 -0
  43. data/lib/ivar/explicit_declaration.rb +56 -0
  44. data/lib/ivar/explicit_keyword_declaration.rb +24 -0
  45. data/lib/ivar/explicit_positional_declaration.rb +19 -0
  46. data/lib/ivar/macros.rb +48 -111
  47. data/lib/ivar/manifest.rb +124 -0
  48. data/lib/ivar/policies.rb +13 -1
  49. data/lib/ivar/project_root.rb +59 -0
  50. data/lib/ivar/targeted_prism_analysis.rb +144 -0
  51. data/lib/ivar/validation.rb +6 -29
  52. data/lib/ivar/version.rb +1 -1
  53. data/lib/ivar.rb +141 -9
  54. data/script/console +11 -0
  55. data/script/de-lint +2 -0
  56. data/script/de-lint-unsafe +2 -0
  57. data/script/lint +2 -0
  58. data/script/release +134 -0
  59. data/script/setup +8 -0
  60. data/script/test +2 -0
  61. metadata +41 -5
  62. data/examples/sandwich_with_kwarg.rb +0 -45
  63. data/lib/ivar/auto_check.rb +0 -77
  64. data/lib/ivar/prism_analysis.rb +0 -102
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ivar"
4
+
5
+ class SandwichWithBlockValues
6
+ include Ivar::Checked
7
+
8
+ # Declare condiments with a block that generates default values based on the variable name
9
+ ivar(:@mayo, :@mustard, :@ketchup) { |varname| !varname.include?("mayo") }
10
+
11
+ # Declare bread and cheese with individual values
12
+ ivar "@bread": "wheat", "@cheese": "cheddar"
13
+
14
+ # Declare a variable without an initial value
15
+ ivar :@side
16
+
17
+ def initialize(options = {})
18
+ # Override any condiments based on options
19
+ @mayo = true if options[:add_mayo]
20
+ @mustard = false if options[:no_mustard]
21
+ @ketchup = false if options[:no_ketchup]
22
+
23
+ # Set the side if provided
24
+ @side = options[:side] if options[:side]
25
+ end
26
+
27
+ def to_s
28
+ result = "A #{@bread} sandwich with #{@cheese}"
29
+
30
+ condiments = []
31
+ condiments << "mayo" if @mayo
32
+ condiments << "mustard" if @mustard
33
+ condiments << "ketchup" if @ketchup
34
+
35
+ result += " with #{condiments.join(", ")}" unless condiments.empty?
36
+ result += " and a side of #{@side}" if defined?(@side) && @side
37
+ result
38
+ end
39
+ end
40
+
41
+ # Create a sandwich with default values (no mayo, but has mustard and ketchup)
42
+ sandwich = SandwichWithBlockValues.new
43
+ puts sandwich
44
+ # => "A wheat sandwich with cheddar with mustard, ketchup"
45
+
46
+ # Create a sandwich with mayo added
47
+ sandwich_with_mayo = SandwichWithBlockValues.new(add_mayo: true)
48
+ puts sandwich_with_mayo
49
+ # => "A wheat sandwich with cheddar with mayo, mustard, ketchup"
50
+
51
+ # Create a sandwich with a side
52
+ sandwich_with_side = SandwichWithBlockValues.new(side: "chips")
53
+ puts sandwich_with_side
54
+ # => "A wheat sandwich with cheddar with mustard, ketchup and a side of chips"
@@ -9,7 +9,6 @@ class SandwichWithChecked
9
9
  @bread = "wheat"
10
10
  @cheese = "muenster"
11
11
  @condiments = ["mayo", "mustard"]
12
- # No need for explicit check_ivars call
13
12
  end
14
13
 
15
14
  def to_s
@@ -10,7 +10,6 @@ class SandwichWithCheckedOnce
10
10
  @bread = "wheat"
11
11
  @cheese = "muenster"
12
12
  @condiments = %w[mayo mustard]
13
- # No need for explicit check_ivars call
14
13
  end
15
14
 
16
15
  def to_s
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ivar"
4
+
5
+ class SandwichWithInitialValues
6
+ include Ivar::Checked
7
+
8
+ # Declare instance variables with initial values
9
+ ivar "@bread": "wheat",
10
+ "@cheese": "muenster",
11
+ "@condiments": ["mayo", "mustard"],
12
+ "@pickles": true
13
+
14
+ # Declare a variable without an initial value
15
+ ivar :@side
16
+
17
+ def initialize(extra_condiments = [])
18
+ # The declared variables are already initialized with their values
19
+ # We can modify them here
20
+ @condiments += extra_condiments unless extra_condiments.empty?
21
+
22
+ # We can also check if pickles were requested and adjust condiments
23
+ @condiments.delete("mayo") if @pickles
24
+ end
25
+
26
+ def to_s
27
+ result = "A #{@bread} sandwich with #{@cheese}"
28
+ result += " and #{@condiments.join(", ")}" unless @condiments.empty?
29
+ result += " with pickles" if @pickles
30
+ result += " and a side of #{@side}" if defined?(@side) && @side
31
+ result
32
+ end
33
+
34
+ def add_side(side)
35
+ @side = side
36
+ end
37
+ end
38
+
39
+ # Create a sandwich with default values
40
+ sandwich = SandwichWithInitialValues.new
41
+ puts sandwich
42
+ # => "A wheat sandwich with muenster and mustard with pickles"
43
+
44
+ # Create a sandwich with extra condiments
45
+ sandwich_with_extras = SandwichWithInitialValues.new(["ketchup", "relish"])
46
+ puts sandwich_with_extras
47
+ # => "A wheat sandwich with muenster and mustard, ketchup, relish with pickles"
48
+
49
+ # Add a side
50
+ sandwich.add_side("chips")
51
+ puts sandwich
52
+ # => "A wheat sandwich with muenster and mustard with pickles and a side of chips"
@@ -5,27 +5,24 @@ require "ivar"
5
5
  class SandwichWithIvarBlock
6
6
  include Ivar::Checked
7
7
 
8
- # Pre-declare instance variables with a block that runs before initialization
9
- ivar :@side do
10
- @pickles = true
11
- @condiments = []
12
- end
8
+ # Declare instance variables
9
+ ivar :@side
13
10
 
14
11
  def initialize
15
12
  @bread = "wheat"
16
13
  @cheese = "muenster"
17
- # Note: @pickles is already set to true by the ivar block
18
- # Note: @condiments is already initialized to an empty array by the ivar block
14
+ @pickles = true
15
+ @condiments = []
19
16
  @condiments << "mayo" if !@pickles
20
17
  @condiments << "mustard"
21
- # Note: @side is not set here, but it's pre-initialized to nil
18
+ # @side is declared but intentionally not initialized here
22
19
  end
23
20
 
24
21
  def to_s
25
22
  result = "A #{@bread} sandwich with #{@cheese}"
26
23
  result += " and #{@condiments.join(", ")}" unless @condiments.empty?
27
24
  result += " with pickles" if @pickles
28
- result += " and a side of #{@side}" if @side
25
+ result += " and a side of #{@side}" if defined?(@side) && @side
29
26
  result
30
27
  end
31
28
 
@@ -5,7 +5,7 @@ require "ivar"
5
5
  class SandwichWithIvarMacro
6
6
  include Ivar::Checked
7
7
 
8
- # Pre-declare only instance variables that might be referenced before being set
8
+ # Declare instance variables that might be referenced before being set
9
9
  # You don't need to include variables that are always set in initialize
10
10
  ivar :@side
11
11
 
@@ -13,13 +13,13 @@ class SandwichWithIvarMacro
13
13
  @bread = "wheat"
14
14
  @cheese = "muenster"
15
15
  @condiments = %w[mayo mustard]
16
- # NOTE: @side is not set here, but it's pre-initialized to nil
16
+ # @side is declared but intentionally not initialized here
17
17
  end
18
18
 
19
19
  def to_s
20
20
  result = "A #{@bread} sandwich with #{@cheese} and #{@condiments.join(", ")}"
21
- # This won't trigger a warning because @side is pre-initialized
22
- result += " and a side of #{@side}" if @side
21
+ # Using defined? to safely check for optional @side
22
+ result += " and a side of #{@side}" if defined?(@side) && @side
23
23
  result
24
24
  end
25
25
 
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ivar"
4
+
5
+ class SandwichWithKwargInit
6
+ include Ivar::Checked
7
+
8
+ # Declare instance variables with keyword argument initialization
9
+ # and default values in case they're not provided
10
+ ivar :@bread, init: :kwarg, value: "wheat"
11
+ ivar :@cheese, init: :kwarg, value: "cheddar"
12
+
13
+ # Declare condiments with a default value
14
+ ivar :@condiments, value: []
15
+
16
+ # Declare pickles with both a default value and kwarg initialization
17
+ ivar :@pickles, value: false, init: :kwarg
18
+
19
+ def initialize(extra_condiments: [])
20
+ # The declared variables are already initialized with their values
21
+ # from keyword arguments or defaults
22
+ # Note: bread, cheese, and pickles keywords are "peeled off" and won't be passed to this method
23
+ # But extra_condiments will be passed through
24
+
25
+ # Add default condiments (clear first to avoid duplicates)
26
+ @condiments = []
27
+ @condiments << "mayo" unless @pickles
28
+ @condiments << "mustard"
29
+
30
+ # Add any extra condiments
31
+ @condiments.concat(extra_condiments)
32
+
33
+ # For demonstration, we'll print what keywords were actually received
34
+ received_vars = []
35
+ local_variables.each do |v|
36
+ next if v == :_ || binding.local_variable_get(v).nil?
37
+ received_vars << "#{v}: #{binding.local_variable_get(v).inspect}"
38
+ end
39
+ puts " Initialize received: #{received_vars.join(", ")}"
40
+ end
41
+
42
+ def to_s
43
+ result = "A #{@bread} sandwich with #{@cheese}"
44
+ result += " and #{@condiments.join(", ")}" unless @condiments.empty?
45
+ result += " with pickles" if @pickles
46
+ result
47
+ end
48
+ end
49
+
50
+ # Create a sandwich with default values
51
+ puts "Default sandwich:"
52
+ sandwich = SandwichWithKwargInit.new
53
+ puts sandwich
54
+ # => "A wheat sandwich with cheddar and mayo, mustard"
55
+
56
+ # Create a sandwich with custom bread and cheese
57
+ puts "\nCustom bread and cheese:"
58
+ custom_sandwich = SandwichWithKwargInit.new(bread: "rye", cheese: "swiss")
59
+ puts custom_sandwich
60
+ # => "A rye sandwich with swiss and mayo, mustard"
61
+
62
+ # Create a sandwich with pickles
63
+ puts "\nSandwich with pickles:"
64
+ pickle_sandwich = SandwichWithKwargInit.new(pickles: true)
65
+ puts pickle_sandwich
66
+ # => "A wheat sandwich with cheddar and mustard with pickles"
67
+
68
+ # Create a sandwich with extra condiments (not peeled off)
69
+ puts "\nSandwich with extra condiments:"
70
+ extra_sandwich = SandwichWithKwargInit.new(extra_condiments: ["ketchup", "relish"])
71
+ puts extra_sandwich
72
+ # => "A wheat sandwich with cheddar and mayo, mustard, ketchup, relish"
73
+
74
+ # Create a sandwich with both peeled off and passed through kwargs
75
+ puts "\nSandwich with both types of kwargs:"
76
+ combo_sandwich = SandwichWithKwargInit.new(bread: "sourdough", pickles: true, extra_condiments: ["hot sauce"])
77
+ puts combo_sandwich
78
+ # => "A sourdough sandwich with cheddar and mustard, hot sauce with pickles"
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ivar"
4
+
5
+ class SandwichWithPositionalInit
6
+ include Ivar::Checked
7
+
8
+ # Declare instance variables with positional argument initialization
9
+ # and default values in case they're not provided
10
+ ivar :@bread, init: :positional, value: "wheat"
11
+ ivar :@cheese, init: :positional, value: "cheddar"
12
+
13
+ # Declare condiments with a default value
14
+ ivar :@condiments, value: []
15
+
16
+ # Declare pickles with both a default value and positional initialization
17
+ ivar :@pickles, value: false, init: :positional
18
+
19
+ # Note: Don't define parameters for the peeled-off positional arguments
20
+ def initialize(extra_condiments = [])
21
+ # The declared variables are already initialized with their values
22
+ # from positional arguments or defaults
23
+ @condiments += extra_condiments unless extra_condiments.empty?
24
+
25
+ # We can also check if pickles were requested and adjust condiments
26
+ @condiments.delete("mayo") if @pickles
27
+ end
28
+
29
+ def to_s
30
+ result = "#{@bread} sandwich with #{@cheese} cheese"
31
+ result += " and pickles" if @pickles
32
+ result += ", condiments: #{@condiments.join(", ")}" unless @condiments.empty?
33
+ result
34
+ end
35
+ end
36
+
37
+ # Create a sandwich with all positional arguments
38
+ sandwich1 = SandwichWithPositionalInit.new("rye", "swiss", true, ["mustard"])
39
+ puts "Sandwich 1: #{sandwich1}"
40
+ # => "Sandwich 1: rye sandwich with swiss cheese and pickles, condiments: mustard"
41
+
42
+ # Create a sandwich with some positional arguments
43
+ sandwich2 = SandwichWithPositionalInit.new("sourdough", "provolone", ["mayo", "mustard"])
44
+ puts "Sandwich 2: #{sandwich2}"
45
+ # => "Sandwich 2: sourdough sandwich with provolone cheese, condiments: mayo, mustard"
46
+
47
+ # Create a sandwich with default values
48
+ sandwich3 = SandwichWithPositionalInit.new
49
+ puts "Sandwich 3: #{sandwich3}"
50
+ # => "Sandwich 3: wheat sandwich with cheddar cheese"
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ivar"
4
+
5
+ class SandwichWithSharedValues
6
+ include Ivar::Checked
7
+
8
+ # Declare multiple condiments with the same initial value (true)
9
+ ivar :@mayo, :@mustard, :@ketchup, value: true
10
+
11
+ # Declare bread and cheese with individual values
12
+ ivar "@bread": "wheat", "@cheese": "cheddar"
13
+
14
+ # Declare a variable without an initial value
15
+ ivar :@side
16
+
17
+ def initialize(options = {})
18
+ # Override any condiments based on options
19
+ @mayo = false if options[:no_mayo]
20
+ @mustard = false if options[:no_mustard]
21
+ @ketchup = false if options[:no_ketchup]
22
+
23
+ # Set the side if provided
24
+ @side = options[:side] if options[:side]
25
+ end
26
+
27
+ def to_s
28
+ result = "A #{@bread} sandwich with #{@cheese}"
29
+
30
+ condiments = []
31
+ condiments << "mayo" if @mayo
32
+ condiments << "mustard" if @mustard
33
+ condiments << "ketchup" if @ketchup
34
+
35
+ result += " with #{condiments.join(", ")}" unless condiments.empty?
36
+ result += " and a side of #{@side}" if defined?(@side) && @side
37
+ result
38
+ end
39
+ end
40
+
41
+ # Create a sandwich with default values (all condiments)
42
+ sandwich = SandwichWithSharedValues.new
43
+ puts sandwich
44
+ # => "A wheat sandwich with cheddar with mayo, mustard, ketchup"
45
+
46
+ # Create a sandwich with no mayo
47
+ sandwich_no_mayo = SandwichWithSharedValues.new(no_mayo: true)
48
+ puts sandwich_no_mayo
49
+ # => "A wheat sandwich with cheddar with mustard, ketchup"
50
+
51
+ # Create a sandwich with a side
52
+ sandwich_with_side = SandwichWithSharedValues.new(side: "chips")
53
+ puts sandwich_with_side
54
+ # => "A wheat sandwich with cheddar with mayo, mustard, ketchup and a side of chips"
data/hooks/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # Git Hooks
2
+
3
+ This directory contains Git hooks for the ivar project.
4
+
5
+ ## Available Hooks
6
+
7
+ - **pre-commit**: Automatically checks and fixes linting issues before committing.
8
+
9
+ ## Installation
10
+
11
+ To install the hooks, run:
12
+
13
+ ```bash
14
+ ./hooks/install.sh
15
+ ```
16
+
17
+ This will copy the hooks to your local `.git/hooks` directory and make them executable.
18
+
19
+ ## Automatic Installation
20
+
21
+ The hooks are automatically installed when you open the project in a devcontainer.
22
+
23
+ ## Manual Installation
24
+
25
+ If you prefer to install the hooks manually, you can copy them to your `.git/hooks` directory:
26
+
27
+ ```bash
28
+ cp hooks/pre-commit .git/hooks/pre-commit
29
+ chmod +x .git/hooks/pre-commit
30
+ ```
31
+
32
+ ## How the Pre-commit Hook Works
33
+
34
+ The pre-commit hook:
35
+
36
+ 1. Identifies staged Ruby files
37
+ 2. Checks them for linting issues using standardrb
38
+ 3. If issues are found, attempts to automatically fix them
39
+ 4. Adds the fixed files back to the staging area
40
+ 5. Performs a final check to ensure all issues are fixed
41
+
42
+ If any issues cannot be automatically fixed, the commit will be aborted with an error message.
data/hooks/install.sh ADDED
@@ -0,0 +1,12 @@
1
+ #!/bin/sh
2
+
3
+ # Create hooks directory if it doesn't exist
4
+ mkdir -p .git/hooks
5
+
6
+ # Copy pre-commit hook
7
+ cp hooks/pre-commit .git/hooks/pre-commit
8
+
9
+ # Make pre-commit hook executable
10
+ chmod +x .git/hooks/pre-commit
11
+
12
+ echo "Git hooks installed successfully!"
data/hooks/pre-commit ADDED
@@ -0,0 +1,54 @@
1
+ #!/bin/sh
2
+
3
+ # Get list of staged Ruby files
4
+ STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep "\.rb$")
5
+
6
+ # Exit if no Ruby files are staged
7
+ if [ -z "$STAGED_FILES" ]; then
8
+ echo "No Ruby files staged for commit. Skipping linting."
9
+ exit 0
10
+ fi
11
+
12
+ # Check if there are any linting issues
13
+ echo "Checking for linting issues..."
14
+ ./script/lint $STAGED_FILES
15
+ LINT_RESULT=$?
16
+
17
+ # If there are linting issues, try to fix them automatically
18
+ if [ $LINT_RESULT -ne 0 ]; then
19
+ echo "Linting issues found. Attempting to fix automatically..."
20
+
21
+ # Stash unstaged changes
22
+ git stash -q --keep-index
23
+
24
+ # Run de-lint to auto-fix
25
+ ./script/de-lint $STAGED_FILES
26
+ FIX_RESULT=$?
27
+
28
+ # If auto-fix was successful, add the fixed files back to staging
29
+ if [ $FIX_RESULT -eq 0 ]; then
30
+ echo "Auto-fix successful. Adding fixed files to staging area..."
31
+ git add $STAGED_FILES
32
+ else
33
+ echo "Auto-fix failed. Please fix the issues manually."
34
+ # Restore unstaged changes
35
+ git stash pop -q
36
+ exit 1
37
+ fi
38
+
39
+ # Restore unstaged changes
40
+ git stash pop -q 2>/dev/null || true
41
+ fi
42
+
43
+ # Run a final check to make sure everything is fixed
44
+ echo "Running final linting check..."
45
+ ./script/lint $STAGED_FILES
46
+ FINAL_RESULT=$?
47
+
48
+ if [ $FINAL_RESULT -ne 0 ]; then
49
+ echo "Linting issues still exist after auto-fix. Please fix them manually."
50
+ exit 1
51
+ fi
52
+
53
+ echo "Linting passed. Proceeding with commit."
54
+ exit 0
data/ivar.gemspec CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
9
9
  spec.email = ["avdi@avdi.codes"]
10
10
 
11
11
  spec.summary = "Automatically check instance variables for typos."
12
- spec.description = <<~EOF
12
+ spec.description = <<~DESCRIPTION
13
13
  Ruby instance variables are so convenient - you don't even need to declare them!
14
14
  But... they are also dangerous, because a mispelled variable name results in `nil`
15
15
  instead of an error.
@@ -22,11 +22,11 @@ Gem::Specification.new do |spec|
22
22
  dynamic, a little bit static. It doesn't encumber your instance variable reads and
23
23
  writes with any extra checking. And with the `:warn_once` policy, it won't overwhelm
24
24
  you with output.
25
- EOF
25
+ DESCRIPTION
26
26
 
27
27
  spec.homepage = "https://github.com/avdi/ivar"
28
28
  spec.license = "MIT"
29
- spec.required_ruby_version = ">= 3.4.0"
29
+ spec.required_ruby_version = ">= 3.3.0"
30
30
 
31
31
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
32
32
 
@@ -39,7 +39,8 @@ Gem::Specification.new do |spec|
39
39
  spec.files = Dir.chdir(__dir__) do
40
40
  `git ls-files -z`.split("\x0").reject do |f|
41
41
  (File.expand_path(f) == __FILE__) ||
42
- f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
42
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile]) ||
43
+ f.end_with?(".gem")
43
44
  end
44
45
  end
45
46
  spec.require_paths = ["lib"]
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../ivar"
4
+
5
+ # Enable automatic inclusion of Ivar::Checked in all classes and modules
6
+ # defined within the project root.
7
+ Ivar.check_all
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module Ivar
6
+ # Manages automatic inclusion of Ivar::Checked in classes and modules
7
+ class CheckAllManager
8
+ def initialize
9
+ @trace_point = nil
10
+ @mutex = Mutex.new
11
+ end
12
+
13
+ # Enables automatic inclusion of Ivar::Checked in all classes and modules
14
+ # defined within the project root.
15
+ #
16
+ # @param project_root [String] The project root directory path
17
+ # @param block [Proc] Optional block. If provided, auto-checking is only active
18
+ # for the duration of the block. Otherwise, it remains active indefinitely.
19
+ # @return [void]
20
+ def enable(project_root, &block)
21
+ disable if @trace_point
22
+ root_pathname = Pathname.new(project_root)
23
+ @mutex.synchronize do
24
+ # :end means "end of module or class definition" in TracePoint
25
+ @trace_point = TracePoint.new(:end) do |tp|
26
+ next unless tp.path
27
+ file_path = Pathname.new(File.expand_path(tp.path))
28
+ if file_path.to_s.start_with?(root_pathname.to_s)
29
+ klass = tp.self
30
+ next if klass.included_modules.include?(Ivar::Checked)
31
+ klass.include(Ivar::Checked)
32
+ end
33
+ end
34
+
35
+ @trace_point.enable
36
+ end
37
+
38
+ if block
39
+ begin
40
+ yield
41
+ ensure
42
+ disable
43
+ end
44
+ end
45
+
46
+ nil
47
+ end
48
+
49
+ # Disables automatic inclusion of Ivar::Checked in classes and modules.
50
+ # @return [void]
51
+ def disable
52
+ @mutex.synchronize do
53
+ if @trace_point
54
+ @trace_point.disable
55
+ @trace_point = nil
56
+ end
57
+ end
58
+ end
59
+
60
+ # Returns whether check_all is currently enabled
61
+ # @return [Boolean] true if check_all is enabled, false otherwise
62
+ def enabled?
63
+ @mutex.synchronize { !@trace_point.nil? && @trace_point.enabled? }
64
+ end
65
+
66
+ # Returns the current trace point (mainly for testing)
67
+ # @return [TracePoint, nil] The current trace point or nil if not enabled
68
+ def trace_point
69
+ @mutex.synchronize { @trace_point }
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ivar
4
+ # Module for adding instance variable check policy configuration to classes.
5
+ # This module provides a way to set and inherit check policies for instance variables.
6
+ # When extended in a class, it allows setting a class-specific policy that overrides
7
+ # the global Ivar policy.
8
+ module CheckPolicy
9
+ # Set or get the check policy for this class
10
+ # @param policy [Symbol, Policy] The check policy to set
11
+ # @param options [Hash] Additional options for the policy
12
+ # @return [Symbol, Policy] The current check policy
13
+ def ivar_check_policy(policy = nil, **options)
14
+ if policy.nil?
15
+ @__ivar_check_policy || Ivar.check_policy
16
+ else
17
+ @__ivar_check_policy = options.empty? ? policy : [policy, options]
18
+ end
19
+ end
20
+
21
+ # Ensure subclasses inherit the check policy from their parent
22
+ # This method is called automatically when a class is inherited
23
+ # @param subclass [Class] The subclass that is inheriting from this class
24
+ def inherited(subclass)
25
+ super
26
+ subclass.instance_variable_set(:@__ivar_check_policy, @__ivar_check_policy)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "instance_methods"
4
+
5
+ module Ivar
6
+ module Checked
7
+ # Class methods added to the including class.
8
+ # These methods ensure proper inheritance of Checked functionality.
9
+ module ClassMethods
10
+ # Ensure subclasses inherit the Checked functionality
11
+ # This method is called automatically when a class is inherited
12
+ # @param subclass [Class] The subclass that is inheriting from this class
13
+ def inherited(subclass)
14
+ super
15
+ subclass.prepend(Ivar::Checked::InstanceMethods)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ivar
4
+ module Checked
5
+ # Instance methods that will be prepended to the including class.
6
+ # These methods provide the core functionality for automatic instance variable validation.
7
+ module InstanceMethods
8
+ # The semantics of prepend are such that the super method becomes wholly inaccessible. So if we override a method
9
+ # (like, say, initialize), we have to stash the original method implementation if we ever want to find out its
10
+ # file and line number.
11
+ def self.prepend_features(othermod)
12
+ (instance_methods(false) | private_instance_methods(false)).each do |method_name|
13
+ Ivar.stash_method(othermod, method_name)
14
+ end
15
+ super
16
+ end
17
+
18
+ # Wrap the initialize method to automatically call check_ivars
19
+ # This method handles the initialization process, including:
20
+ # 1. Processing manifest declarations before calling super
21
+ # 3. Checking instance variables for validity
22
+ def initialize(*args, **kwargs, &block)
23
+ if @__ivar_skip_init
24
+ super
25
+ else
26
+ @__ivar_skip_init = true
27
+ manifest = Ivar.get_or_create_manifest(self.class)
28
+ manifest.process_before_init(self, args, kwargs)
29
+ super
30
+ check_ivars
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end