diff --git a/ast/src/lang/registry/mod.rs b/ast/src/lang/registry/mod.rs index 56bf82a43..8fd557467 100644 --- a/ast/src/lang/registry/mod.rs +++ b/ast/src/lang/registry/mod.rs @@ -11,6 +11,8 @@ pub mod php_registry; pub mod php_resolver; pub mod py_resolver; pub mod python; +pub mod ruby_registry; +pub mod ruby_resolver; pub mod rust_registry; pub mod rust_resolver; pub mod swift_registry; @@ -49,6 +51,7 @@ pub fn build( Language::Kotlin => Some(Box::new(kotlin_registry::KotlinRegistry::new(graph, filez))), Language::Swift => Some(Box::new(swift_registry::SwiftRegistry::new(graph, filez))), Language::Php => Some(Box::new(php_registry::PhpRegistry::new(graph, filez))), + Language::Ruby => Some(Box::new(ruby_registry::RubyRegistry::new(graph, filez))), _ => None, } } diff --git a/ast/src/lang/registry/ruby_registry.rs b/ast/src/lang/registry/ruby_registry.rs new file mode 100644 index 000000000..9a3c77ad3 --- /dev/null +++ b/ast/src/lang/registry/ruby_registry.rs @@ -0,0 +1,80 @@ +use super::{ruby_resolver, Registry}; +use crate::lang::asg::NodeKeys; +use crate::lang::graphs::{Graph, NodeType}; +use std::collections::HashMap; +use std::path::Path; + +fn parent_dir(file: &str) -> String { + Path::new(file) + .parent() + .map(|p| p.to_string_lossy().into_owned()) + .unwrap_or_default() +} + +pub struct RubyRegistry { + dir_fns: HashMap>, + resolved: HashMap<(String, usize, usize), NodeKeys>, +} + +impl RubyRegistry { + pub fn new(graph: &impl Graph, filez: &[(String, String)]) -> Self { + let mut reg = RubyRegistry { + dir_fns: HashMap::new(), + resolved: HashMap::new(), + }; + + // Pass 1: index Function nodes by directory for bare-name fallback. + for (node_type, node_data) in graph.iter_all_nodes() { + if !node_data.file.ends_with(".rb") { + continue; + } + if *node_type != NodeType::Function { + continue; + } + let dir = parent_dir(&node_data.file); + reg.dir_fns + .entry(dir) + .or_default() + .entry(node_data.name.clone()) + .or_insert_with(|| NodeKeys::from(node_data)); + } + + // Pass 2: pre-resolve all call sites per file. + // Skip spec/test files: the default resolver handles those and must + // preserve class_call edges that test annotations rely on. + let all_resolved: Vec<((String, usize, usize), NodeKeys)> = filez + .iter() + .filter(|(f, _)| { + f.ends_with(".rb") + && !f.contains("/spec/") + && !f.contains("/test/") + && !f.ends_with("_spec.rb") + && !f.ends_with("_test.rb") + }) + .flat_map(|(file, source)| { + ruby_resolver::resolve_file_calls(source, file, ®.dir_fns, graph) + .into_iter() + .map(|((row, col), nk)| ((file.clone(), row, col), nk)) + }) + .collect(); + reg.resolved.extend(all_resolved); + + reg + } +} + +impl Registry for RubyRegistry { + fn resolve_type(&self, _file: &str, _var_name: &str) -> Option<&str> { + None + } + + fn resolve_method(&self, _type_name: &str, _method_name: &str) -> Option<&str> { + None + } + + fn resolve_call_at(&self, file: &str, row: usize, col: usize) -> Option { + self.resolved + .get(&(file.to_string(), row, col)) + .cloned() + } +} diff --git a/ast/src/lang/registry/ruby_resolver.rs b/ast/src/lang/registry/ruby_resolver.rs new file mode 100644 index 000000000..009c5a945 --- /dev/null +++ b/ast/src/lang/registry/ruby_resolver.rs @@ -0,0 +1,217 @@ +use super::scope::{scope_bind, scope_lookup, scope_pop, scope_push, Scope}; +use crate::lang::asg::NodeKeys; +use crate::lang::graphs::{Graph, NodeType}; +use std::collections::HashMap; +use std::path::Path; +use tree_sitter::{Node, Parser}; + +fn make_parser() -> Option { + let mut parser = Parser::new(); + let lang: tree_sitter::Language = tree_sitter_ruby::LANGUAGE.into(); + parser.set_language(&lang).ok()?; + Some(parser) +} + +fn parent_dir(file: &str) -> String { + Path::new(file) + .parent() + .map(|p| p.to_string_lossy().into_owned()) + .unwrap_or_default() +} + +// ── Method lookup ────────────────────────────────────────────────────────────── + +fn find_method_in_class( + graph: &G, + class_name: &str, + method_name: &str, +) -> Option { + graph + .find_nodes_by_name(NodeType::Function, method_name) + .into_iter() + .find(|n| n.meta.get("operand").map(|s| s.as_str()) == Some(class_name)) + .map(|n| NodeKeys::from(&n)) +} + +// ── Type evaluator ───────────────────────────────────────────────────────────── + +// Ruby has no type annotations, so resolution is limited to: +// - direct constant (ClassName) +// - local variable previously bound via ClassName.new / ClassName.find / etc. +// - ClassName.new(...) call expression itself +fn eval_expr_type(scope: &Scope, node: Node, src: &[u8]) -> Option { + match node.kind() { + "constant" => node.utf8_text(src).ok().map(str::to_string), + "identifier" => scope_lookup(scope, node.utf8_text(src).ok()?).map(str::to_string), + "call" => { + let receiver = node.child_by_field_name("receiver")?; + let method = node.child_by_field_name("method")?; + // ClassName.new(...) → type is ClassName + if receiver.kind() == "constant" + && method.utf8_text(src).ok()? == "new" + { + receiver.utf8_text(src).ok().map(str::to_string) + } else { + None + } + } + _ => None, + } +} + +// ── AST walker ───────────────────────────────────────────────────────────────── + +// ActiveRecord class-level finders: when called on a constant, bind the left-hand +// variable to that constant's type so chained calls can be resolved. +const BINDING_METHODS: &[&str] = &["new", "find", "find_by", "find_by!", "create", "first", "last", "build"]; + +fn walk_node( + node: Node, + src: &[u8], + scope: &mut Scope, + dir_fns: &HashMap>, + graph: &G, + out: &mut HashMap<(usize, usize), NodeKeys>, + file: &str, +) { + match node.kind() { + "class" | "module" => { + let class_name = node + .child_by_field_name("name") + .and_then(|n| n.utf8_text(src).ok().map(str::to_string)); + scope_push(scope); + if let Some(ref name) = class_name { + scope_bind(scope, "self", name); + } + // body_statement is a named child (no field name in tree-sitter-ruby) + for i in 0..node.named_child_count() { + if let Some(child) = node.named_child(i) { + if child.kind() == "body_statement" { + walk_node(child, src, scope, dir_fns, graph, out, file); + } + } + } + scope_pop(scope); + } + + "method" | "singleton_method" => { + scope_push(scope); + if let Some(body) = node.child_by_field_name("body").or_else(|| { + (0..node.named_child_count()) + .filter_map(|i| node.named_child(i)) + .find(|n| n.kind() == "body_statement") + }) { + walk_node(body, src, scope, dir_fns, graph, out, file); + } + scope_pop(scope); + } + + "assignment" => { + let left = node.child_by_field_name("left"); + let right = node.child_by_field_name("right"); + if let (Some(left), Some(right)) = (left, right) { + // Bind local variable when assigned from a typed expression. + if left.kind() == "identifier" { + if let Ok(var_name) = left.utf8_text(src) { + // ClassName.new / ClassName.find / etc. → bind var → ClassName + let type_name = if right.kind() == "call" { + let recv = right.child_by_field_name("receiver"); + let meth = right.child_by_field_name("method"); + match (recv, meth) { + (Some(r), Some(m)) + if r.kind() == "constant" + && m.utf8_text(src) + .map(|s| BINDING_METHODS.contains(&s)) + .unwrap_or(false) => + { + r.utf8_text(src).ok().map(str::to_string) + } + _ => None, + } + } else { + eval_expr_type(scope, right, src) + }; + if let Some(t) = type_name { + scope_bind(scope, var_name, &t); + } + } + } + // Always recurse into right side for nested calls. + walk_node(right, src, scope, dir_fns, graph, out, file); + } + } + + "call" => { + let receiver_node = node.child_by_field_name("receiver"); + let method_node = node.child_by_field_name("method"); + + if let Some(method_node) = method_node { + let method_name = method_node.utf8_text(src).ok(); + + let resolved = if let Some(recv) = receiver_node { + // Typed receiver: constant or scope-tracked variable. + eval_expr_type(scope, recv, src).and_then(|class_name| { + method_name.and_then(|mn| { + let pos = { + let p = method_node.start_position(); + (p.row, p.column) + }; + find_method_in_class(graph, &class_name, mn).map(|t| (pos, t)) + }) + }) + } else if let Some(mn) = method_name { + // Bare function call → same-directory fallback. + let dir = parent_dir(file); + let pos = { + let p = method_node.start_position(); + (p.row, p.column) + }; + dir_fns.get(&dir).and_then(|m| m.get(mn)).map(|nk| (pos, nk.clone())) + } else { + None + }; + + if let Some((pos, target)) = resolved { + out.entry(pos).or_insert(target); + } + } + + // Recurse into arguments and block. + if let Some(args) = node.child_by_field_name("arguments") { + walk_node(args, src, scope, dir_fns, graph, out, file); + } + if let Some(block) = node.child_by_field_name("block") { + walk_node(block, src, scope, dir_fns, graph, out, file); + } + } + + _ => { + for i in 0..node.named_child_count() { + if let Some(child) = node.named_child(i) { + walk_node(child, src, scope, dir_fns, graph, out, file); + } + } + } + } +} + +// ── Public entry point ───────────────────────────────────────────────────────── + +pub fn resolve_file_calls( + source: &str, + file: &str, + dir_fns: &HashMap>, + graph: &G, +) -> HashMap<(usize, usize), NodeKeys> { + let mut out = HashMap::new(); + let Some(mut parser) = make_parser() else { + return out; + }; + let Some(tree) = parser.parse(source, None) else { + return out; + }; + let src = source.as_bytes(); + let mut scope: Scope = vec![HashMap::new()]; + walk_node(tree.root_node(), src, &mut scope, dir_fns, graph, &mut out, file); + out +} diff --git a/ast/src/repo.rs b/ast/src/repo.rs index daf79de4e..abbdcf502 100644 --- a/ast/src/repo.rs +++ b/ast/src/repo.rs @@ -691,11 +691,10 @@ impl Repo { let fname = path.display().to_string(); let rel = path.strip_prefix(&self.root).unwrap_or(path); - let rel_str = rel.display().to_string(); - let in_skipped_dir = conf - .skip_dirs - .iter() - .any(|sd| rel_str == *sd || rel_str.starts_with(&format!("{}/", sd))); + let in_skipped_dir = conf.skip_dirs.iter().any(|sd| { + rel.components() + .any(|c| c.as_os_str().to_str() == Some(sd.as_str())) + }); if in_skipped_dir { return true; } @@ -875,10 +874,13 @@ fn skip_dir(entry: &DirEntry, skip_dirs: &[String], root: &PathBuf) -> bool { } let entry_path = entry.path(); let relative = entry_path.strip_prefix(root).unwrap_or(entry_path); - let relative_str = relative.display().to_string(); - let should_skip = skip_dirs - .iter() - .any(|sd| relative_str == *sd || relative_str.starts_with(&format!("{}/", sd))); + // Match any path component by name so that e.g. "migrate" skips "db/migrate" as well as + // top-level "migrate/". + let should_skip = skip_dirs.iter().any(|sd| { + relative + .components() + .any(|c| c.as_os_str().to_str() == Some(sd.as_str())) + }); should_skip } fn only_files(path: &std::path::Path, only_include_files: &[String]) -> bool { diff --git a/ast/src/testing/coverage/ruby.rs b/ast/src/testing/coverage/ruby.rs index aee934c8e..01c1f4695 100644 --- a/ast/src/testing/coverage/ruby.rs +++ b/ast/src/testing/coverage/ruby.rs @@ -295,8 +295,8 @@ async fn test_nodes_class_type() -> Result<()> { ) .await?; - assert_eq!(count, 35, "Should have 35 Class nodes"); - assert_eq!(results.len(), 35); + assert_eq!(count, 32, "Should have 32 Class nodes"); + assert_eq!(results.len(), 32); Ok(()) } diff --git a/ast/src/testing/ruby/app/channels/chat_channel.rb b/ast/src/testing/ruby/app/channels/chat_channel.rb index f09d279c7..c99bd61dd 100644 --- a/ast/src/testing/ruby/app/channels/chat_channel.rb +++ b/ast/src/testing/ruby/app/channels/chat_channel.rb @@ -1,12 +1,15 @@ class ChatChannel < ApplicationCable::Channel + # @ast node: Function "subscribed" def subscribed stream_from "chat_#{params[:room_id]}" end + # @ast node: Function "unsubscribed" def unsubscribed stop_all_streams end + # @ast node: Function "speak" def speak(data) ActionCable.server.broadcast("chat_#{params[:room_id]}", message: data['message']) end diff --git a/ast/src/testing/ruby/app/controllers/admin/settings_controller.rb b/ast/src/testing/ruby/app/controllers/admin/settings_controller.rb index 212edf02e..0cffef402 100644 --- a/ast/src/testing/ruby/app/controllers/admin/settings_controller.rb +++ b/ast/src/testing/ruby/app/controllers/admin/settings_controller.rb @@ -1,10 +1,12 @@ module Admin class SettingsController < ApplicationController + # @ast node: Function "index" def index settings = fetch_all_settings render json: settings, status: :ok end + # @ast node: Function "update" def update setting = update_setting(params[:id]) render json: setting, status: :ok @@ -12,10 +14,12 @@ def update private + # @ast node: Function "fetch_all_settings" def fetch_all_settings [] end + # @ast node: Function "update_setting" def update_setting(id) {} end diff --git a/ast/src/testing/ruby/app/controllers/api/v1/health_controller.rb b/ast/src/testing/ruby/app/controllers/api/v1/health_controller.rb index e83b5806f..e3a417a6f 100644 --- a/ast/src/testing/ruby/app/controllers/api/v1/health_controller.rb +++ b/ast/src/testing/ruby/app/controllers/api/v1/health_controller.rb @@ -1,6 +1,7 @@ module Api module V1 class HealthController < ApplicationController + # @ast node: Function "status" def status render json: { status: 'ok', version: 'v1' }, status: :ok end diff --git a/ast/src/testing/ruby/app/controllers/api/v1/tokens_controller.rb b/ast/src/testing/ruby/app/controllers/api/v1/tokens_controller.rb index 48c87961f..ea27d8ace 100644 --- a/ast/src/testing/ruby/app/controllers/api/v1/tokens_controller.rb +++ b/ast/src/testing/ruby/app/controllers/api/v1/tokens_controller.rb @@ -1,11 +1,13 @@ module Api module V1 class TokensController < ApplicationController + # @ast node: Function "create" def create token = generate_token render json: { token: token }, status: :created end + # @ast node: Function "destroy" def destroy revoke_token(params[:id]) render json: { message: 'Token revoked' }, status: :ok @@ -13,10 +15,12 @@ def destroy private + # @ast node: Function "generate_token" def generate_token SecureRandom.hex(32) end + # @ast node: Function "revoke_token" def revoke_token(token_id) # Token revocation logic end diff --git a/ast/src/testing/ruby/app/controllers/articles_controller.rb b/ast/src/testing/ruby/app/controllers/articles_controller.rb index 70ae8cc18..1ec7fbdf8 100644 --- a/ast/src/testing/ruby/app/controllers/articles_controller.rb +++ b/ast/src/testing/ruby/app/controllers/articles_controller.rb @@ -1,5 +1,6 @@ class ArticlesController < ApplicationController private + # @ast node: Function "article_params" def article_params params.require(:article).permit(:title, :body) end diff --git a/ast/src/testing/ruby/app/controllers/authors_controller.rb b/ast/src/testing/ruby/app/controllers/authors_controller.rb index 1c999a002..1fc2e02b2 100644 --- a/ast/src/testing/ruby/app/controllers/authors_controller.rb +++ b/ast/src/testing/ruby/app/controllers/authors_controller.rb @@ -1,9 +1,11 @@ class AuthorsController < ApplicationController + # @ast node: Function "index" def index authors = Author.all render json: authors, status: :ok end + # @ast node: Function "create" def create author = Author.new(author_params) if author.save @@ -13,6 +15,7 @@ def create end end + # @ast node: Function "show" def show author = Author.find(params[:id]) render json: author, status: :ok @@ -20,6 +23,7 @@ def show private + # @ast node: Function "author_params" def author_params params.require(:author).permit(:name, :bio) end diff --git a/ast/src/testing/ruby/app/controllers/books_controller.rb b/ast/src/testing/ruby/app/controllers/books_controller.rb index dd7ad9c99..85a130c4f 100644 --- a/ast/src/testing/ruby/app/controllers/books_controller.rb +++ b/ast/src/testing/ruby/app/controllers/books_controller.rb @@ -1,10 +1,12 @@ class BooksController < ApplicationController + # @ast node: Function "index" def index author = Author.find(params[:author_id]) books = author.books render json: books, status: :ok end + # @ast node: Function "create" def create author = Author.find(params[:author_id]) book = author.books.build(book_params) @@ -15,6 +17,7 @@ def create end end + # @ast node: Function "show" def show book = Book.find(params[:id]) render json: book, status: :ok @@ -22,6 +25,7 @@ def show private + # @ast node: Function "book_params" def book_params params.require(:book).permit(:title, :description, :published_at) end diff --git a/ast/src/testing/ruby/app/controllers/countries_controller.rb b/ast/src/testing/ruby/app/controllers/countries_controller.rb index 7906e8fa6..152c024b9 100644 --- a/ast/src/testing/ruby/app/controllers/countries_controller.rb +++ b/ast/src/testing/ruby/app/controllers/countries_controller.rb @@ -1,4 +1,5 @@ class CountriesController < ApplicationController + # @ast node: Function "process" def process country = Country.new(country_params) if country.save @@ -10,7 +11,8 @@ def process end private - + + # @ast node: Function "country_params" def country_params params.require(:country).permit(:name, :code) end diff --git a/ast/src/testing/ruby/app/controllers/dashboards_controller.rb b/ast/src/testing/ruby/app/controllers/dashboards_controller.rb index 922b4a3a2..deed05205 100644 --- a/ast/src/testing/ruby/app/controllers/dashboards_controller.rb +++ b/ast/src/testing/ruby/app/controllers/dashboards_controller.rb @@ -1,9 +1,11 @@ class DashboardsController < ApplicationController + # @ast node: Function "show" def show dashboard_data = fetch_dashboard_data render json: dashboard_data, status: :ok end + # @ast node: Function "update" def update updated_dashboard = update_dashboard_settings(params) render json: updated_dashboard, status: :ok @@ -11,10 +13,12 @@ def update private + # @ast node: Function "fetch_dashboard_data" def fetch_dashboard_data {} end + # @ast node: Function "update_dashboard_settings" def update_dashboard_settings(params) {} end diff --git a/ast/src/testing/ruby/app/controllers/home_controller.rb b/ast/src/testing/ruby/app/controllers/home_controller.rb index a8b041d04..3f7a62e68 100644 --- a/ast/src/testing/ruby/app/controllers/home_controller.rb +++ b/ast/src/testing/ruby/app/controllers/home_controller.rb @@ -1,4 +1,5 @@ class HomeController < ApplicationController + # @ast node: Function "index" def index render json: { message: 'Welcome to the homepage' }, status: :ok end diff --git a/ast/src/testing/ruby/app/controllers/people_controller.rb b/ast/src/testing/ruby/app/controllers/people_controller.rb index 25647e276..2a7e1f1a8 100644 --- a/ast/src/testing/ruby/app/controllers/people_controller.rb +++ b/ast/src/testing/ruby/app/controllers/people_controller.rb @@ -4,9 +4,10 @@ class PeopleController < ApplicationController # Retrieves a person by ID + # @ast node: Function "get_person" def get_person person = PersonService.get_person_by_id(params[:id]) - + if person render json: person, status: :ok else @@ -14,9 +15,10 @@ def get_person end end + # @ast node: Function "create_person" def create_person person = PersonService.new_person(person_params) - + if person.persisted? render json: person, status: :created else @@ -24,6 +26,7 @@ def create_person end end + # @ast node: Function "destroy" def destroy deleted_person = PersonService.delete(params[:id]) @@ -34,11 +37,13 @@ def destroy end end + # @ast node: Function "articles" def articles articles = Article.all render json: articles, status: :ok end + # @ast node: Function "create_article" def create_article person = Person.find(params[:id]) article = person.articles.build(article_params) @@ -50,12 +55,14 @@ def create_article end end + # @ast node: Function "show_person_profile" def show_person_profile @person = Person.find(params[:id]) end private + # @ast node: Function "person_params" def person_params params.require(:person).permit(:name, :email) end diff --git a/ast/src/testing/ruby/app/controllers/profiles_controller.rb b/ast/src/testing/ruby/app/controllers/profiles_controller.rb index dcc1ad53d..284b25bf5 100644 --- a/ast/src/testing/ruby/app/controllers/profiles_controller.rb +++ b/ast/src/testing/ruby/app/controllers/profiles_controller.rb @@ -1,14 +1,17 @@ class ProfilesController < ApplicationController + # @ast node: Function "show" def show profile = fetch_user_profile render json: profile, status: :ok end + # @ast node: Function "edit" def edit profile = fetch_user_profile render json: profile, status: :ok end + # @ast node: Function "update" def update updated_profile = update_user_profile(params) render json: updated_profile, status: :ok @@ -16,10 +19,12 @@ def update private + # @ast node: Function "fetch_user_profile" def fetch_user_profile {} end + # @ast node: Function "update_user_profile" def update_user_profile(params) {} end diff --git a/ast/src/testing/ruby/app/jobs/process_payment_job.rb b/ast/src/testing/ruby/app/jobs/process_payment_job.rb index a5bc15954..872f0663c 100644 --- a/ast/src/testing/ruby/app/jobs/process_payment_job.rb +++ b/ast/src/testing/ruby/app/jobs/process_payment_job.rb @@ -1,6 +1,7 @@ class ProcessPaymentJob < ApplicationJob queue_as :default + # @ast node: Function "perform" def perform(order_id, amount) # Payment processing logic here order = Order.find(order_id) diff --git a/ast/src/testing/ruby/app/jobs/send_notification_job.rb b/ast/src/testing/ruby/app/jobs/send_notification_job.rb index 1f0528aa9..0609bfb7a 100644 --- a/ast/src/testing/ruby/app/jobs/send_notification_job.rb +++ b/ast/src/testing/ruby/app/jobs/send_notification_job.rb @@ -1,6 +1,7 @@ class SendNotificationJob < ApplicationJob queue_as :urgent + # @ast node: Function "perform" def perform(user_id, message) user = User.find(user_id) NotificationService.send(user, message) diff --git a/ast/src/testing/ruby/app/mailers/user_mailer.rb b/ast/src/testing/ruby/app/mailers/user_mailer.rb index 20adda84a..f1b524bb2 100644 --- a/ast/src/testing/ruby/app/mailers/user_mailer.rb +++ b/ast/src/testing/ruby/app/mailers/user_mailer.rb @@ -1,12 +1,14 @@ class UserMailer < ApplicationMailer default from: 'notifications@example.com' + # @ast node: Function "welcome_email" def welcome_email(user) @user = user @url = 'http://example.com/login' mail(to: @user.email, subject: 'Welcome to My Awesome Site') end + # @ast node: Function "password_reset" def password_reset(user) @user = user @token = user.reset_token diff --git a/ast/src/testing/ruby/app/policies/person_policy.rb b/ast/src/testing/ruby/app/policies/person_policy.rb index 303775450..841e0845b 100644 --- a/ast/src/testing/ruby/app/policies/person_policy.rb +++ b/ast/src/testing/ruby/app/policies/person_policy.rb @@ -1,23 +1,28 @@ class PersonPolicy attr_reader :user, :person + # @ast node: Function "initialize" def initialize(user, person) @user = user @person = person end + # @ast node: Function "show?" def show? true end + # @ast node: Function "create?" def create? user.admin? end + # @ast node: Function "update?" def update? user.admin? || user == person end + # @ast node: Function "destroy?" def destroy? user.admin? end diff --git a/ast/src/testing/ruby/app/serializers/person_serializer.rb b/ast/src/testing/ruby/app/serializers/person_serializer.rb index f2557e22b..aafe038af 100644 --- a/ast/src/testing/ruby/app/serializers/person_serializer.rb +++ b/ast/src/testing/ruby/app/serializers/person_serializer.rb @@ -1,6 +1,7 @@ class PersonSerializer < ActiveModel::Serializer attributes :id, :name, :email, :age + # @ast node: Function "email" def email object.email.downcase end diff --git a/ast/src/testing/ruby/app/services/person_service.rb b/ast/src/testing/ruby/app/services/person_service.rb index c1660749e..5cbaace12 100644 --- a/ast/src/testing/ruby/app/services/person_service.rb +++ b/ast/src/testing/ruby/app/services/person_service.rb @@ -1,12 +1,15 @@ class PersonService + # @ast node: Function "get_person_by_id" def self.get_person_by_id(id) Person.find_by(id: id) end + # @ast node: Function "new_person" def self.new_person(person_params) Person.create(person_params) end + # @ast node: Function "delete" def self.delete(id) Person.destroy(id) end diff --git a/ast/src/testing/ruby/spec/support/auth_helpers.rb b/ast/src/testing/ruby/spec/support/auth_helpers.rb index 558ae4a7f..906a4307b 100644 --- a/ast/src/testing/ruby/spec/support/auth_helpers.rb +++ b/ast/src/testing/ruby/spec/support/auth_helpers.rb @@ -1,15 +1,18 @@ module AuthHelpers + # @ast node: Function "sign_in" def sign_in(user) @current_user = user { "Authorization" => "Bearer #{generate_token(user)}" } end - + + # @ast node: Function "auth_headers" def auth_headers(user) { "Authorization" => "Bearer #{generate_token(user)}" } end - + private - + + # @ast node: Function "generate_token" def generate_token(user) "token_#{user.id}_#{user.email}" end diff --git a/ast/src/testing/ruby/spec/support/json_helpers.rb b/ast/src/testing/ruby/spec/support/json_helpers.rb index 7cb80ba9f..d82246347 100644 --- a/ast/src/testing/ruby/spec/support/json_helpers.rb +++ b/ast/src/testing/ruby/spec/support/json_helpers.rb @@ -1,14 +1,17 @@ module JsonHelpers + # @ast node: Function "json_response" def json_response JSON.parse(response.body) rescue JSON::ParserError {} end - + + # @ast node: Function "json_data" def json_data json_response['data'] end - + + # @ast node: Function "json_errors" def json_errors json_response['errors'] end diff --git a/ast/src/testing/ruby/test/models/person_minitest_test.rb b/ast/src/testing/ruby/test/models/person_minitest_test.rb index 2057ae303..156aeaeb3 100644 --- a/ast/src/testing/ruby/test/models/person_minitest_test.rb +++ b/ast/src/testing/ruby/test/models/person_minitest_test.rb @@ -6,6 +6,7 @@ require 'test_helper' class PersonMinitestTest < Minitest::Test + # @ast node: Function "setup" def setup @person = Person.new(name: "Alice", email: "alice@example.com") end diff --git a/ast/src/testing/ruby/test/services/person_service_minitest_test.rb b/ast/src/testing/ruby/test/services/person_service_minitest_test.rb index 05c74be53..23b6a0581 100644 --- a/ast/src/testing/ruby/test/services/person_service_minitest_test.rb +++ b/ast/src/testing/ruby/test/services/person_service_minitest_test.rb @@ -10,6 +10,7 @@ require 'test_helper' class PersonServiceMinitestTest < Minitest::Test + # @ast node: Function "setup" def setup @person = Person.create!(name: "Test Person", email: "test@example.com") end diff --git a/cli/tests/cli/ruby.rs b/cli/tests/cli/ruby.rs index f91f89db6..62737fa88 100644 --- a/cli/tests/cli/ruby.rs +++ b/cli/tests/cli/ruby.rs @@ -19,20 +19,20 @@ fn person_service_rb_contains_exact_named_nodes() { let out = run_stakgraph(&[&file]); assert_eq!(out.exit_code, 0); - assert_eq!(out.stdout.contains("Class: PersonService (1-13)"), true); + assert_eq!(out.stdout.contains("Class: PersonService (1-16)"), true); assert_eq!( out.stdout - .contains("Function: PersonService.get_person_by_id (2-4)"), + .contains("Function: PersonService.get_person_by_id (3-5)"), true ); assert_eq!( out.stdout - .contains("Function: PersonService.new_person (6-8)"), + .contains("Function: PersonService.new_person (8-10)"), true ); assert_eq!( out.stdout - .contains("Function: PersonService.delete (10-12)"), + .contains("Function: PersonService.delete (13-15)"), true ); } @@ -46,8 +46,8 @@ fn parse_stats_ruby_dir() { assert_eq!(out.exit_code, 0, "stderr: {}", out.stderr); assert!(out.stdout.contains("Endpoint 23"), "stdout: {}", out.stdout); - assert!(out.stdout.contains("Class 35"), "stdout: {}", out.stdout); - assert!(out.stdout.contains("Function 64"), "stdout: {}", out.stdout); + assert!(out.stdout.contains("Class 32"), "stdout: {}", out.stdout); + assert!(out.stdout.contains("Function 61"), "stdout: {}", out.stdout); assert!(out.stdout.contains("UnitTest 21"), "stdout: {}", out.stdout); }