rails-ai-context 5.30.2 → 5.31.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/CHANGELOG.md +173 -0
- data/docs/COMPATIBILITY.md +21 -7
- data/docs/CONFIGURATION.md +3 -0
- data/docs/GUIDE.md +7 -1
- data/docs/INTROSPECTORS.md +6 -1
- data/docs/SECURITY.md +23 -1
- data/docs/TOOLS.md +11 -2
- data/lib/rails_ai_context/action_filters.rb +2 -3
- data/lib/rails_ai_context/base_mixins.rb +44 -16
- data/lib/rails_ai_context/concern_macros.rb +698 -92
- data/lib/rails_ai_context/concern_membership.rb +6 -4
- data/lib/rails_ai_context/concern_paths.rb +29 -3
- data/lib/rails_ai_context/configuration.rb +2 -2
- data/lib/rails_ai_context/git_ignore.rb +13 -11
- data/lib/rails_ai_context/introspectors/action_presence.rb +5 -3
- data/lib/rails_ai_context/introspectors/action_resolver.rb +2 -1
- data/lib/rails_ai_context/introspectors/call_site_expansion.rb +198 -18
- data/lib/rails_ai_context/introspectors/controller_introspector.rb +3 -3
- data/lib/rails_ai_context/introspectors/declared_constant.rb +15 -6
- data/lib/rails_ai_context/introspectors/job_introspector.rb +55 -10
- data/lib/rails_ai_context/introspectors/listeners/base_listener.rb +3 -3
- data/lib/rails_ai_context/introspectors/listeners/callbacks_listener.rb +28 -1
- data/lib/rails_ai_context/introspectors/listeners/generic_macro_listener.rb +3 -1
- data/lib/rails_ai_context/introspectors/listeners/mixins_listener.rb +21 -28
- data/lib/rails_ai_context/introspectors/listeners/owner_scope.rb +67 -0
- data/lib/rails_ai_context/introspectors/listeners/proc_literal_listener.rb +42 -0
- data/lib/rails_ai_context/introspectors/model_introspector.rb +277 -118
- data/lib/rails_ai_context/introspectors/source_scan.rb +5 -3
- data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +13 -4
- data/lib/rails_ai_context/introspectors/structure_sql_reader.rb +104 -21
- data/lib/rails_ai_context/method_name.rb +31 -0
- data/lib/rails_ai_context/output_guard.rb +6 -4
- data/lib/rails_ai_context/redaction.rb +8 -2
- data/lib/rails_ai_context/safe_file.rb +4 -1
- data/lib/rails_ai_context/safe_path.rb +17 -4
- data/lib/rails_ai_context/server.rb +26 -7
- data/lib/rails_ai_context/tools/base_tool.rb +11 -2
- data/lib/rails_ai_context/tools/get_callbacks.rb +14 -6
- data/lib/rails_ai_context/tools/get_concern.rb +1 -1
- data/lib/rails_ai_context/tools/get_conventions.rb +5 -7
- data/lib/rails_ai_context/tools/get_job_pattern.rb +8 -2
- data/lib/rails_ai_context/tools/get_model_details.rb +4 -3
- data/lib/rails_ai_context/tools/get_schema.rb +4 -0
- data/lib/rails_ai_context/tools/performance_check.rb +5 -13
- data/lib/rails_ai_context/tools/query.rb +122 -28
- data/lib/rails_ai_context/tools/search_code.rb +64 -22
- data/lib/rails_ai_context/tools/search_docs.rb +4 -5
- data/lib/rails_ai_context/tools/security_scan.rb +3 -1
- data/lib/rails_ai_context/version.rb +1 -1
- data/lib/rails_ai_context.rb +1 -1
- data/server.json +1 -1
- metadata +4 -1
|
@@ -7,6 +7,7 @@ module RailsAiContext
|
|
|
7
7
|
# before_validation, after_save, after_commit, etc.
|
|
8
8
|
class CallbacksListener < BaseListener
|
|
9
9
|
include WithOptionsScope
|
|
10
|
+
include OwnerScope
|
|
10
11
|
|
|
11
12
|
CALLBACK_METHODS = %i[
|
|
12
13
|
before_validation after_validation
|
|
@@ -39,6 +40,31 @@ module RailsAiContext
|
|
|
39
40
|
end
|
|
40
41
|
end
|
|
41
42
|
|
|
43
|
+
ON_EVENT = "after_commit_on_"
|
|
44
|
+
|
|
45
|
+
# Every commit spelling lands in the one after-commit kind of one chain.
|
|
46
|
+
def self.chain_key(type)
|
|
47
|
+
type.to_s.match?(/\Aafter_(\w+_)?commit/) ? "after_commit" : type.to_s
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.transaction?(type)
|
|
51
|
+
type.to_s.match?(/\Aafter_(\w+_)?(commit|rollback)/)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.after?(type)
|
|
55
|
+
type.to_s.start_with?("after_")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# `after_commit on: :create` resolves to a type that names its event.
|
|
59
|
+
def self.names_event?(type)
|
|
60
|
+
type.to_s.start_with?(ON_EVENT)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The event such a type names, `"create"`, or nil.
|
|
64
|
+
def self.event(type)
|
|
65
|
+
type.to_s.delete_prefix(ON_EVENT) if names_event?(type)
|
|
66
|
+
end
|
|
67
|
+
|
|
42
68
|
private
|
|
43
69
|
|
|
44
70
|
# `around_create Snowflake::Callbacks` names a real target;
|
|
@@ -66,6 +92,7 @@ module RailsAiContext
|
|
|
66
92
|
type: callback_type,
|
|
67
93
|
method: method_name,
|
|
68
94
|
options: options,
|
|
95
|
+
owner: @owner_stack.dup,
|
|
69
96
|
location: node.location.start_line,
|
|
70
97
|
confidence: confidence
|
|
71
98
|
}
|
|
@@ -77,7 +104,7 @@ module RailsAiContext
|
|
|
77
104
|
# declaration for several events stays one callback, its on: kept.
|
|
78
105
|
def resolve_callback_types(name, options)
|
|
79
106
|
events = Array(options[:on])
|
|
80
|
-
name == :after_commit && events.one? ? [ "
|
|
107
|
+
name == :after_commit && events.one? ? [ "#{ON_EVENT}#{events.first}" ] : [ name.to_s ]
|
|
81
108
|
end
|
|
82
109
|
end
|
|
83
110
|
end
|
|
@@ -4,10 +4,11 @@ module RailsAiContext
|
|
|
4
4
|
module Introspectors
|
|
5
5
|
module Listeners
|
|
6
6
|
class GenericMacroListener < BaseListener
|
|
7
|
-
def initialize(*target_methods)
|
|
7
|
+
def initialize(*target_methods, block_source: [])
|
|
8
8
|
super()
|
|
9
9
|
@target_methods = target_methods.flatten.map(&:to_sym).to_set
|
|
10
10
|
@enclosing = []
|
|
11
|
+
@block_source = block_source.to_set
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def on_call_node_enter(node)
|
|
@@ -21,6 +22,7 @@ module RailsAiContext
|
|
|
21
22
|
options: extract_keyword_options(node),
|
|
22
23
|
option_values: extract_keyword_sources(node),
|
|
23
24
|
option_nodes: extract_keyword_nodes(node),
|
|
25
|
+
block: (one_line_source(node.block) if node.block.is_a?(Prism::BlockNode) && @block_source.include?(node.name)),
|
|
24
26
|
# Offsets, not line numbers: a one-line block puts the parent and
|
|
25
27
|
# its nested calls on one line, and a consumer pairing them by
|
|
26
28
|
# line then attaches the second child to the first.
|
|
@@ -14,28 +14,11 @@ module RailsAiContext
|
|
|
14
14
|
MIXIN_MACROS = %i[include prepend extend].to_set.freeze
|
|
15
15
|
ANCESTOR_MACROS = %i[include prepend].to_set.freeze
|
|
16
16
|
|
|
17
|
+
include OwnerScope
|
|
18
|
+
|
|
17
19
|
def initialize
|
|
18
20
|
super
|
|
19
21
|
@singleton_depth = 0
|
|
20
|
-
@owner_stack = []
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# The class or module the mixin is written in, the way MethodsListener
|
|
24
|
-
# records a method's: a nested class's include is not its module's.
|
|
25
|
-
def on_class_node_enter(node)
|
|
26
|
-
@owner_stack.push(constant_path_string(node.constant_path))
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def on_class_node_leave(_node)
|
|
30
|
-
@owner_stack.pop
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def on_module_node_enter(node)
|
|
34
|
-
@owner_stack.push(constant_path_string(node.constant_path))
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def on_module_node_leave(_node)
|
|
38
|
-
@owner_stack.pop
|
|
39
22
|
end
|
|
40
23
|
|
|
41
24
|
# `include` inside `class << self` lands on the singleton class, so it
|
|
@@ -48,27 +31,37 @@ module RailsAiContext
|
|
|
48
31
|
@singleton_depth -= 1
|
|
49
32
|
end
|
|
50
33
|
|
|
34
|
+
# `singleton_class` called on what stands for the class: nothing or `self` in its body, `base` in a hook.
|
|
35
|
+
# `singleton_class.include M` gives the class M's methods, as `extend` does, without `extended`.
|
|
36
|
+
def self.singleton_class_of?(node, &stands_for_class)
|
|
37
|
+
node.is_a?(Prism::CallNode) && node.name == :singleton_class && node.arguments.nil? && stands_for_class.call(node.receiver)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The record of `node` mixing in `name`.
|
|
41
|
+
def self.record(node, macro, name, ancestor:, owner: [])
|
|
42
|
+
{ macro: macro, name: name, ancestor: ancestor, owner: owner, location: node.location.start_line,
|
|
43
|
+
confidence: confidence_for(node) }
|
|
44
|
+
end
|
|
45
|
+
|
|
51
46
|
# `Type.include(StatusPatch)` mixes into Type, not the class the line sits in, so the
|
|
52
47
|
# record names Type as `receiver` and it is never an ancestor of the enclosing class.
|
|
53
48
|
def on_call_node_enter(node)
|
|
54
49
|
receiver = node.receiver && constant_name(node.receiver)
|
|
55
|
-
|
|
50
|
+
singleton = self.class.singleton_class_of?(node.receiver) { |inner| inner.nil? || inner.is_a?(Prism::SelfNode) }
|
|
51
|
+
return unless node.receiver.nil? || receiver || singleton
|
|
56
52
|
|
|
57
53
|
macro, arguments = mixin_call(node)
|
|
58
54
|
return unless macro
|
|
55
|
+
return if singleton && macro == :extend
|
|
56
|
+
|
|
57
|
+
macro = :"singleton_#{macro}" if singleton
|
|
59
58
|
|
|
60
59
|
arguments.each do |arg|
|
|
61
60
|
name = constant_name(arg)
|
|
62
61
|
next unless name
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
name: name,
|
|
67
|
-
ancestor: receiver.nil? && @singleton_depth.zero? && ANCESTOR_MACROS.include?(macro),
|
|
68
|
-
owner: @owner_stack.dup,
|
|
69
|
-
location: node.location.start_line,
|
|
70
|
-
confidence: confidence_for(node)
|
|
71
|
-
}
|
|
63
|
+
ancestor = receiver.nil? && @singleton_depth.zero? && ANCESTOR_MACROS.include?(macro)
|
|
64
|
+
record = self.class.record(node, macro, name, ancestor: ancestor, owner: @owner_stack.dup)
|
|
72
65
|
record[:receiver] = receiver if receiver
|
|
73
66
|
@results << record
|
|
74
67
|
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiContext
|
|
4
|
+
module Introspectors
|
|
5
|
+
module Listeners
|
|
6
|
+
# The class or module a record is written in, so a nested class's
|
|
7
|
+
# declarations stay its own. `Sib = Class.new(Base) do` opens Sib.
|
|
8
|
+
module OwnerScope
|
|
9
|
+
def initialize
|
|
10
|
+
super
|
|
11
|
+
@owner_stack = []
|
|
12
|
+
@class_new_writes = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def on_class_node_enter(node)
|
|
16
|
+
@owner_stack.push(constant_path_string(node.constant_path))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def on_class_node_leave(_node)
|
|
20
|
+
@owner_stack.pop
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def on_module_node_enter(node)
|
|
24
|
+
@owner_stack.push(constant_path_string(node.constant_path))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def on_module_node_leave(_node)
|
|
28
|
+
@owner_stack.pop
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def on_constant_write_node_enter(node)
|
|
32
|
+
enter_class_new(node, node.name.to_s)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def on_constant_write_node_leave(node)
|
|
36
|
+
leave_class_new(node)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def on_constant_path_write_node_enter(node)
|
|
40
|
+
enter_class_new(node, node.target.slice)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def on_constant_path_write_node_leave(node)
|
|
44
|
+
leave_class_new(node)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def enter_class_new(node, name)
|
|
50
|
+
value = node.value
|
|
51
|
+
return unless value.is_a?(Prism::CallNode) && value.name == :new && value.block &&
|
|
52
|
+
value.receiver.is_a?(Prism::ConstantReadNode) && value.receiver.name == :Class
|
|
53
|
+
|
|
54
|
+
@owner_stack.push(name)
|
|
55
|
+
@class_new_writes.push(node)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def leave_class_new(node)
|
|
59
|
+
return unless @class_new_writes.last.equal?(node)
|
|
60
|
+
|
|
61
|
+
@class_new_writes.pop
|
|
62
|
+
@owner_stack.pop
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiContext
|
|
4
|
+
module Introspectors
|
|
5
|
+
module Listeners
|
|
6
|
+
# Every Proc literal (`-> {}`, `lambda {}`, `proc {}`, `Proc.new {}`) with
|
|
7
|
+
# the line it starts on and the constant it is assigned to, if any.
|
|
8
|
+
class ProcLiteralListener < BaseListener
|
|
9
|
+
def initialize
|
|
10
|
+
super
|
|
11
|
+
@assigned = {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def on_constant_write_node_enter(node)
|
|
15
|
+
@assigned[node.value.location.start_offset] = node.name.to_s
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def on_lambda_node_enter(node)
|
|
19
|
+
record(node)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def on_call_node_enter(node)
|
|
23
|
+
record(node) if proc_call?(node)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def proc_call?(node)
|
|
29
|
+
return false unless node.block.is_a?(Prism::BlockNode)
|
|
30
|
+
return %i[lambda proc].include?(node.name) if node.receiver.nil?
|
|
31
|
+
|
|
32
|
+
node.name == :new && node.receiver.is_a?(Prism::ConstantReadNode) && node.receiver.name == :Proc
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def record(node)
|
|
36
|
+
@results << { line: node.location.start_line, constant: @assigned[node.location.start_offset],
|
|
37
|
+
source: one_line_source(node) }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|