call_map 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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +131 -0
- data/exe/call_map +7 -0
- data/lib/call_map/analyzer.rb +143 -0
- data/lib/call_map/call_extractor.rb +124 -0
- data/lib/call_map/call_node.rb +52 -0
- data/lib/call_map/callback_extractor.rb +180 -0
- data/lib/call_map/cli.rb +68 -0
- data/lib/call_map/definition.rb +70 -0
- data/lib/call_map/definition_collector.rb +348 -0
- data/lib/call_map/formatters/text_tree.rb +69 -0
- data/lib/call_map/method_call.rb +74 -0
- data/lib/call_map/resolver.rb +107 -0
- data/lib/call_map/source_index.rb +91 -0
- data/lib/call_map/version.rb +5 -0
- data/lib/call_map.rb +18 -0
- data/sig/call_map.rbs +4 -0
- metadata +81 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CallMap
|
|
4
|
+
# Resolves a MethodCall to a Definition using the SourceIndex.
|
|
5
|
+
#
|
|
6
|
+
# This handles the common Rails patterns:
|
|
7
|
+
# - bare call within the same class → instance method on `context_owner`
|
|
8
|
+
# - `SomeService.execute` → class method on SomeService
|
|
9
|
+
# - `SomeClass.new(...).execute` → instance method on SomeClass
|
|
10
|
+
# - `self.foo` → class method on `context_owner`
|
|
11
|
+
class Resolver
|
|
12
|
+
# @param index [SourceIndex]
|
|
13
|
+
def initialize(index)
|
|
14
|
+
@index = index
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @param call [MethodCall] the call to resolve
|
|
18
|
+
# @param context_owner [String] the class/module the calling method belongs to
|
|
19
|
+
# @return [Definition, nil]
|
|
20
|
+
def resolve(call, context_owner:, context_kind: :instance_method, lexical_nesting: nil)
|
|
21
|
+
return nil if call.dynamic?
|
|
22
|
+
|
|
23
|
+
if call.bare? || call.receiver == "self"
|
|
24
|
+
resolve_bare(call, context_owner, context_kind)
|
|
25
|
+
else
|
|
26
|
+
resolve_receiver(call, context_owner, context_kind, lexical_nesting)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# Bare (and `self.`) calls dispatch through normal method lookup, so walk
|
|
33
|
+
# the owner's superclass chain — inherited helpers like a parent
|
|
34
|
+
# controller's `authenticate_user!` resolve too.
|
|
35
|
+
def resolve_bare(call, context_owner, context_kind)
|
|
36
|
+
finder = context_kind == :class_method ? :find_class_method : :find_instance_method
|
|
37
|
+
find_in_chain(finder, context_owner, call.method_name)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Resolve a call with an explicit receiver.
|
|
41
|
+
def resolve_receiver(call, context_owner, context_kind, lexical_nesting)
|
|
42
|
+
receiver = call.receiver
|
|
43
|
+
|
|
44
|
+
# `SomeClass.new(...)` chain → instance method on SomeClass
|
|
45
|
+
if receiver.match?(/\A([A-Z][A-Za-z0-9:]*?)\.new\z/)
|
|
46
|
+
owner = receiver.sub(/\.new\z/, "")
|
|
47
|
+
return resolve_constant(:instance_method, owner, call, lexical_nesting, context_owner)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# bare `new` or `self.new` chain (implicit self.new inside a class method only)
|
|
51
|
+
if %w[new self.new].include?(receiver) && context_kind == :class_method
|
|
52
|
+
return find_in_chain(:find_instance_method, context_owner, call.method_name)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# `SomeClass.method` → class method
|
|
56
|
+
return unless receiver.match?(/\A[A-Z]/)
|
|
57
|
+
|
|
58
|
+
resolve_constant(:class_method, receiver, call, lexical_nesting, context_owner)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def resolve_constant(kind, owner, call, lexical_nesting, context_owner)
|
|
62
|
+
finder = kind == :class_method ? :find_class_method : :find_instance_method
|
|
63
|
+
if call.absolute?
|
|
64
|
+
find_in_chain(finder, owner, call.method_name)
|
|
65
|
+
else
|
|
66
|
+
find_with_namespace_fallback(finder, owner, call.method_name, lexical_nesting, context_owner)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Try the constant against each lexical scope from innermost outward
|
|
71
|
+
# (mirroring Ruby's constant lookup), then against the context class's
|
|
72
|
+
# superclass chain (constants nested in a parent class are visible from
|
|
73
|
+
# the child), then fall back to top-level. Each scope entry is a full
|
|
74
|
+
# qualified name used as a prefix directly.
|
|
75
|
+
def find_with_namespace_fallback(finder, owner, method_name, lexical_nesting, context_owner)
|
|
76
|
+
scopes = (lexical_nesting || []).reverse + ancestor_scopes(lexical_nesting, context_owner)
|
|
77
|
+
scopes.each do |scope|
|
|
78
|
+
result = find_in_chain(finder, "#{scope}::#{owner}", method_name)
|
|
79
|
+
return result if result
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
find_in_chain(finder, owner, method_name)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Ruby searches the ancestors of the innermost lexically enclosing scope
|
|
86
|
+
# (the cref), not the method's owner. Only when the method is defined
|
|
87
|
+
# inside its own class body do the two coincide — for explicit-receiver
|
|
88
|
+
# definitions like `module Reports; def Generator.build` the owner's
|
|
89
|
+
# ancestors are NOT part of constant lookup.
|
|
90
|
+
def ancestor_scopes(lexical_nesting, context_owner)
|
|
91
|
+
return [] unless (lexical_nesting || []).last == context_owner
|
|
92
|
+
|
|
93
|
+
@index.ancestor_chain(context_owner)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Method dispatch walks the receiver class's superclass chain, so a
|
|
97
|
+
# method defined on a parent (class or instance side) resolves too.
|
|
98
|
+
def find_in_chain(finder, owner, method_name)
|
|
99
|
+
@index.ancestor_chain(owner).each do |candidate|
|
|
100
|
+
result = @index.public_send(finder, candidate, method_name)
|
|
101
|
+
return result if result
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "definition_collector"
|
|
4
|
+
|
|
5
|
+
module CallMap
|
|
6
|
+
# Indexes class / module / method definitions found under a directory.
|
|
7
|
+
#
|
|
8
|
+
# AST handling is delegated to DefinitionCollector; this class only stores
|
|
9
|
+
# the resulting Definitions and answers lookups against them.
|
|
10
|
+
class SourceIndex
|
|
11
|
+
DEFAULT_GLOB = "app/**/*.rb"
|
|
12
|
+
|
|
13
|
+
# Build a SourceIndex by indexing every Ruby file under `root`.
|
|
14
|
+
def self.build(root:, glob: DEFAULT_GLOB)
|
|
15
|
+
new.index_directory(root, glob: glob)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
@definitions = []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attr_reader :definitions
|
|
23
|
+
|
|
24
|
+
# @param root [String] directory to search from
|
|
25
|
+
# @param glob [String] glob pattern relative to root
|
|
26
|
+
def index_directory(root, glob: DEFAULT_GLOB)
|
|
27
|
+
Dir.glob(File.join(root, glob)).each { |path| index_file(path) }
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def index_file(path)
|
|
32
|
+
@definitions.concat(DefinitionCollector.collect(File.read(path), path: path))
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def find_instance_method(owner, name)
|
|
37
|
+
find_method(:instance_method, owner, name)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def find_class_method(owner, name)
|
|
41
|
+
find_method(:class_method, owner, name)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# All :class definitions matching the qualified name (a class may be
|
|
45
|
+
# reopened across files), in indexing order.
|
|
46
|
+
def find_class_definitions(qualified_name)
|
|
47
|
+
definitions.select { |d| d.kind == :class && d.name == qualified_name }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The class plus its superclasses (innermost first), resolved against the
|
|
51
|
+
# index. Stops at classes not present in the index or on a cycle.
|
|
52
|
+
def ancestor_chain(owner)
|
|
53
|
+
chain = []
|
|
54
|
+
current = owner
|
|
55
|
+
while current && !chain.include?(current)
|
|
56
|
+
chain << current
|
|
57
|
+
current = superclass_of(current)
|
|
58
|
+
end
|
|
59
|
+
chain
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def superclass_of(owner)
|
|
63
|
+
definition = find_class_definitions(owner).find(&:superclass)
|
|
64
|
+
return nil unless definition
|
|
65
|
+
|
|
66
|
+
superclass = definition.superclass
|
|
67
|
+
# A "::"-prefixed superclass is an absolute path — no namespace fallback.
|
|
68
|
+
return superclass.delete_prefix("::") if superclass.start_with?("::")
|
|
69
|
+
|
|
70
|
+
resolve_class_name(superclass, definition.lexical_nesting)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def find_method(kind, owner, name)
|
|
76
|
+
definitions.reverse_each.find { |d| d.kind == kind && d.owner == owner && d.name == name.to_s }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Resolve a superclass constant written relative to the subclass, mirroring
|
|
80
|
+
# Ruby's lexical lookup: each enclosing scope (a full qualified name) from
|
|
81
|
+
# innermost outward, then top-level.
|
|
82
|
+
def resolve_class_name(name, nesting)
|
|
83
|
+
(nesting || []).reverse_each do |scope|
|
|
84
|
+
candidate = "#{scope}::#{name}"
|
|
85
|
+
return candidate if find_class_definitions(candidate).any?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
name
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
data/lib/call_map.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "call_map/version"
|
|
4
|
+
require_relative "call_map/definition"
|
|
5
|
+
require_relative "call_map/definition_collector"
|
|
6
|
+
require_relative "call_map/source_index"
|
|
7
|
+
require_relative "call_map/method_call"
|
|
8
|
+
require_relative "call_map/call_extractor"
|
|
9
|
+
require_relative "call_map/callback_extractor"
|
|
10
|
+
require_relative "call_map/call_node"
|
|
11
|
+
require_relative "call_map/resolver"
|
|
12
|
+
require_relative "call_map/analyzer"
|
|
13
|
+
require_relative "call_map/formatters/text_tree"
|
|
14
|
+
|
|
15
|
+
module CallMap
|
|
16
|
+
class Error < StandardError; end
|
|
17
|
+
# Your code goes here...
|
|
18
|
+
end
|
data/sig/call_map.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: call_map
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- bumpfuji10
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-03 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: prism
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.0'
|
|
27
|
+
description: CallMap statically analyzes a Rails application and prints the method
|
|
28
|
+
call chain from a controller action as a stable text tree, including before_action
|
|
29
|
+
callbacks. It is a code-reading aid focused on application code, not a full static
|
|
30
|
+
analyzer.
|
|
31
|
+
email:
|
|
32
|
+
- bumpfuji10@gmail.com
|
|
33
|
+
executables:
|
|
34
|
+
- call_map
|
|
35
|
+
extensions: []
|
|
36
|
+
extra_rdoc_files: []
|
|
37
|
+
files:
|
|
38
|
+
- LICENSE.txt
|
|
39
|
+
- README.md
|
|
40
|
+
- exe/call_map
|
|
41
|
+
- lib/call_map.rb
|
|
42
|
+
- lib/call_map/analyzer.rb
|
|
43
|
+
- lib/call_map/call_extractor.rb
|
|
44
|
+
- lib/call_map/call_node.rb
|
|
45
|
+
- lib/call_map/callback_extractor.rb
|
|
46
|
+
- lib/call_map/cli.rb
|
|
47
|
+
- lib/call_map/definition.rb
|
|
48
|
+
- lib/call_map/definition_collector.rb
|
|
49
|
+
- lib/call_map/formatters/text_tree.rb
|
|
50
|
+
- lib/call_map/method_call.rb
|
|
51
|
+
- lib/call_map/resolver.rb
|
|
52
|
+
- lib/call_map/source_index.rb
|
|
53
|
+
- lib/call_map/version.rb
|
|
54
|
+
- sig/call_map.rbs
|
|
55
|
+
homepage: https://github.com/bumpfuji10/call_map
|
|
56
|
+
licenses:
|
|
57
|
+
- MIT
|
|
58
|
+
metadata:
|
|
59
|
+
homepage_uri: https://github.com/bumpfuji10/call_map
|
|
60
|
+
source_code_uri: https://github.com/bumpfuji10/call_map
|
|
61
|
+
rubygems_mfa_required: 'true'
|
|
62
|
+
post_install_message:
|
|
63
|
+
rdoc_options: []
|
|
64
|
+
require_paths:
|
|
65
|
+
- lib
|
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: 3.2.0
|
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
requirements: []
|
|
77
|
+
rubygems_version: 3.4.19
|
|
78
|
+
signing_key:
|
|
79
|
+
specification_version: 4
|
|
80
|
+
summary: Generate readable call maps from Rails controller actions.
|
|
81
|
+
test_files: []
|