woods 2.0.1 → 2.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 +4 -4
- data/CHANGELOG.md +94 -7
- data/CONTRIBUTING.md +134 -19
- data/README.md +1 -1
- data/docs/AGENT_GUIDE.md +19 -0
- data/docs/AGENT_SETUP.md +22 -2
- data/docs/BACKEND_MATRIX.md +7 -0
- data/docs/CLIENT_HOOKS.md +6 -0
- data/docs/CONFIGURATION_REFERENCE.md +133 -25
- data/docs/CONSOLE_MCP_SETUP.md +82 -30
- data/docs/EMBEDDING_MODELS.md +16 -19
- data/docs/EXTRACTOR_REFERENCE.md +219 -21
- data/docs/FAQ.md +11 -25
- data/docs/GETTING_STARTED.md +7 -1
- data/docs/INCREMENTAL_EXTRACTION.md +261 -19
- data/docs/INDEX_LAYOUT.md +5 -0
- data/docs/INTERNALS.md +9 -0
- data/docs/MCP_HTTP_TRANSPORT.md +20 -15
- data/docs/MCP_SERVERS.md +87 -8
- data/docs/MCP_TOOL_COOKBOOK.md +13 -55
- data/docs/NOTION_INTEGRATION.md +7 -1
- data/docs/PUBLISHED_INDEX.md +6 -0
- data/docs/README.md +6 -1
- data/docs/RETRIEVAL_GUIDE.md +17 -0
- data/docs/SOURCE_FRESHNESS.md +157 -5
- data/docs/TOKEN_BENCHMARK.md +10 -18
- data/docs/TROUBLESHOOTING.md +70 -14
- data/docs/UNBLOCKED_INTEGRATION.md +60 -8
- data/docs/UPGRADING_TO_2.md +153 -38
- data/docs/WATCH_DAEMON.md +97 -14
- data/exe/woods-console-mcp +2 -2
- data/lib/generators/woods/templates/woods.rb.tt +2 -1
- data/lib/tasks/woods.rake +23 -7
- data/lib/tasks/woods_checks.rake +2 -2
- data/lib/woods/agent_configuration/cli.rb +1 -1
- data/lib/woods/agent_configuration/layout.rb +16 -2
- data/lib/woods/agent_configuration/plan.rb +13 -3
- data/lib/woods/agent_configuration/planner_validation.rb +4 -2
- data/lib/woods/agent_configuration/preflight.rb +5 -3
- data/lib/woods/builder.rb +17 -57
- data/lib/woods/cache/cache_middleware.rb +56 -30
- data/lib/woods/chunking/contributor_chunks.rb +119 -0
- data/lib/woods/chunking/semantic_chunker.rb +44 -21
- data/lib/woods/console/connection_manager.rb +56 -3
- data/lib/woods/console/embedded_executor.rb +30 -5
- data/lib/woods/console/rack_middleware.rb +29 -1
- data/lib/woods/dependency_graph.rb +34 -10
- data/lib/woods/embedding/fake.rb +12 -0
- data/lib/woods/embedding/indexer.rb +195 -98
- data/lib/woods/embedding/input_budget.rb +67 -0
- data/lib/woods/embedding/openai.rb +70 -20
- data/lib/woods/embedding/provider.rb +37 -25
- data/lib/woods/embedding/text_preparer.rb +76 -32
- data/lib/woods/embedding/token_counter.rb +18 -81
- data/lib/woods/embedding/vector_configuration.rb +48 -0
- data/lib/woods/extraction_identities.rb +175 -0
- data/lib/woods/extractor.rb +304 -107
- data/lib/woods/extractors/action_cable_extractor.rb +8 -3
- data/lib/woods/extractors/assigned_value_discovery.rb +74 -0
- data/lib/woods/extractors/class_declarations.rb +121 -0
- data/lib/woods/extractors/configuration_extractor.rb +11 -3
- data/lib/woods/extractors/declaration_ancestry.rb +92 -0
- data/lib/woods/extractors/event_extractor.rb +8 -0
- data/lib/woods/extractors/graphql_extractor.rb +134 -77
- data/lib/woods/extractors/job_extractor.rb +5 -1
- data/lib/woods/extractors/lib_extractor.rb +132 -15
- data/lib/woods/extractors/mailer_extractor.rb +3 -5
- data/lib/woods/extractors/manager_extractor.rb +7 -21
- data/lib/woods/extractors/migration_declaration.rb +87 -0
- data/lib/woods/extractors/migration_extractor.rb +5 -39
- data/lib/woods/extractors/phlex_extractor.rb +6 -2
- data/lib/woods/extractors/policy_extractor.rb +9 -5
- data/lib/woods/extractors/poro_extractor.rb +112 -53
- data/lib/woods/extractors/pundit_extractor.rb +11 -6
- data/lib/woods/extractors/scheduled_job_extractor.rb +45 -4
- data/lib/woods/extractors/serializer_extractor.rb +34 -22
- data/lib/woods/extractors/shared_utility_methods.rb +18 -1
- data/lib/woods/extractors/source_nesting.rb +142 -106
- data/lib/woods/extractors/standalone_module_discovery.rb +123 -0
- data/lib/woods/extractors/state_machine_extractor.rb +46 -40
- data/lib/woods/extractors/view_component_extractor.rb +9 -7
- data/lib/woods/flow_assembler.rb +4 -1
- data/lib/woods/generation.rb +25 -0
- data/lib/woods/hooks/context_hint.rb +7 -2
- data/lib/woods/mcp/bootstrapper.rb +33 -7
- data/lib/woods/mcp/config_resolver.rb +26 -7
- data/lib/woods/mcp/index_reader.rb +125 -24
- data/lib/woods/mcp/index_reader_pinning.rb +16 -0
- data/lib/woods/mcp/renderers/markdown_renderer.rb +7 -1
- data/lib/woods/mcp/renderers/plain_renderer.rb +3 -1
- data/lib/woods/mcp/search_results.rb +7 -1
- data/lib/woods/mcp/server.rb +24 -4
- data/lib/woods/module_reconciliation.rb +151 -0
- data/lib/woods/path_dispatcher.rb +7 -2
- data/lib/woods/rake_helpers.rb +43 -11
- data/lib/woods/release.rb +1 -1
- data/lib/woods/resilience/index_validator.rb +8 -3
- data/lib/woods/resilience/retryable_provider.rb +18 -1
- data/lib/woods/resolved_config.rb +68 -8
- data/lib/woods/retrieval/context_assembler.rb +3 -3
- data/lib/woods/retrieval/lexical_assembler.rb +3 -2
- data/lib/woods/retrieval/scope.rb +18 -2
- data/lib/woods/retrieval/source_evidence.rb +14 -2
- data/lib/woods/source_contributor_validation.rb +78 -0
- data/lib/woods/source_contributors.rb +116 -0
- data/lib/woods/source_inputs/handoff.rb +37 -0
- data/lib/woods/source_inputs/launcher.rb +53 -13
- data/lib/woods/source_inputs/manifest.rb +84 -3
- data/lib/woods/source_inputs/private_key.rb +44 -12
- data/lib/woods/source_inputs/scanner.rb +98 -27
- data/lib/woods/source_inputs/scopes.rb +1 -1
- data/lib/woods/source_inputs/session.rb +147 -15
- data/lib/woods/source_inputs/stable_reader.rb +127 -0
- data/lib/woods/source_inputs/status.rb +40 -8
- data/lib/woods/source_inputs/verifier.rb +28 -5
- data/lib/woods/source_path_encoding.rb +33 -0
- data/lib/woods/source_references/cache.rb +284 -0
- data/lib/woods/source_references/collector.rb +120 -0
- data/lib/woods/source_references/extraction.rb +185 -0
- data/lib/woods/source_references/inputs.rb +134 -0
- data/lib/woods/source_references/parser_adapter.rb +134 -0
- data/lib/woods/source_references/pass.rb +152 -0
- data/lib/woods/source_references/prism_adapter.rb +116 -0
- data/lib/woods/source_references/registry.rb +178 -0
- data/lib/woods/source_references/runtime_lookup.rb +127 -0
- data/lib/woods/source_references/value_class.rb +82 -0
- data/lib/woods/storage/metadata_store.rb +4 -1
- data/lib/woods/storage/qdrant.rb +2 -2
- data/lib/woods/unblocked/client.rb +12 -7
- data/lib/woods/unblocked/document_builder.rb +4 -1
- data/lib/woods/unblocked/exporter.rb +127 -37
- data/lib/woods/unblocked/sync_manifest.rb +137 -21
- data/lib/woods/unblocked/uri_migration.rb +105 -0
- data/lib/woods/util/host_guard.rb +3 -2
- data/lib/woods/version.rb +1 -1
- data/lib/woods/watch/catch_up.rb +138 -0
- data/lib/woods/watch/claim_lease.rb +150 -0
- data/lib/woods/watch/cli.rb +26 -2
- data/lib/woods/watch/daemon.rb +80 -59
- data/lib/woods/watch/installation/options.rb +1 -1
- data/lib/woods/watch/installation/receipt.rb +6 -1
- data/lib/woods/watch/managed_child.rb +1 -1
- data/lib/woods/watch/supervisor.rb +1 -1
- data/lib/woods/watch/tree_scan.rb +14 -2
- data/plugin/.claude-plugin/plugin.json +1 -1
- data/plugin/hooks/adapters/normalize.rb +3 -2
- data/plugin/hooks/woods-input-rules.sh +4 -0
- data/plugin/hooks/woods-refresh.sh +15 -7
- data/plugin/hooks/woods-session-start.sh +60 -3
- data/plugin/skills/woods-diagnose/SKILL.md +334 -11
- data/plugin/skills/woods-investigate/SKILL.md +11 -0
- data/plugin/skills/woods-mcp-config/SKILL.md +79 -8
- data/plugin/skills/woods-setup/SKILL.md +53 -6
- metadata +32 -5
|
@@ -56,6 +56,8 @@ module Woods
|
|
|
56
56
|
# @param klass [Class] A channel subclass
|
|
57
57
|
# @return [ExtractedUnit, nil]
|
|
58
58
|
def extract_channel(klass)
|
|
59
|
+
return nil unless app_channel?(klass)
|
|
60
|
+
|
|
59
61
|
name = klass.name
|
|
60
62
|
file_path = source_file_for(klass, name)
|
|
61
63
|
source = read_source(file_path)
|
|
@@ -91,9 +93,12 @@ module Woods
|
|
|
91
93
|
#
|
|
92
94
|
# @return [Array<Class>]
|
|
93
95
|
def channel_descendants
|
|
94
|
-
ActionCable::Channel::Base.descendants.
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
ActionCable::Channel::Base.descendants.select { |klass| app_channel?(klass) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def app_channel?(klass)
|
|
100
|
+
action_cable_available? && klass.name && klass.name != 'ApplicationCable::Channel' &&
|
|
101
|
+
klass < ActionCable::Channel::Base && app_source_file?(source_file_for(klass, klass.name))
|
|
97
102
|
end
|
|
98
103
|
|
|
99
104
|
# Locate the source file for a channel class.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../source_references/value_class'
|
|
4
|
+
|
|
5
|
+
module Woods
|
|
6
|
+
module Extractors
|
|
7
|
+
# Selects the source-owned value class instead of its reopening namespace.
|
|
8
|
+
class AssignedValueDiscovery
|
|
9
|
+
CORE_METHODS = Module.instance_method(:instance_methods)
|
|
10
|
+
CORE_PRIVATE_METHODS = Module.instance_method(:private_instance_methods)
|
|
11
|
+
|
|
12
|
+
# @param file_path [String] original Ruby file
|
|
13
|
+
# @param analysis [Hash] original-source Collector result
|
|
14
|
+
# @param expected [String, nil] active loader's expected constant identity
|
|
15
|
+
# @param preserve_modules [Boolean] retain a canonical library module primary
|
|
16
|
+
# @return [String, nil] verified assigned child, or nil to preserve ordinary discovery
|
|
17
|
+
def call(file_path, analysis:, expected: nil, preserve_modules: false)
|
|
18
|
+
return if analysis['parse_error']
|
|
19
|
+
|
|
20
|
+
verifier = SourceReferences::ValueClass.new
|
|
21
|
+
declarations = analysis.fetch('declarations')
|
|
22
|
+
candidates = declarations.filter_map { |record| verifier.call(record, file_path: file_path) }
|
|
23
|
+
return expected if candidates.include?(expected)
|
|
24
|
+
return if candidates.empty? || owns_primary?(declarations, file_path, expected, preserve_modules)
|
|
25
|
+
|
|
26
|
+
candidates.first
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def owns_primary?(declarations, file_path, expected, preserve_modules)
|
|
32
|
+
lookup = SourceReferences::RuntimeLookup.new
|
|
33
|
+
declarations.any? do |record|
|
|
34
|
+
next false if record['constructor'] || record.fetch('singleton_depth', 0).positive?
|
|
35
|
+
next false unless canonical_file?(record['owner'], file_path, lookup)
|
|
36
|
+
next true if record['kind'] == 'class'
|
|
37
|
+
next false unless preserve_modules
|
|
38
|
+
|
|
39
|
+
record['owner'] == expected || callable_module?(record['owner'], file_path, lookup)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def canonical_file?(identifier, file_path, lookup)
|
|
44
|
+
parts = identifier.split('::')
|
|
45
|
+
name = parts.pop
|
|
46
|
+
scope = parts.empty? ? Object : lookup.call("::#{parts.join('::')}", allow_private: true)[:value]
|
|
47
|
+
return false unless lookup.module_object?(scope)
|
|
48
|
+
|
|
49
|
+
location = SourceReferences::ValueClass::SOURCE_LOCATION.bind(scope).call(name, false)
|
|
50
|
+
location&.first && File.expand_path(location.first) == File.expand_path(file_path)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def callable_module?(identifier, path, lookup)
|
|
54
|
+
value = lookup.call("::#{identifier}", allow_private: true)[:value]
|
|
55
|
+
return false unless lookup.module_object?(value)
|
|
56
|
+
|
|
57
|
+
singleton = SourceReferences::ValueClass::SINGLETON_CLASS.bind(value).call
|
|
58
|
+
[value, singleton].any? { |scope| own_method_at?(scope, path) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def own_method_at?(scope, path)
|
|
62
|
+
names = CORE_METHODS.bind(scope).call(false) + CORE_PRIVATE_METHODS.bind(scope).call(false)
|
|
63
|
+
names.any? do |name|
|
|
64
|
+
method = SourceReferences::ValueClass::INSTANCE_METHOD.bind(scope).call(name)
|
|
65
|
+
owner = SourceReferences::ValueClass::METHOD_OWNER.bind(method).call
|
|
66
|
+
next false unless SourceReferences::RuntimeLookup::CORE_EQUAL.bind(owner).call(scope)
|
|
67
|
+
|
|
68
|
+
location = SourceReferences::ValueClass::METHOD_SOURCE.bind(method).call
|
|
69
|
+
location && File.expand_path(location.first) == File.expand_path(path)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'prism'
|
|
4
|
+
require_relative '../source_references/runtime_lookup'
|
|
5
|
+
|
|
6
|
+
module Woods
|
|
7
|
+
module Extractors
|
|
8
|
+
# Structural declaration evidence without loading or evaluating source.
|
|
9
|
+
module ClassDeclarations
|
|
10
|
+
class Unresolved < StandardError; end
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# @param source [String] original Ruby source
|
|
15
|
+
# @return [Array<Hash>] class identities, lexical scopes and syntax nodes
|
|
16
|
+
def read(source)
|
|
17
|
+
parsed = Prism.parse(source)
|
|
18
|
+
return [] unless parsed.success?
|
|
19
|
+
|
|
20
|
+
Collector.new.call(parsed.value)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param declaration [Hash] record returned by .read
|
|
24
|
+
# @return [String, nil] constant-path parent, including Migration[version]
|
|
25
|
+
def parent_name(declaration)
|
|
26
|
+
parent = declaration.fetch(:node).superclass
|
|
27
|
+
parent = parent.receiver if parent.is_a?(Prism::CallNode) && parent.name == :[]
|
|
28
|
+
constant_name(parent)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param node [Prism::Node, nil] constant syntax
|
|
32
|
+
# @return [String, nil] written constant path, preserving leading ::
|
|
33
|
+
def constant_name(node)
|
|
34
|
+
case node
|
|
35
|
+
when Prism::ConstantReadNode then node.name.to_s
|
|
36
|
+
when Prism::ConstantPathNode
|
|
37
|
+
parent = node.parent ? constant_name(node.parent) : ''
|
|
38
|
+
"#{parent}::#{node.name}" if parent
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Qualified declaration receivers follow constant lookup; they do not
|
|
43
|
+
# automatically belong to the innermost syntactic namespace.
|
|
44
|
+
class Collector
|
|
45
|
+
def initialize
|
|
46
|
+
@lookup = SourceReferences::RuntimeLookup.new
|
|
47
|
+
@namespaces = {}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def call(node, nesting = [], result = [])
|
|
51
|
+
return result unless node
|
|
52
|
+
return result if node.is_a?(Prism::SingletonClassNode) || node.is_a?(Prism::DefNode)
|
|
53
|
+
|
|
54
|
+
if node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)
|
|
55
|
+
name = ClassDeclarations.constant_name(node.constant_path)
|
|
56
|
+
return result unless name
|
|
57
|
+
|
|
58
|
+
identifier = declaration_identity(name, nesting)
|
|
59
|
+
@namespaces[identifier] = true
|
|
60
|
+
if node.is_a?(Prism::ClassNode)
|
|
61
|
+
result << { identifier: identifier, nesting: nesting, node: node, source: node.location.slice }
|
|
62
|
+
end
|
|
63
|
+
call(node.body, [identifier, *nesting], result)
|
|
64
|
+
else
|
|
65
|
+
node.compact_child_nodes.each { |child| call(child, nesting, result) }
|
|
66
|
+
end
|
|
67
|
+
result
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def declaration_identity(name, nesting)
|
|
73
|
+
parts = name.delete_prefix('::').split('::')
|
|
74
|
+
return [name.start_with?('::') ? nil : nesting.first, parts.first].compact.join('::') if parts.one?
|
|
75
|
+
|
|
76
|
+
leaf = parts.pop
|
|
77
|
+
receiver = if name.start_with?('::')
|
|
78
|
+
namespace(parts.shift)
|
|
79
|
+
else
|
|
80
|
+
lexical_namespace(parts.shift, nesting)
|
|
81
|
+
end
|
|
82
|
+
parts.each { |part| receiver = namespace("#{receiver}::#{part}") }
|
|
83
|
+
"#{receiver}::#{leaf}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def lexical_namespace(name, nesting)
|
|
87
|
+
nesting.each do |scope|
|
|
88
|
+
candidate = "#{scope}::#{name}"
|
|
89
|
+
return namespace(candidate) if @namespaces[candidate]
|
|
90
|
+
|
|
91
|
+
result = @lookup.call("::#{scope}", allow_private: true)
|
|
92
|
+
unresolved!(candidate) unless result[:status] == :resolved
|
|
93
|
+
next unless @lookup.reflect(result[:value], :const_defined?, name, false)
|
|
94
|
+
|
|
95
|
+
return namespace(candidate)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Only after all lexical constant tables can be checked may inherited
|
|
99
|
+
# or root constants supply a receiver. An unloaded scope is unknown.
|
|
100
|
+
result = @lookup.call(name, nesting: nesting, allow_private: true)
|
|
101
|
+
return result[:target] if result[:status] == :resolved
|
|
102
|
+
return namespace(name) if result[:status] == :missing && result[:root_lookup]
|
|
103
|
+
|
|
104
|
+
unresolved!(name)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def namespace(name)
|
|
108
|
+
result = @lookup.call("::#{name}", allow_private: true)
|
|
109
|
+
return result[:target] if result[:status] == :resolved
|
|
110
|
+
return name if @namespaces[name] && result[:status] == :missing
|
|
111
|
+
|
|
112
|
+
unresolved!(name)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def unresolved!(name)
|
|
116
|
+
raise Unresolved, "Unresolved qualified declaration namespace: #{name}"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -42,9 +42,7 @@ module Woods
|
|
|
42
42
|
extract_configuration_file(file)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
profile = profiler.extract
|
|
47
|
-
SourceInputs::ConsumerErrors.record(self) if SourceInputs::ConsumerErrors.failed?(profiler)
|
|
45
|
+
profile = extract_behavioral_profile
|
|
48
46
|
units << profile if profile
|
|
49
47
|
|
|
50
48
|
units
|
|
@@ -53,6 +51,16 @@ module Woods
|
|
|
53
51
|
units || []
|
|
54
52
|
end
|
|
55
53
|
|
|
54
|
+
# Re-derive the synthetic unit from resolved runtime values, never from
|
|
55
|
+
# its nominal config/application.rb source path.
|
|
56
|
+
# @return [ExtractedUnit, nil]
|
|
57
|
+
def extract_behavioral_profile
|
|
58
|
+
profiler = BehavioralProfile.new
|
|
59
|
+
profile = profiler.extract
|
|
60
|
+
SourceInputs::ConsumerErrors.record(self) if SourceInputs::ConsumerErrors.failed?(profiler)
|
|
61
|
+
profile
|
|
62
|
+
end
|
|
63
|
+
|
|
56
64
|
# Extract a single configuration file
|
|
57
65
|
#
|
|
58
66
|
# @param file_path [String] Path to the configuration file
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'class_declarations'
|
|
4
|
+
require_relative '../source_references/runtime_lookup'
|
|
5
|
+
|
|
6
|
+
module Woods
|
|
7
|
+
module Extractors
|
|
8
|
+
# Runtime qualification belongs to the selected class and its actual source.
|
|
9
|
+
# An unavailable class may use limited declaration evidence; a foreign one
|
|
10
|
+
# must never lend its ancestry to a same-named file.
|
|
11
|
+
class DeclarationAncestry
|
|
12
|
+
SOURCE_LOCATION = Module.instance_method(:const_source_location)
|
|
13
|
+
|
|
14
|
+
# @param source [String] original source, never evaluated
|
|
15
|
+
# @param identifier [String] selected extraction identity
|
|
16
|
+
# @param file_path [String] source file owning the unit
|
|
17
|
+
def initialize(source:, identifier:, file_path:)
|
|
18
|
+
@lookup = SourceReferences::RuntimeLookup.new
|
|
19
|
+
@declaration = declaration(source, identifier)
|
|
20
|
+
result = @lookup.call("::#{identifier}", allow_private: true)
|
|
21
|
+
@runtime = result[:value] if result[:status] == :resolved
|
|
22
|
+
@foreign = if @runtime
|
|
23
|
+
!owned_class?(identifier, file_path)
|
|
24
|
+
else
|
|
25
|
+
result[:status] == :unknown && !%w[autoload_pending unloaded_scope].include?(result[:reason])
|
|
26
|
+
end
|
|
27
|
+
@names = ancestry_names(@runtime) if @runtime && !@foreign
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [Boolean] whether this file can qualify as a delegator
|
|
31
|
+
def manager?
|
|
32
|
+
return false if @foreign
|
|
33
|
+
return @names.include?('Delegator') if @names
|
|
34
|
+
|
|
35
|
+
%w[SimpleDelegator Delegator].include?(parent_name&.delete_prefix('::')) || delegate_class? ||
|
|
36
|
+
@declaration&.fetch(:source)&.match?(/include\s+(?:::)?Delegator\b/) || false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [Symbol] supported delegation metadata, without running a factory
|
|
40
|
+
def delegation_type
|
|
41
|
+
return :simple_delegator if @names&.include?('SimpleDelegator')
|
|
42
|
+
return :simple_delegator if !@names && parent_name&.delete_prefix('::') == 'SimpleDelegator'
|
|
43
|
+
return :delegate_class if delegate_class?
|
|
44
|
+
|
|
45
|
+
:unknown
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [Boolean] whether a loaded class belongs to another file
|
|
49
|
+
def foreign?
|
|
50
|
+
!!@foreign
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [Boolean] conventional ApplicationPolicy ancestry
|
|
54
|
+
def application_policy?
|
|
55
|
+
return false if @foreign
|
|
56
|
+
return @names.drop(1).any? { |name| name.split('::').last == 'ApplicationPolicy' } if @names
|
|
57
|
+
|
|
58
|
+
parent_name&.split('::')&.last == 'ApplicationPolicy'
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def declaration(source, identifier)
|
|
64
|
+
ClassDeclarations.read(source).find { |record| record[:identifier] == identifier }
|
|
65
|
+
rescue ClassDeclarations::Unresolved
|
|
66
|
+
# Missing static evidence must not suppress independently owned runtime
|
|
67
|
+
# ancestry or change the generic policy extractor's emission policy.
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def parent_name
|
|
72
|
+
ClassDeclarations.parent_name(@declaration) if @declaration
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def delegate_class?
|
|
76
|
+
parent = @declaration&.fetch(:node)&.superclass
|
|
77
|
+
parent.is_a?(Prism::CallNode) && parent.name == :DelegateClass && parent.receiver.nil?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def owned_class?(identifier, path)
|
|
81
|
+
return false unless @lookup.class_object?(@runtime)
|
|
82
|
+
|
|
83
|
+
location = SOURCE_LOCATION.bind(Object).call(identifier, false)
|
|
84
|
+
location && File.expand_path(location.first) == File.expand_path(path)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def ancestry_names(klass)
|
|
88
|
+
@lookup.reflect(klass, :ancestors).filter_map { |ancestor| @lookup.reflect(ancestor, :name) }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -187,6 +187,14 @@ module Woods
|
|
|
187
187
|
all_paths = (data[:publishers] + data[:subscribers]).uniq
|
|
188
188
|
combined_source = load_source_files(all_paths)
|
|
189
189
|
|
|
190
|
+
# Keep absolute paths for source reads; emitted paths must not make
|
|
191
|
+
# metadata or annotation hashes depend on the checkout directory.
|
|
192
|
+
prefix = File.join(Rails.root.to_s, '')
|
|
193
|
+
data = data.merge(
|
|
194
|
+
publishers: data[:publishers].map { |path| path.delete_prefix(prefix) },
|
|
195
|
+
subscribers: data[:subscribers].map { |path| path.delete_prefix(prefix) }
|
|
196
|
+
)
|
|
197
|
+
|
|
190
198
|
unit = ExtractedUnit.new(
|
|
191
199
|
type: :event,
|
|
192
200
|
identifier: event_name,
|