Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions ext/rubydex/definition.c
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,20 @@ static VALUE rdxr_definition_name(VALUE self) {
return str;
}

// Class/module and constant definition #source_name -> String
static VALUE rdxr_definition_source_name(VALUE self) {
HandleData *data;
TypedData_Get_Struct(self, HandleData, &handle_type, data);

void *graph;
TypedData_Get_Struct(data->graph_obj, void *, &graph_type, graph);

const char *name = rdx_definition_source_name(graph, data->id);
VALUE str = rb_utf8_str_new_cstr(name);
free_c_string(name);
return str;
}

// Definition#deprecated? -> bool
static VALUE rdxr_definition_deprecated(VALUE self) {
HandleData *data;
Expand Down Expand Up @@ -394,17 +408,21 @@ void rdxi_initialize_definition(VALUE mod) {
rb_define_method(cDefinition, "lexical_nesting", rdxr_definition_lexical_nesting, 0);

cClassDefinition = rb_define_class_under(mRubydex, "ClassDefinition", cDefinition);
rb_define_method(cClassDefinition, "source_name", rdxr_definition_source_name, 0);
rb_define_method(cClassDefinition, "superclass", rdxr_class_definition_superclass, 0);
rb_define_method(cClassDefinition, "mixins", rdxr_definition_mixins, 0);

cSingletonClassDefinition = rb_define_class_under(mRubydex, "SingletonClassDefinition", cDefinition);
rb_define_method(cSingletonClassDefinition, "mixins", rdxr_definition_mixins, 0);

cModuleDefinition = rb_define_class_under(mRubydex, "ModuleDefinition", cDefinition);
rb_define_method(cModuleDefinition, "source_name", rdxr_definition_source_name, 0);
rb_define_method(cModuleDefinition, "mixins", rdxr_definition_mixins, 0);

cConstantDefinition = rb_define_class_under(mRubydex, "ConstantDefinition", cDefinition);
rb_define_method(cConstantDefinition, "source_name", rdxr_definition_source_name, 0);
cConstantAliasDefinition = rb_define_class_under(mRubydex, "ConstantAliasDefinition", cDefinition);
rb_define_method(cConstantAliasDefinition, "source_name", rdxr_definition_source_name, 0);
cConstantVisibilityDefinition = rb_define_class_under(mRubydex, "ConstantVisibilityDefinition", cDefinition);
cMethodVisibilityDefinition = rb_define_class_under(mRubydex, "MethodVisibilityDefinition", cDefinition);
cMethodDefinition = rb_define_class_under(mRubydex, "MethodDefinition", cDefinition);
Expand Down
62 changes: 61 additions & 1 deletion rust/rubydex-sys/src/definition_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use crate::location_api::{Location, create_location_for_uri_and_offset};
use crate::reference_api::CConstantReference;
use libc::c_char;
use rubydex::model::definitions::{Definition, Mixin};
use rubydex::model::ids::DefinitionId;
use rubydex::model::graph::Graph;
use rubydex::model::ids::{DefinitionId, NameId};
use rubydex::model::name::ParentScope;
use rubydex::query::AliasResolutionError;
use std::ffi::CString;
use std::ptr;
Expand Down Expand Up @@ -110,6 +112,64 @@ pub unsafe extern "C" fn rdx_definition_name(pointer: GraphPointer, definition_i
})
}

fn source_name_for_name(graph: &Graph, name_id: NameId) -> String {
let name = graph
.names()
.get(&name_id)
.expect("name should exist while building source_name");
let simple_name_id = *name.str();
let simple_name = graph
.strings()
.get(&simple_name_id)
.expect("string should exist while building source_name")
.as_str();

match name.parent_scope() {
ParentScope::None => simple_name.to_string(),
ParentScope::TopLevel => format!("::{simple_name}"),
ParentScope::Some(parent_id) | ParentScope::Attached(parent_id) => {
let parent_name = source_name_for_name(graph, *parent_id);
format!("{parent_name}::{simple_name}")
}
}
}

fn definition_source_name_id(definition: &Definition) -> NameId {
match definition {
Definition::Class(definition) => *definition.name_id(),
Definition::Module(definition) => *definition.name_id(),
Definition::Constant(definition) => *definition.name_id(),
Definition::ConstantAlias(definition) => *definition.name_id(),
_ => panic!("source_name is only available for class, module, and constant definitions"),
}
}

/// Returns the UTF-8 source name string for a class, module, or constant definition id.
///
/// The source name is reconstructed from the definition's `Name`, preserving explicit parent scopes like `Foo::Bar`
/// and rooted paths like `::Bar`. Caller must free with `free_c_string`.
///
/// # Safety
///
/// Assumes pointer is valid.
///
/// # Panics
///
/// This function will panic if the definition cannot be found or if it is not a class, module, or constant definition.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rdx_definition_source_name(pointer: GraphPointer, definition_id: u64) -> *const c_char {
with_graph(pointer, |graph| {
let def_id = DefinitionId::new(definition_id);
let definition = graph
.definitions()
.get(&def_id)
.unwrap_or_else(|| panic!("Definition not found: {definition_id:?}"));

let name = source_name_for_name(graph, definition_source_name_id(definition));
CString::new(name).unwrap().into_raw().cast_const()
})
}

/// Shared iterator over definition (id, kind) pairs
#[derive(Debug)]
pub struct DefinitionsIter {
Expand Down
2 changes: 1 addition & 1 deletion rust/rubydex-sys/src/graph_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ pub unsafe extern "C" fn rdx_graph_resolve_constant(

let mut resolver = Resolver::new(graph);

let declaration = match resolver.resolve_constant(name_id) {
let declaration = match resolver.resolve_constant_rec(name_id) {
Some(id) => {
let decl = graph.declarations().get(&id).unwrap();
Box::into_raw(Box::new(CDeclaration::from_declaration(id, decl))).cast_const()
Expand Down
35 changes: 35 additions & 0 deletions rust/rubydex-sys/src/name_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,39 @@ mod tests {
assert!(foo_name.parent_scope().is_none());
assert!(foo_name.nesting().is_none());
}

#[test]
fn rooted_fully_qualified_nesting_is_converted_to_name_id() {
let mut graph = Graph::new();

let (name_id, _) =
nesting_stack_to_name_id(&mut graph, "CONST", vec!["::Foo".into(), "::Foo::Bar".into()]).unwrap();

let const_name = graph.names().get(&name_id).unwrap();
assert_eq!(StringId::from("CONST"), *const_name.str());
assert!(const_name.parent_scope().is_none());

// Equivalent to resolving `CONST` from:
//
// module ::Foo
// module ::Foo::Bar
// CONST
// end
// end
let bar_name = graph.names().get(&const_name.nesting().unwrap()).unwrap();
assert_eq!(StringId::from("Bar"), *bar_name.str());

let bar_parent_foo_name = graph
.names()
.get(&bar_name.parent_scope().expect("Parent scope should exist"))
.unwrap();
assert_eq!(StringId::from("Foo"), *bar_parent_foo_name.str());
assert!(bar_parent_foo_name.parent_scope().is_top_level());
assert_eq!(bar_name.nesting(), bar_parent_foo_name.nesting());

let outer_foo_name = graph.names().get(&bar_name.nesting().unwrap()).unwrap();
assert_eq!(StringId::from("Foo"), *outer_foo_name.str());
assert!(outer_foo_name.parent_scope().is_top_level());
assert!(outer_foo_name.nesting().is_none());
}
}
34 changes: 34 additions & 0 deletions rust/rubydex/src/resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,40 @@ impl<'a> Resolver<'a> {
}
}

/// Resolves a single constant and the names it depends on against the graph. This is meant for APIs that create a
/// temporary name chain outside of the normal resolution phase.
pub fn resolve_constant_rec(&mut self, name_id: NameId) -> Option<DeclarationId> {
self.resolve_constant_rec_internal(name_id, &mut HashSet::new())
}

fn resolve_constant_rec_internal(
&mut self,
name_id: NameId,
resolving: &mut HashSet<NameId>,
) -> Option<DeclarationId> {
if !resolving.insert(name_id) {
return self.graph.name_id_to_declaration_id(name_id).copied();
}

let Some(name_ref) = self.graph.names().get(&name_id).cloned() else {
resolving.remove(&name_id);
return None;
};

if let Some(nesting_id) = name_ref.nesting() {
self.resolve_constant_rec_internal(*nesting_id, resolving);
}

if let Some(parent_scope_id) = name_ref.parent_scope().as_ref() {
self.resolve_constant_rec_internal(*parent_scope_id, resolving);
}

let result = self.resolve_constant(name_id);
resolving.remove(&name_id);

result
}

/// Handles a unit of work for resolving a constant definition or singleton method
fn handle_definition_unit(&mut self, unit_id: Unit, id: DefinitionId) {
let mut needs_linearization = false;
Expand Down
113 changes: 112 additions & 1 deletion rust/rubydex/src/resolution_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ use crate::{
assert_diagnostics_eq, assert_instance_variables_eq, assert_members_eq, assert_no_constant_alias_target,
assert_no_diagnostics, assert_no_members, assert_owner_eq, assert_singleton_class_eq,
diagnostic::Rule,
model::{declaration::Ancestors, ids::DeclarationId, name::NameRef},
model::{
declaration::Ancestors,
ids::{DeclarationId, StringId},
name::{Name, NameRef, ParentScope},
},
resolution::Resolver,
test_utils::GraphTest,
};
Expand All @@ -21,6 +25,113 @@ fn graph_test() -> GraphTest {
mod constant_resolution_tests {
use super::*;

#[test]
fn recursive_constant_resolution_resolves_parent_scope_chain_first() {
let mut context = graph_test();
context.index_uri("file:///foo.rb", {
r"
module Foo
module Bar
CONST = 1
end
end
"
});
context.resolve();

// Equivalent to resolving:
//
// ::Foo::Bar::CONST
let mut graph = context.into_graph();
let foo_id = graph.add_name(Name::new(StringId::from("Foo"), ParentScope::TopLevel, None));
let bar_id = graph.add_name(Name::new(StringId::from("Bar"), ParentScope::Some(foo_id), None));
let const_id = graph.add_name(Name::new(StringId::from("CONST"), ParentScope::Some(bar_id), None));

let mut resolver = Resolver::new(&mut graph);

assert_eq!(None, resolver.resolve_constant(const_id));
assert_eq!(
Some(DeclarationId::from("Foo::Bar::CONST")),
resolver.resolve_constant_rec(const_id)
);
}

#[test]
fn recursive_constant_resolution_resolves_nesting_names_first() {
let mut context = graph_test();
context.index_uri("file:///foo.rb", {
r"
module Foo
module Bar
CONST = 1
end
end
"
});
context.resolve();

// Equivalent to resolving `CONST` from:
//
// module ::Foo
// module ::Foo::Bar
// CONST
// end
// end
let mut graph = context.into_graph();
let outer_foo_id = graph.add_name(Name::new(StringId::from("Foo"), ParentScope::TopLevel, None));
let inner_parent_foo_id = graph.add_name(Name::new(
StringId::from("Foo"),
ParentScope::TopLevel,
Some(outer_foo_id),
));
let inner_bar_id = graph.add_name(Name::new(
StringId::from("Bar"),
ParentScope::Some(inner_parent_foo_id),
Some(outer_foo_id),
));
let const_id = graph.add_name(Name::new(
StringId::from("CONST"),
ParentScope::None,
Some(inner_bar_id),
));

let mut resolver = Resolver::new(&mut graph);

assert_eq!(None, resolver.resolve_constant(const_id));
assert_eq!(
Some(DeclarationId::from("Foo::Bar::CONST")),
resolver.resolve_constant_rec(const_id)
);
}

#[test]
fn recursive_constant_resolution_resolves_attached_parent_scope_names_first() {
let mut context = graph_test();
context.index_uri("file:///foo.rb", {
r"
class Foo; end
"
});
context.resolve();

// Equivalent to resolving `self` from:
//
// class << ::Foo
// self # equivalent to ::Foo::<Foo>
// end
let mut graph = context.into_graph();
let foo_id = graph.add_name(Name::new(StringId::from("Foo"), ParentScope::TopLevel, None));
let singleton_id = graph.add_name(Name::new(StringId::from("<Foo>"), ParentScope::Attached(foo_id), None));

let mut resolver = Resolver::new(&mut graph);

assert_eq!(None, resolver.resolve_constant(singleton_id));
assert_eq!(
Some(DeclarationId::from("Foo::<Foo>")),
resolver.resolve_constant_rec(singleton_id)
);
}

#[test]
fn resolving_top_level_references() {
let mut context = graph_test();
Expand Down
39 changes: 39 additions & 0 deletions test/definition_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,45 @@ class ::Bar; end
end
end

def test_definition_source_name
with_context do |context|
context.write!("file1.rb", <<~RUBY)
class Foo
class Bar; end
class ::Baz; end
module Qux::Quux; end

CONST = 1
::ROOT_CONST = 2
Foo::NESTED_CONST = 3
ALIAS_CONST = Baz

def foo; end
end
RUBY

graph = Rubydex::Graph.new
graph.index_all(context.glob("**/*.rb"))

definitions = graph.documents.find { |d| d.uri == context.uri_to("file1.rb") }.definitions.to_a
source_names = definitions
.select { |definition| definition.respond_to?(:source_name) }
.map { |definition| [definition.class, definition.name, definition.source_name] }

assert_includes(source_names, [Rubydex::ClassDefinition, "Foo", "Foo"])
assert_includes(source_names, [Rubydex::ClassDefinition, "Bar", "Bar"])
assert_includes(source_names, [Rubydex::ClassDefinition, "Baz", "::Baz"])
assert_includes(source_names, [Rubydex::ModuleDefinition, "Quux", "Qux::Quux"])
assert_includes(source_names, [Rubydex::ConstantDefinition, "CONST", "CONST"])
assert_includes(source_names, [Rubydex::ConstantDefinition, "ROOT_CONST", "::ROOT_CONST"])
assert_includes(source_names, [Rubydex::ConstantDefinition, "NESTED_CONST", "Foo::NESTED_CONST"])
assert_includes(source_names, [Rubydex::ConstantAliasDefinition, "ALIAS_CONST", "ALIAS_CONST"])

method_def = definitions.find { |definition| definition.is_a?(Rubydex::MethodDefinition) }
refute_respond_to(method_def, :source_name)
end
end

def test_definition_declaration
with_context do |context|
context.write!("file1.rb", <<~RUBY)
Expand Down
Loading
Loading