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.
- checksums.yaml +4 -4
- data/.augment-guidelines +5 -3
- data/.devcontainer/devcontainer.json +28 -20
- data/.devcontainer/post-create.sh +18 -0
- data/.editorconfig +35 -0
- data/.rubocop.yml +6 -0
- data/.standard.yml +1 -1
- data/.vscode/extensions.json +3 -1
- data/.vscode/launch.json +25 -0
- data/.vscode/settings.json +38 -2
- data/CHANGELOG.md +83 -1
- data/README.md +272 -207
- data/Rakefile +1 -1
- data/VERSION.md +44 -0
- data/examples/check_all_block_example.rb +84 -0
- data/examples/check_all_example.rb +42 -0
- data/examples/inheritance_with_kwarg_init.rb +156 -0
- data/examples/inheritance_with_positional_init.rb +142 -0
- data/examples/mixed_positional_and_kwarg_init.rb +125 -0
- data/examples/require_check_all_example.rb +23 -0
- data/examples/sandwich_inheritance.rb +1 -1
- data/examples/sandwich_with_accessors.rb +78 -0
- data/examples/sandwich_with_block_values.rb +54 -0
- data/examples/sandwich_with_checked.rb +0 -1
- data/examples/sandwich_with_checked_once.rb +0 -1
- data/examples/sandwich_with_initial_values.rb +52 -0
- data/examples/sandwich_with_ivar_block.rb +6 -9
- data/examples/sandwich_with_ivar_macro.rb +4 -4
- data/examples/sandwich_with_kwarg_init.rb +78 -0
- data/examples/sandwich_with_positional_init.rb +50 -0
- data/examples/sandwich_with_shared_values.rb +54 -0
- data/hooks/README.md +42 -0
- data/hooks/install.sh +12 -0
- data/hooks/pre-commit +54 -0
- data/ivar.gemspec +5 -4
- data/lib/ivar/check_all.rb +7 -0
- data/lib/ivar/check_all_manager.rb +72 -0
- data/lib/ivar/check_policy.rb +29 -0
- data/lib/ivar/checked/class_methods.rb +19 -0
- data/lib/ivar/checked/instance_methods.rb +35 -0
- data/lib/ivar/checked.rb +17 -24
- data/lib/ivar/declaration.rb +30 -0
- data/lib/ivar/explicit_declaration.rb +56 -0
- data/lib/ivar/explicit_keyword_declaration.rb +24 -0
- data/lib/ivar/explicit_positional_declaration.rb +19 -0
- data/lib/ivar/macros.rb +48 -111
- data/lib/ivar/manifest.rb +124 -0
- data/lib/ivar/policies.rb +13 -1
- data/lib/ivar/project_root.rb +59 -0
- data/lib/ivar/targeted_prism_analysis.rb +144 -0
- data/lib/ivar/validation.rb +6 -29
- data/lib/ivar/version.rb +1 -1
- data/lib/ivar.rb +141 -9
- data/script/console +11 -0
- data/script/de-lint +2 -0
- data/script/de-lint-unsafe +2 -0
- data/script/lint +2 -0
- data/script/release +134 -0
- data/script/setup +8 -0
- data/script/test +2 -0
- metadata +41 -5
- data/examples/sandwich_with_kwarg.rb +0 -45
- data/lib/ivar/auto_check.rb +0 -77
- data/lib/ivar/prism_analysis.rb +0 -102
data/lib/ivar/checked.rb
CHANGED
@@ -1,40 +1,33 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "validation"
|
4
|
+
require_relative "macros"
|
5
|
+
require_relative "check_policy"
|
6
|
+
require_relative "checked/class_methods"
|
7
|
+
require_relative "checked/instance_methods"
|
4
8
|
|
5
9
|
module Ivar
|
6
|
-
# Provides automatic validation for instance variables
|
7
|
-
# When included
|
10
|
+
# Provides automatic validation for instance variables.
|
11
|
+
# When included in a class, this module:
|
12
|
+
# 1. Automatically calls check_ivars after initialization
|
13
|
+
# 2. Extends the class with CheckPolicy for policy configuration
|
14
|
+
# 3. Extends the class with Macros for ivar declarations
|
15
|
+
# 4. Sets a default check policy of :warn
|
16
|
+
# 5. Handles proper inheritance of these behaviors in subclasses
|
8
17
|
module Checked
|
9
18
|
# When this module is included in a class, it extends the class
|
10
19
|
# with ClassMethods and includes the Validation module
|
20
|
+
# @param base [Class] The class that is including this module
|
11
21
|
def self.included(base)
|
12
22
|
base.include(Validation)
|
13
23
|
base.extend(ClassMethods)
|
24
|
+
base.extend(CheckPolicy)
|
25
|
+
base.extend(Macros)
|
14
26
|
base.prepend(InstanceMethods)
|
15
|
-
end
|
16
|
-
|
17
|
-
# Class methods added to the including class
|
18
|
-
module ClassMethods
|
19
|
-
# Hook method called when the module is included
|
20
|
-
def inherited(subclass)
|
21
|
-
super
|
22
|
-
# Ensure subclasses also get the initialize wrapper
|
23
|
-
subclass.prepend(InstanceMethods)
|
24
|
-
end
|
25
|
-
end
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
def initialize(*args, **kwargs, &block)
|
31
|
-
# Call the original initialize method
|
32
|
-
super
|
33
|
-
# Automatically check instance variables
|
34
|
-
# We need to collect all instance variables from the current object
|
35
|
-
# and pass them to check_ivars_once to ensure they're all recognized
|
36
|
-
check_ivars_once
|
37
|
-
end
|
28
|
+
# Set default policy for Checked to :warn
|
29
|
+
# This can be overridden by calling ivar_check_policy in the class
|
30
|
+
base.ivar_check_policy(Ivar.check_policy)
|
38
31
|
end
|
39
32
|
end
|
40
33
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ivar
|
4
|
+
# Base class for all declarations
|
5
|
+
class Declaration
|
6
|
+
# @return [Symbol] The name of the instance variable
|
7
|
+
attr_reader :name, :manifest
|
8
|
+
|
9
|
+
# Initialize a new declaration
|
10
|
+
# @param name [Symbol, String] The name of the instance variable
|
11
|
+
def initialize(name, manifest)
|
12
|
+
@name = name.to_sym
|
13
|
+
@manifest = manifest
|
14
|
+
end
|
15
|
+
|
16
|
+
# Called when the declaration is added to a class
|
17
|
+
# @param klass [Class, Module] The class or module the declaration is added to
|
18
|
+
def on_declare(klass)
|
19
|
+
# Base implementation does nothing
|
20
|
+
end
|
21
|
+
|
22
|
+
# Called before object initialization
|
23
|
+
# @param instance [Object] The object being initialized
|
24
|
+
# @param args [Array] Positional arguments
|
25
|
+
# @param kwargs [Hash] Keyword arguments
|
26
|
+
def before_init(instance, args, kwargs)
|
27
|
+
# Base implementation does nothing
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "declaration"
|
4
|
+
require_relative "macros"
|
5
|
+
|
6
|
+
module Ivar
|
7
|
+
# Represents an explicit declaration from the ivar macro
|
8
|
+
class ExplicitDeclaration < Declaration
|
9
|
+
# Initialize a new explicit declaration
|
10
|
+
# @param name [Symbol, String] The name of the instance variable
|
11
|
+
# @param options [Hash] Options for the declaration
|
12
|
+
def initialize(name, manifest, options = {})
|
13
|
+
super(name, manifest)
|
14
|
+
@init_method = options[:init]
|
15
|
+
@initial_value = options[:value]
|
16
|
+
@reader = options[:reader] || false
|
17
|
+
@writer = options[:writer] || false
|
18
|
+
@accessor = options[:accessor] || false
|
19
|
+
@init_block = options[:block]
|
20
|
+
end
|
21
|
+
|
22
|
+
# Called when the declaration is added to a class
|
23
|
+
# @param klass [Class, Module] The class or module the declaration is added to
|
24
|
+
def on_declare(klass)
|
25
|
+
add_accessor_methods(klass)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Check if this declaration uses keyword argument initialization
|
29
|
+
# @return [Boolean] Whether this declaration uses keyword argument initialization
|
30
|
+
def kwarg_init? = false
|
31
|
+
|
32
|
+
# Called before object initialization
|
33
|
+
# @param instance [Object] The object being initialized
|
34
|
+
# @param args [Array] Positional arguments
|
35
|
+
# @param kwargs [Hash] Keyword arguments
|
36
|
+
def before_init(instance, args, kwargs)
|
37
|
+
if @init_block
|
38
|
+
instance.instance_variable_set(@name, @init_block.call(@name))
|
39
|
+
end
|
40
|
+
if @initial_value != Ivar::Macros::UNSET
|
41
|
+
instance.instance_variable_set(@name, @initial_value)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# Add accessor methods to the class
|
48
|
+
# @param klass [Class, Module] The class to add methods to
|
49
|
+
def add_accessor_methods(klass)
|
50
|
+
var_name = @name.to_s.delete_prefix("@")
|
51
|
+
|
52
|
+
klass.__send__(:attr_reader, var_name) if @reader || @accessor
|
53
|
+
klass.__send__(:attr_writer, var_name) if @writer || @accessor
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "explicit_declaration"
|
4
|
+
|
5
|
+
module Ivar
|
6
|
+
# Represents an explicit declaration that initializes from keyword arguments
|
7
|
+
class ExplicitKeywordDeclaration < ExplicitDeclaration
|
8
|
+
# Check if this declaration uses keyword argument initialization
|
9
|
+
# @return [Boolean] Whether this declaration uses keyword argument initialization
|
10
|
+
def kwarg_init? = true
|
11
|
+
|
12
|
+
# Called before object initialization
|
13
|
+
# @param instance [Object] The object being initialized
|
14
|
+
# @param args [Array] Positional arguments
|
15
|
+
# @param kwargs [Hash] Keyword arguments
|
16
|
+
def before_init(instance, args, kwargs)
|
17
|
+
super
|
18
|
+
kwarg_name = @name.to_s.delete_prefix("@").to_sym
|
19
|
+
if kwargs.key?(kwarg_name)
|
20
|
+
instance.instance_variable_set(@name, kwargs.delete(kwarg_name))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "explicit_declaration"
|
4
|
+
|
5
|
+
module Ivar
|
6
|
+
# Represents an explicit declaration that initializes from positional arguments
|
7
|
+
class ExplicitPositionalDeclaration < ExplicitDeclaration
|
8
|
+
# Called before object initialization
|
9
|
+
# @param instance [Object] The object being initialized
|
10
|
+
# @param args [Array] Positional arguments
|
11
|
+
# @param kwargs [Hash] Keyword arguments
|
12
|
+
def before_init(instance, args, kwargs)
|
13
|
+
super
|
14
|
+
if args.length > 0
|
15
|
+
instance.instance_variable_set(@name, args.shift)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/ivar/macros.rb
CHANGED
@@ -3,121 +3,58 @@
|
|
3
3
|
module Ivar
|
4
4
|
# Provides macros for working with instance variables
|
5
5
|
module Macros
|
6
|
+
# Special flag object to detect when a parameter is not provided
|
7
|
+
UNSET = Object.new.freeze
|
8
|
+
|
6
9
|
# When this module is extended, it adds class methods to the extending class
|
7
10
|
def self.extended(base)
|
8
|
-
#
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
# @
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
def ivar_pre_declared
|
53
|
-
instance_variable_get(:@__ivar_pre_declared_ivars) || []
|
54
|
-
end
|
55
|
-
|
56
|
-
# Get the keyword argument mappings for this class
|
57
|
-
# @return [Array<Symbol>] Keyword argument mappings
|
58
|
-
def ivar_kwarg_mappings
|
59
|
-
instance_variable_get(:@__ivar_kwarg_mappings) || []
|
60
|
-
end
|
61
|
-
|
62
|
-
# Get the initialization block for this class
|
63
|
-
# @return [Proc, nil] The initialization block or nil if none was provided
|
64
|
-
def ivar_init_block
|
65
|
-
instance_variable_get(:@__ivar_init_block)
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
# Module to pre-initialize instance variables
|
70
|
-
module PreInitializeIvars
|
71
|
-
# Initialize pre-declared instance variables to nil
|
72
|
-
def initialize_pre_declared_ivars
|
73
|
-
klass = self.class
|
74
|
-
while klass.respond_to?(:ivar_pre_declared)
|
75
|
-
klass.ivar_pre_declared.each do |ivar|
|
76
|
-
instance_variable_set(ivar, nil) unless instance_variable_defined?(ivar)
|
11
|
+
# Get or create a manifest for this class
|
12
|
+
Ivar.get_or_create_manifest(base)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Declares instance variables that should be considered valid
|
16
|
+
# without being explicitly initialized
|
17
|
+
# @param ivars [Array<Symbol>] Instance variables to declare
|
18
|
+
# @param value [Object] Optional value to initialize all declared variables with
|
19
|
+
# Example: ivar :@foo, :@bar, value: 123
|
20
|
+
# @param init [Symbol] Initialization method for the variable
|
21
|
+
# :kwarg or :keyword - initializes from a keyword argument with the same name
|
22
|
+
# Example: ivar :@foo, init: :kwarg
|
23
|
+
# @param reader [Boolean] If true, creates attr_reader for all declared variables
|
24
|
+
# Example: ivar :@foo, :@bar, reader: true
|
25
|
+
# @param writer [Boolean] If true, creates attr_writer for all declared variables
|
26
|
+
# Example: ivar :@foo, :@bar, writer: true
|
27
|
+
# @param accessor [Boolean] If true, creates attr_accessor for all declared variables
|
28
|
+
# Example: ivar :@foo, :@bar, accessor: true
|
29
|
+
# @param ivars_with_values [Hash] Individual initial values for instance variables
|
30
|
+
# Example: ivar "@foo": 123, "@bar": 456
|
31
|
+
# @yield [varname] Block to generate initial values based on variable name
|
32
|
+
# Example: ivar(:@foo, :@bar) { |varname| "#{varname} default" }
|
33
|
+
def ivar(*ivars, value: UNSET, init: nil, reader: false, writer: false, accessor: false, **ivars_with_values, &block)
|
34
|
+
manifest = Ivar.get_or_create_manifest(self)
|
35
|
+
|
36
|
+
ivar_hash = ivars.map { |ivar| [ivar, value] }.to_h.merge(ivars_with_values)
|
37
|
+
|
38
|
+
ivar_hash.each do |ivar_name, ivar_value|
|
39
|
+
raise ArgumentError, "ivars must be symbols (#{ivar_name.inspect})" unless ivar_name.is_a?(Symbol)
|
40
|
+
raise ArgumentError, "ivar names must start with @ (#{ivar_name.inspect})" unless /\A@/.match?(ivar_name)
|
41
|
+
|
42
|
+
options = {init:, value: ivar_value, reader:, writer:, accessor:, block:}
|
43
|
+
|
44
|
+
declaration = case init
|
45
|
+
when :kwarg, :keyword
|
46
|
+
Ivar::ExplicitKeywordDeclaration.new(ivar_name, manifest, options)
|
47
|
+
when :arg, :positional
|
48
|
+
# TODO: probably fail if a duplicate positional comes in
|
49
|
+
# There aren't any obvious semantics for it.
|
50
|
+
Ivar::ExplicitPositionalDeclaration.new(ivar_name, manifest, options)
|
51
|
+
when nil
|
52
|
+
Ivar::ExplicitDeclaration.new(ivar_name, manifest, options)
|
53
|
+
else
|
54
|
+
raise ArgumentError, "Invalid init method: #{init.inspect}"
|
77
55
|
end
|
78
|
-
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
# Execute the initialization block in the context of the instance
|
83
|
-
def execute_ivar_init_block
|
84
|
-
klass = self.class
|
85
|
-
while klass.respond_to?(:ivar_init_block)
|
86
|
-
block = klass.ivar_init_block
|
87
|
-
instance_eval(&block) if block
|
88
|
-
klass = klass.superclass
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
# Get all keyword argument mappings from the class hierarchy
|
93
|
-
# @return [Array<Symbol>] All keyword argument mappings
|
94
|
-
def all_kwarg_mappings
|
95
|
-
mappings = []
|
96
|
-
klass = self.class
|
97
|
-
while klass.respond_to?(:ivar_kwarg_mappings)
|
98
|
-
mappings.concat(klass.ivar_kwarg_mappings)
|
99
|
-
klass = klass.superclass
|
56
|
+
manifest.add_explicit_declaration(declaration)
|
100
57
|
end
|
101
|
-
mappings
|
102
|
-
end
|
103
|
-
|
104
|
-
# Initialize instance variables from keyword arguments
|
105
|
-
# @param kwargs [Hash] Keyword arguments
|
106
|
-
# @return [Hash] Remaining keyword arguments
|
107
|
-
def initialize_from_kwargs(kwargs)
|
108
|
-
remaining_kwargs = kwargs.dup
|
109
|
-
|
110
|
-
all_kwarg_mappings.each do |ivar|
|
111
|
-
# Convert @ivar_name to ivar_name for keyword lookup
|
112
|
-
key = ivar.to_s.delete_prefix("@").to_sym
|
113
|
-
|
114
|
-
if remaining_kwargs.key?(key)
|
115
|
-
instance_variable_set(ivar, remaining_kwargs[key])
|
116
|
-
remaining_kwargs.delete(key)
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
|
-
remaining_kwargs
|
121
58
|
end
|
122
59
|
end
|
123
60
|
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "declaration"
|
4
|
+
require_relative "explicit_declaration"
|
5
|
+
require_relative "explicit_keyword_declaration"
|
6
|
+
require_relative "explicit_positional_declaration"
|
7
|
+
|
8
|
+
module Ivar
|
9
|
+
# Represents a manifest of instance variable declarations for a class/module
|
10
|
+
class Manifest
|
11
|
+
# @return [Class, Module] The class or module this manifest is associated with
|
12
|
+
attr_reader :owner
|
13
|
+
|
14
|
+
# Initialize a new manifest
|
15
|
+
# @param owner [Class, Module] The class or module this manifest is associated with
|
16
|
+
def initialize(owner)
|
17
|
+
@owner = owner
|
18
|
+
@declarations_by_name = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [Hash<Symbol, Declaration>] The declarations hash keyed by variable name
|
22
|
+
attr_reader :declarations_by_name
|
23
|
+
|
24
|
+
# @return [Array<Declaration>] The declarations in this manifest
|
25
|
+
def declarations
|
26
|
+
@declarations_by_name.values
|
27
|
+
end
|
28
|
+
|
29
|
+
# Add an explicit declaration to the manifest
|
30
|
+
# @param declaration [ExplicitDeclaration] The declaration to add
|
31
|
+
# @return [ExplicitDeclaration] The added declaration
|
32
|
+
def add_explicit_declaration(declaration)
|
33
|
+
name = declaration.name
|
34
|
+
@declarations_by_name[name] = declaration
|
35
|
+
declaration.on_declare(@owner)
|
36
|
+
declaration
|
37
|
+
end
|
38
|
+
|
39
|
+
# Get all ancestor manifests in reverse order (from highest to lowest in the hierarchy)
|
40
|
+
# Only includes ancestors that have existing manifests
|
41
|
+
# @return [Array<Manifest>] Array of ancestor manifests
|
42
|
+
def ancestor_manifests
|
43
|
+
return [] unless @owner.respond_to?(:ancestors)
|
44
|
+
|
45
|
+
@owner
|
46
|
+
.ancestors.reject { |ancestor| ancestor == @owner }
|
47
|
+
.filter_map { |ancestor| Ivar.get_manifest(ancestor, create: false) }
|
48
|
+
.reverse
|
49
|
+
end
|
50
|
+
|
51
|
+
def explicitly_declared_ivars
|
52
|
+
all_declarations.grep(ExplicitDeclaration).map(&:name)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Get all declarations, including those from ancestor manifests
|
56
|
+
# @return [Array<Declaration>] All declarations
|
57
|
+
def all_declarations
|
58
|
+
ancestor_manifests
|
59
|
+
.flat_map(&:declarations)
|
60
|
+
.+(declarations)
|
61
|
+
# use hash stores to preserve order and deduplicate by name
|
62
|
+
.each_with_object({}) { |decl, acc| acc[decl.name] = decl }
|
63
|
+
.values
|
64
|
+
end
|
65
|
+
|
66
|
+
# Check if a variable is declared in this manifest or ancestor manifests
|
67
|
+
# @param name [Symbol, String] The variable name
|
68
|
+
# @return [Boolean] Whether the variable is declared
|
69
|
+
def declared?(name)
|
70
|
+
name = name.to_sym
|
71
|
+
|
72
|
+
# Check in this manifest first
|
73
|
+
return true if @declarations_by_name.key?(name)
|
74
|
+
|
75
|
+
# Then check in ancestor manifests
|
76
|
+
ancestor_manifests.any? do |ancestor_manifest|
|
77
|
+
ancestor_manifest.declarations_by_name.key?(name)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Get a declaration by name
|
82
|
+
# @param name [Symbol, String] The variable name
|
83
|
+
# @return [Declaration, nil] The declaration, or nil if not found
|
84
|
+
def get_declaration(name)
|
85
|
+
name = name.to_sym
|
86
|
+
|
87
|
+
# Check in this manifest first
|
88
|
+
return @declarations_by_name[name] if @declarations_by_name.key?(name)
|
89
|
+
|
90
|
+
# Then check in ancestor manifests, starting from the closest ancestor
|
91
|
+
ancestor_manifests.each do |ancestor_manifest|
|
92
|
+
if ancestor_manifest.declarations_by_name.key?(name)
|
93
|
+
return ancestor_manifest.declarations_by_name[name]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
nil
|
98
|
+
end
|
99
|
+
|
100
|
+
# Get all explicit declarations
|
101
|
+
# @return [Array<ExplicitDeclaration>] All explicit declarations
|
102
|
+
def explicit_declarations
|
103
|
+
declarations.select { |decl| decl.is_a?(ExplicitDeclaration) }
|
104
|
+
end
|
105
|
+
|
106
|
+
# Process before_init callbacks for all declarations
|
107
|
+
# @param instance [Object] The object being initialized
|
108
|
+
# @param args [Array] Positional arguments
|
109
|
+
# @param kwargs [Hash] Keyword arguments
|
110
|
+
# @return [Array, Hash] The modified args and kwargs
|
111
|
+
def process_before_init(instance, args, kwargs)
|
112
|
+
# Get all declarations from parent to child, with child declarations taking precedence
|
113
|
+
declarations_to_process = all_declarations
|
114
|
+
|
115
|
+
# Process all initializations in a single pass
|
116
|
+
# The before_init method will handle keyword arguments with proper precedence
|
117
|
+
declarations_to_process.each do |declaration|
|
118
|
+
declaration.before_init(instance, args, kwargs)
|
119
|
+
end
|
120
|
+
|
121
|
+
[args, kwargs]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
data/lib/ivar/policies.rb
CHANGED
@@ -115,12 +115,24 @@ module Ivar
|
|
115
115
|
end
|
116
116
|
end
|
117
117
|
|
118
|
+
# Policy that does nothing (no-op) for unknown instance variables
|
119
|
+
class NonePolicy < Policy
|
120
|
+
# Handle unknown instance variables by doing nothing
|
121
|
+
# @param unknown_refs [Array<Hash>] References to unknown instance variables
|
122
|
+
# @param klass [Class] The class being checked
|
123
|
+
# @param allowed_ivars [Array<Symbol>] List of allowed instance variables
|
124
|
+
def handle_unknown_ivars(_unknown_refs, _klass, _allowed_ivars)
|
125
|
+
# No-op - do nothing
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
118
129
|
# Map of policy symbols to policy classes
|
119
130
|
POLICY_CLASSES = {
|
120
131
|
warn: WarnPolicy,
|
121
132
|
warn_once: WarnOncePolicy,
|
122
133
|
raise: RaisePolicy,
|
123
|
-
log: LogPolicy
|
134
|
+
log: LogPolicy,
|
135
|
+
none: NonePolicy
|
124
136
|
}.freeze
|
125
137
|
|
126
138
|
# Get a policy instance from a symbol or policy object
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
|
5
|
+
module Ivar
|
6
|
+
# Handles project root detection and caching
|
7
|
+
class ProjectRoot
|
8
|
+
# Project root indicator files, in order of precedence
|
9
|
+
INDICATORS = %w[Gemfile .git .ruby-version Rakefile].freeze
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@cache = {}
|
13
|
+
@mutex = Mutex.new
|
14
|
+
end
|
15
|
+
|
16
|
+
# Determines the project root directory based on the caller's location
|
17
|
+
# @param caller_location [String, nil] Optional file path to start from (defaults to caller's location)
|
18
|
+
# @return [String] The absolute path to the project root directory
|
19
|
+
def find(caller_location = nil)
|
20
|
+
file_path = caller_location || caller_locations(2, 1).first&.path
|
21
|
+
return Dir.pwd unless file_path
|
22
|
+
|
23
|
+
@mutex.synchronize do
|
24
|
+
return @cache[file_path] if @cache.key?(file_path)
|
25
|
+
end
|
26
|
+
|
27
|
+
dir = File.dirname(File.expand_path(file_path))
|
28
|
+
root = find_project_root(dir)
|
29
|
+
|
30
|
+
@mutex.synchronize do
|
31
|
+
@cache[file_path] = root
|
32
|
+
end
|
33
|
+
|
34
|
+
root
|
35
|
+
end
|
36
|
+
|
37
|
+
# Clear the cache (mainly for testing)
|
38
|
+
def clear_cache
|
39
|
+
@mutex.synchronize { @cache.clear }
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# Find the project root by walking up the directory tree
|
45
|
+
# @param start_dir [String] Directory to start the search from
|
46
|
+
# @return [String] The project root directory
|
47
|
+
def find_project_root(start_dir)
|
48
|
+
path = Pathname.new(start_dir)
|
49
|
+
|
50
|
+
path.ascend do |dir|
|
51
|
+
INDICATORS.each do |indicator|
|
52
|
+
return dir.to_s if dir.join(indicator).exist?
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
start_dir
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|