Loading config/ontology/edges/extends.yaml +2 −2 Original line number Diff line number Diff line # config/ontology/edges/extends.yaml description: Supertype relationships in source code (inheritance, interface implementation, struct embedding) description: Supertype relationships in source code (inheritance, interface implementation, struct embedding, mixin composition) table: gl_code_edge variants: - from_node: { type: Definition, id: id } to_node: { type: Definition, id: id } description: "Definition declares a supertype relationship to another definition. Covers class extension, interface implementation, and struct embedding." description: "Definition declares a supertype relationship to another definition. Covers class extension, interface implementation, struct embedding, and mixin composition." crates/code-graph/src/v2/langs/generic/ruby.rs +61 −28 Original line number Diff line number Diff line Loading @@ -475,34 +475,25 @@ fn ruby_extract_imports(node: &N<'_>, imports: &mut Vec<CanonicalImport>) -> boo if node.kind().as_ref() != "call" { return false; } let method = match node.field("method") { Some(m) => m.text().to_string(), None => return false, }; if method != "require" && method != "require_relative" { let Some(method) = node.field("method").map(|m| m.text().to_string()) else { return false; } let arg = node .field("arguments") .and_then(|args| args.find(Child, Kind("string"))) .and_then(|s| s.find(Child, Kind("string_content"))) .map(|c| c.text().to_string()); let Some(path) = arg else { }; match method.as_str() { "require" | "require_relative" => { let Some(args) = node.field("arguments") else { return true; }; let import_type = if method == "require_relative" { let path = args .find(Child, Kind("string")) .and_then(|s| s.find(Child, Kind("string_content"))) .map(|c| c.text().to_string()); if let Some(path) = path { imports.push(CanonicalImport { import_type: if method == "require_relative" { "RequireRelative" } else { "Require" }; imports.push(CanonicalImport { import_type, }, binding_kind: ImportBindingKind::SideEffect, mode: ImportMode::Runtime, path, Loading @@ -513,9 +504,51 @@ fn ruby_extract_imports(node: &N<'_>, imports: &mut Vec<CanonicalImport>) -> boo is_type_only: false, wildcard: false, }); } true } "include" | "extend" | "prepend" => { let Some(args) = node.field("arguments") else { return true; }; let import_type = match method.as_str() { "include" => "Include", "extend" => "Extend", _ => "Prepend", }; for arg in args.children() { if !matches!(arg.kind().as_ref(), "constant" | "scope_resolution") { continue; } push_named_import(imports, import_type, arg.text().to_string()); } true } _ => false, } } fn push_named_import(imports: &mut Vec<CanonicalImport>, import_type: &'static str, fqn: String) { if fqn.is_empty() { return; } let (path, leaf) = fqn .rsplit_once("::") .map(|(p, l)| (p.to_string(), l.to_string())) .unwrap_or((String::new(), fqn)); imports.push(CanonicalImport { import_type, binding_kind: ImportBindingKind::Named, mode: ImportMode::Declarative, path, name: Some(leaf), alias: None, scope_fqn: None, range: crate::v2::types::Range::empty(), is_type_only: false, wildcard: false, }); } fn ruby_imported_symbol_candidates( graph: &CodeGraph, Loading crates/integration-tests-codegraph/fixtures/ruby/module_imports.yaml 0 → 100644 +95 −0 Original line number Diff line number Diff line name: "Ruby module composition imports" fixtures: - path: app/models/base_record.rb content: | class BaseRecord end - path: app/models/concerns/auditable.rb content: | module Auditable def audit_event true end end - path: app/models/concerns/cacheable.rb content: | module Cacheable def cache_key "cache" end end - path: app/models/concerns/gitlab/tracking.rb content: | module Gitlab module Tracking def track_internal_event true end end end - path: app/models/concerns/instrumentation.rb content: | module Instrumentation def measure true end end - path: app/models/report.rb content: | class Report < BaseRecord include Auditable, Gitlab::Tracking extend Cacheable prepend Instrumentation end tests: - name: "include emits one ImportedSymbol per module" query: | MATCH (f:File)-[e:FileToImportedSymbol]->(s:ImportedSymbol) WHERE f.path = 'app/models/report.rb' AND s.import_type = 'Include' RETURN s.name AS name, s.path AS path assert: - { row_count: 2 } - { row: { name: "Auditable", path: "" } } - { row: { name: "Tracking", path: "Gitlab" } } - name: "extend emits a module ImportedSymbol" query: | MATCH (f:File)-[e:FileToImportedSymbol]->(s:ImportedSymbol) WHERE f.path = 'app/models/report.rb' AND s.import_type = 'Extend' RETURN s.name AS name, s.path AS path assert: - { row_count: 1 } - { row: { name: "Cacheable", path: "" } } - name: "prepend emits a module ImportedSymbol" query: | MATCH (f:File)-[e:FileToImportedSymbol]->(s:ImportedSymbol) WHERE f.path = 'app/models/report.rb' AND s.import_type = 'Prepend' RETURN s.name AS name, s.path AS path assert: - { row_count: 1 } - { row: { name: "Instrumentation", path: "" } } - name: "class links to superclass and composed modules" query: | MATCH (child:Definition)-[e:DefinitionToDefinition]->(parent:Definition) WHERE child.fqn = 'Report' AND e.edge_kind = 'Extends' RETURN parent.fqn AS parent assert: - { row_count: 5 } - { row: { parent: "BaseRecord" } } - { row: { parent: "Auditable" } } - { row: { parent: "Gitlab::Tracking" } } - { row: { parent: "Cacheable" } } - { row: { parent: "Instrumentation" } } crates/integration-tests-codegraph/tests/suites.rs +1 −0 Original line number Diff line number Diff line Loading @@ -272,6 +272,7 @@ yaml_test!(ruby_v1_resolution, "ruby/v1_resolution.yaml"); yaml_test!(ruby_metaprogramming, "ruby/metaprogramming.yaml"); yaml_test!(ruby_gitlab_monolith, "ruby/gitlab_monolith.yaml"); yaml_test!(ruby_e2e_weather_app, "ruby/e2e_weather_app.yaml"); yaml_test!(ruby_module_imports, "ruby/module_imports.yaml"); yaml_test!( ruby_imported_symbol_fallback_matrix, "ruby/imported_symbol_fallback_matrix.yaml" Loading Loading
config/ontology/edges/extends.yaml +2 −2 Original line number Diff line number Diff line # config/ontology/edges/extends.yaml description: Supertype relationships in source code (inheritance, interface implementation, struct embedding) description: Supertype relationships in source code (inheritance, interface implementation, struct embedding, mixin composition) table: gl_code_edge variants: - from_node: { type: Definition, id: id } to_node: { type: Definition, id: id } description: "Definition declares a supertype relationship to another definition. Covers class extension, interface implementation, and struct embedding." description: "Definition declares a supertype relationship to another definition. Covers class extension, interface implementation, struct embedding, and mixin composition."
crates/code-graph/src/v2/langs/generic/ruby.rs +61 −28 Original line number Diff line number Diff line Loading @@ -475,34 +475,25 @@ fn ruby_extract_imports(node: &N<'_>, imports: &mut Vec<CanonicalImport>) -> boo if node.kind().as_ref() != "call" { return false; } let method = match node.field("method") { Some(m) => m.text().to_string(), None => return false, }; if method != "require" && method != "require_relative" { let Some(method) = node.field("method").map(|m| m.text().to_string()) else { return false; } let arg = node .field("arguments") .and_then(|args| args.find(Child, Kind("string"))) .and_then(|s| s.find(Child, Kind("string_content"))) .map(|c| c.text().to_string()); let Some(path) = arg else { }; match method.as_str() { "require" | "require_relative" => { let Some(args) = node.field("arguments") else { return true; }; let import_type = if method == "require_relative" { let path = args .find(Child, Kind("string")) .and_then(|s| s.find(Child, Kind("string_content"))) .map(|c| c.text().to_string()); if let Some(path) = path { imports.push(CanonicalImport { import_type: if method == "require_relative" { "RequireRelative" } else { "Require" }; imports.push(CanonicalImport { import_type, }, binding_kind: ImportBindingKind::SideEffect, mode: ImportMode::Runtime, path, Loading @@ -513,9 +504,51 @@ fn ruby_extract_imports(node: &N<'_>, imports: &mut Vec<CanonicalImport>) -> boo is_type_only: false, wildcard: false, }); } true } "include" | "extend" | "prepend" => { let Some(args) = node.field("arguments") else { return true; }; let import_type = match method.as_str() { "include" => "Include", "extend" => "Extend", _ => "Prepend", }; for arg in args.children() { if !matches!(arg.kind().as_ref(), "constant" | "scope_resolution") { continue; } push_named_import(imports, import_type, arg.text().to_string()); } true } _ => false, } } fn push_named_import(imports: &mut Vec<CanonicalImport>, import_type: &'static str, fqn: String) { if fqn.is_empty() { return; } let (path, leaf) = fqn .rsplit_once("::") .map(|(p, l)| (p.to_string(), l.to_string())) .unwrap_or((String::new(), fqn)); imports.push(CanonicalImport { import_type, binding_kind: ImportBindingKind::Named, mode: ImportMode::Declarative, path, name: Some(leaf), alias: None, scope_fqn: None, range: crate::v2::types::Range::empty(), is_type_only: false, wildcard: false, }); } fn ruby_imported_symbol_candidates( graph: &CodeGraph, Loading
crates/integration-tests-codegraph/fixtures/ruby/module_imports.yaml 0 → 100644 +95 −0 Original line number Diff line number Diff line name: "Ruby module composition imports" fixtures: - path: app/models/base_record.rb content: | class BaseRecord end - path: app/models/concerns/auditable.rb content: | module Auditable def audit_event true end end - path: app/models/concerns/cacheable.rb content: | module Cacheable def cache_key "cache" end end - path: app/models/concerns/gitlab/tracking.rb content: | module Gitlab module Tracking def track_internal_event true end end end - path: app/models/concerns/instrumentation.rb content: | module Instrumentation def measure true end end - path: app/models/report.rb content: | class Report < BaseRecord include Auditable, Gitlab::Tracking extend Cacheable prepend Instrumentation end tests: - name: "include emits one ImportedSymbol per module" query: | MATCH (f:File)-[e:FileToImportedSymbol]->(s:ImportedSymbol) WHERE f.path = 'app/models/report.rb' AND s.import_type = 'Include' RETURN s.name AS name, s.path AS path assert: - { row_count: 2 } - { row: { name: "Auditable", path: "" } } - { row: { name: "Tracking", path: "Gitlab" } } - name: "extend emits a module ImportedSymbol" query: | MATCH (f:File)-[e:FileToImportedSymbol]->(s:ImportedSymbol) WHERE f.path = 'app/models/report.rb' AND s.import_type = 'Extend' RETURN s.name AS name, s.path AS path assert: - { row_count: 1 } - { row: { name: "Cacheable", path: "" } } - name: "prepend emits a module ImportedSymbol" query: | MATCH (f:File)-[e:FileToImportedSymbol]->(s:ImportedSymbol) WHERE f.path = 'app/models/report.rb' AND s.import_type = 'Prepend' RETURN s.name AS name, s.path AS path assert: - { row_count: 1 } - { row: { name: "Instrumentation", path: "" } } - name: "class links to superclass and composed modules" query: | MATCH (child:Definition)-[e:DefinitionToDefinition]->(parent:Definition) WHERE child.fqn = 'Report' AND e.edge_kind = 'Extends' RETURN parent.fqn AS parent assert: - { row_count: 5 } - { row: { parent: "BaseRecord" } } - { row: { parent: "Auditable" } } - { row: { parent: "Gitlab::Tracking" } } - { row: { parent: "Cacheable" } } - { row: { parent: "Instrumentation" } }
crates/integration-tests-codegraph/tests/suites.rs +1 −0 Original line number Diff line number Diff line Loading @@ -272,6 +272,7 @@ yaml_test!(ruby_v1_resolution, "ruby/v1_resolution.yaml"); yaml_test!(ruby_metaprogramming, "ruby/metaprogramming.yaml"); yaml_test!(ruby_gitlab_monolith, "ruby/gitlab_monolith.yaml"); yaml_test!(ruby_e2e_weather_app, "ruby/e2e_weather_app.yaml"); yaml_test!(ruby_module_imports, "ruby/module_imports.yaml"); yaml_test!( ruby_imported_symbol_fallback_matrix, "ruby/imported_symbol_fallback_matrix.yaml" Loading