diff --git a/ast/src/lang/call_finder.rs b/ast/src/lang/call_finder.rs index 546f44680..0382834c7 100644 --- a/ast/src/lang/call_finder.rs +++ b/ast/src/lang/call_finder.rs @@ -279,7 +279,9 @@ fn find_nested_function_in_variable( for func in func_nodes.clone() { if let Some(nested_in) = func.meta.get("nested_in") { - if nested_in == var_name { + let n = crate::lang::parse::utils::trim_quotes(nested_in); + let v = crate::lang::parse::utils::trim_quotes(var_name); + if n == v { return Some(func); } } diff --git a/ast/src/lang/mod.rs b/ast/src/lang/mod.rs index cf7248e22..f45af38b9 100644 --- a/ast/src/lang/mod.rs +++ b/ast/src/lang/mod.rs @@ -7,6 +7,7 @@ pub mod linker; pub mod parse; pub mod queries; +use crate::lang::parse::utils::trim_quotes; pub use asg::NodeData; use asg::*; use consts::*; @@ -500,8 +501,7 @@ impl Lang { self.collect_functions(&qo, code, file, graph, lsp_tx, &identified_tests)?; self.attach_function_comments(code, &mut funcs1)?; - let mut func_nodes: Vec = funcs1.iter().map(|f| f.0.clone()).collect(); - let nested_pairs = self.find_nested_functions(&func_nodes); + let nested_pairs = self.find_nested_functions(&funcs1); let mut nested_edges_by_child: std::collections::HashMap> = std::collections::HashMap::new(); @@ -518,8 +518,7 @@ impl Lang { } let all_variables = graph.find_nodes_by_type(NodeType::Var); - let var_nested_pairs = - self.find_functions_nested_in_variables(&mut func_nodes, &all_variables); + let var_nested_pairs = self.find_functions_nested_in_variables(&mut funcs1, &all_variables); for (func, var) in var_nested_pairs { let edge = Edge::new( EdgeType::NestedIn, @@ -669,7 +668,7 @@ impl Lang { let mut attributes = Vec::new(); Self::loop_captures(&q_tests, tm, code, |body, node, o| { if o == FUNCTION_NAME { - caller_name = body; + caller_name = trim_quotes(&body).to_string(); } else if o == ATTRIBUTES { attributes.push(body); } else if o == FUNCTION_DEFINITION { @@ -686,7 +685,6 @@ impl Lang { lsp_tx, graph.get_allow_unverified_calls(), )?; - // Combine attributes and body for accurate test detection let full_body = if !attributes.is_empty() { format!("{} {}", attributes.join(" "), body) } else { diff --git a/ast/src/lang/parse/collect.rs b/ast/src/lang/parse/collect.rs index 7ec7810ba..6cd7742ee 100644 --- a/ast/src/lang/parse/collect.rs +++ b/ast/src/lang/parse/collect.rs @@ -430,7 +430,8 @@ impl Lang { if !targets.is_empty() { let target = targets - .iter().find(|node_data| node_data.file.contains(&resolved_path)); + .iter() + .find(|node_data| node_data.file.contains(&resolved_path)); let file_nodes = graph.find_nodes_by_file_ends_with(NodeType::File, file); @@ -540,7 +541,8 @@ impl Lang { let all_vars = graph.find_nodes_by_type(NodeType::Var); let imports = graph.find_nodes_by_file_ends_with(NodeType::Import, &func.file); - let import_body = imports.first() + let import_body = imports + .first() .map(|imp| imp.body.clone()) .unwrap_or_default(); @@ -637,12 +639,7 @@ impl Lang { .iter() .find(|v| target_file.ends_with(&v.file)) { - edges.push(Edge::contains( - NodeType::Function, - func, - NodeType::Var, - var, - )); + edges.push(Edge::contains(NodeType::Function, func, NodeType::Var, var)); processed.insert(key); } } @@ -676,16 +673,16 @@ impl Lang { } pub fn find_nested_functions<'a>( &self, - functions: &'a [NodeData], + functions: &'a [Function], ) -> Vec<(&'a NodeData, &'a NodeData)> { let mut nested = Vec::new(); for child in functions { for parent in functions { - if std::ptr::eq(child, parent) { + if std::ptr::eq(&child.0, &parent.0) { continue; } - if child.start > parent.start && child.end < parent.end { - nested.push((child, parent)); + if child.0.start > parent.0.start && child.0.end < parent.0.end { + nested.push((&child.0, &parent.0)); } } } @@ -694,7 +691,7 @@ impl Lang { pub fn find_functions_nested_in_variables<'a>( &self, - functions: &'a mut [NodeData], + functions: &'a mut [Function], variables: &'a [NodeData], ) -> Vec<(&'a NodeData, &'a NodeData)> { use std::collections::HashMap; @@ -703,18 +700,15 @@ impl Lang { let mut var_index: HashMap> = HashMap::new(); for var in variables { - var_index - .entry(var.file.clone()) - .or_default() - .push(var); + var_index.entry(var.file.clone()).or_default().push(var); } for func in functions.iter_mut() { - if let Some(vars_in_file) = var_index.get(&func.file) { + if let Some(vars_in_file) = var_index.get(&func.0.file) { for var in vars_in_file { - if func.start > var.start && func.end < var.end { - func.add_nested_in(&var.name); - nested.push((func as &NodeData, var as &NodeData)); + if func.0.start > var.start && func.0.end < var.end { + func.0.add_nested_in(&var.name); + nested.push((&func.0 as &NodeData, var as &NodeData)); break; } } diff --git a/ast/src/testing/coverage/nextjs.rs b/ast/src/testing/coverage/nextjs.rs index d7c23861a..906c494b8 100644 --- a/ast/src/testing/coverage/nextjs.rs +++ b/ast/src/testing/coverage/nextjs.rs @@ -74,7 +74,7 @@ async fn test_btreemap_graph_structure() -> Result<()> { assert_eq!(endpoints.len(), 21); let integration_tests = btree_graph.find_nodes_by_type(NodeType::IntegrationTest); - assert_eq!(integration_tests.len(), 18); + assert_eq!(integration_tests.len(), 19); let test_edges = btree_graph.find_nodes_with_edge_type( NodeType::IntegrationTest, @@ -213,8 +213,8 @@ async fn test_nextjs_graph_upload() -> Result<()> { let graph_ops = setup_nextjs_graph().await?; let (nodes, edges) = graph_ops.get_graph_size().await?; - assert_eq!(nodes, 527); - assert_eq!(edges, 874); + assert_eq!(nodes, 564); + assert_eq!(edges, 962); Ok(()) } @@ -230,7 +230,7 @@ async fn test_coverage_default_params() -> Result<()> { if let Some(integration) = &coverage.integration_tests { assert_eq!(integration.total, 21); - assert_eq!(integration.total_tests, 18); + assert_eq!(integration.total_tests, 19); } Ok(()) @@ -388,8 +388,8 @@ async fn test_nodes_function_type() -> Result<()> { ) .await?; - assert_eq!(count, 38); - assert_eq!(results.len(), 38); + assert_eq!(count, 49); + assert_eq!(results.len(), 49); for (node_type, _, _, _, _, _, _, _, _) in &results { assert_eq!(*node_type, NodeType::Function); @@ -446,8 +446,8 @@ async fn test_nodes_integration_test_type() -> Result<()> { ) .await?; - assert_eq!(count, 18); - assert_eq!(results.len(), 18); + assert_eq!(count, 19); + assert_eq!(results.len(), 19); Ok(()) } @@ -473,8 +473,8 @@ async fn test_nodes_unit_test_type() -> Result<()> { ) .await?; - assert_eq!(count, 25); - assert_eq!(results.len(), 25); + assert_eq!(count, 27); + assert_eq!(results.len(), 27); Ok(()) } @@ -527,7 +527,7 @@ async fn test_nodes_multi_type() -> Result<()> { ) .await?; - assert_eq!(count, 59); + assert_eq!(count, 70); let has_function = results .iter() @@ -567,8 +567,8 @@ async fn test_nodes_all_test_types() -> Result<()> { ) .await?; - assert_eq!(count, 48); - assert_eq!(results.len(), 48); + assert_eq!(count, 51); + assert_eq!(results.len(), 51); Ok(()) } diff --git a/ast/src/testing/coverage/react.rs b/ast/src/testing/coverage/react.rs index cec7b145b..ef2ec3e01 100644 --- a/ast/src/testing/coverage/react.rs +++ b/ast/src/testing/coverage/react.rs @@ -67,7 +67,7 @@ async fn test_btreemap_graph_structure() -> Result<()> { assert_eq!(endpoints.len(), 5); let functions = graph.find_nodes_by_type(NodeType::Function); - assert_eq!(functions.len(), 52); + assert_eq!(functions.len(), 56); let unit_tests = graph.find_nodes_by_type(NodeType::UnitTest); assert_eq!(unit_tests.len(), 3); @@ -82,7 +82,7 @@ async fn test_btreemap_graph_structure() -> Result<()> { assert_eq!(classes.len(), 4); let data_models = graph.find_nodes_by_type(NodeType::DataModel); - assert_eq!(data_models.len(), 22); + assert_eq!(data_models.len(), 25); let pages = graph.find_nodes_by_type(NodeType::Page); assert_eq!(pages.len(), 4); @@ -109,7 +109,7 @@ async fn test_btreemap_edges() -> Result<()> { assert_eq!(renders_edges, 4); let contains_edges = graph.count_edges_of_type(EdgeType::Contains); - assert_eq!(contains_edges, 208); + assert_eq!(contains_edges, 218); let handler_edges = graph.count_edges_of_type(EdgeType::Handler); assert_eq!(handler_edges, 5); @@ -134,8 +134,8 @@ async fn test_react_graph_upload() -> Result<()> { // Requests(14) + Pages(4) + Variables(7) + DataModels(22) + Endpoints(5) // UnitTests(3) + IntegrationTests(2) + E2eTests(2) + Files(~11 TSX + others) + Dirs(14) // Total approx 187+. We will adjust based on actual test run. - assert_eq!(nodes, 200); - assert_eq!(edges, 256); // Derived from previous test runs matches actual output + assert_eq!(nodes, 209); + assert_eq!(edges, 266); // Derived from previous test runs matches actual output Ok(()) } @@ -280,8 +280,8 @@ async fn test_nodes_function_type() -> Result<()> { // due to unique_functions_filters (component=true or operand=true). // We will adjust this assertion after the first run if needed. // We expect 52 functions in BTreeMap, but Neo4j query filters components/operands - assert_eq!(count, 23); - assert_eq!(results.len(), 23); + assert_eq!(count, 26); + assert_eq!(results.len(), 26); for (node_type, _, _, _, _, _, _, _, _) in &results { assert_eq!(*node_type, NodeType::Function); @@ -365,8 +365,8 @@ async fn test_nodes_data_model_type() -> Result<()> { ) .await?; - assert_eq!(count, 22); - assert_eq!(results.len(), 22); + assert_eq!(count, 25); + assert_eq!(results.len(), 25); Ok(()) } diff --git a/ast/src/testing/coverage/typescript.rs b/ast/src/testing/coverage/typescript.rs index e39f6afd6..6227a0246 100644 --- a/ast/src/testing/coverage/typescript.rs +++ b/ast/src/testing/coverage/typescript.rs @@ -67,7 +67,7 @@ async fn test_btreemap_graph_structure() -> Result<()> { assert_eq!(endpoints.len(), 22); let functions = graph.find_nodes_by_type(NodeType::Function); - assert_eq!(functions.len(), 33); + assert_eq!(functions.len(), 36); let unit_tests = graph.find_nodes_by_type(NodeType::UnitTest); assert_eq!(unit_tests.len(), 9); @@ -79,13 +79,13 @@ async fn test_btreemap_graph_structure() -> Result<()> { assert_eq!(e2e_tests.len(), 4); let classes = graph.find_nodes_by_type(NodeType::Class); - assert_eq!(classes.len(), 7); + assert_eq!(classes.len(), 9); let data_models = graph.find_nodes_by_type(NodeType::DataModel); - assert_eq!(data_models.len(), 17); + assert_eq!(data_models.len(), 27); let traits = graph.find_nodes_by_type(NodeType::Trait); - assert_eq!(traits.len(), 4); + assert_eq!(traits.len(), 6); Ok(()) } @@ -109,7 +109,7 @@ async fn test_btreemap_test_to_function_edges() -> Result<()> { assert_eq!(calls_edges, 10); let contains_edges = graph.count_edges_of_type(EdgeType::Contains); - assert_eq!(contains_edges, 162); + assert_eq!(contains_edges, 180); let handler_edges = graph.count_edges_of_type(EdgeType::Handler); assert_eq!(handler_edges, 22); @@ -126,8 +126,8 @@ async fn test_typescript_graph_upload() -> Result<()> { let graph_ops = setup_typescript_graph().await?; let (nodes, edges) = graph_ops.get_graph_size().await?; - assert_eq!(nodes, 172); - assert_eq!(edges, 213); + assert_eq!(nodes, 190); + assert_eq!(edges, 231); Ok(()) } @@ -303,8 +303,8 @@ async fn test_nodes_function_type() -> Result<()> { ) .await?; - assert_eq!(count, 12); - assert_eq!(results.len(), 12); + assert_eq!(count, 15); + assert_eq!(results.len(), 15); for (node_type, _, _, _, _, _, _, _, _) in &results { assert_eq!(*node_type, NodeType::Function); @@ -334,8 +334,8 @@ async fn test_nodes_class_type() -> Result<()> { ) .await?; - assert_eq!(count, 7); - assert_eq!(results.len(), 7); + assert_eq!(count, 9); + assert_eq!(results.len(), 9); Ok(()) } @@ -361,8 +361,8 @@ async fn test_nodes_data_model_type() -> Result<()> { ) .await?; - assert_eq!(count, 17); - assert_eq!(results.len(), 17); + assert_eq!(count, 27); + assert_eq!(results.len(), 27); Ok(()) } @@ -388,8 +388,8 @@ async fn test_nodes_trait_type() -> Result<()> { ) .await?; - assert_eq!(count, 4); - assert_eq!(results.len(), 4); + assert_eq!(count, 6); + assert_eq!(results.len(), 6); Ok(()) } @@ -496,7 +496,7 @@ async fn test_nodes_multi_type() -> Result<()> { ) .await?; - assert_eq!(count, 34); + assert_eq!(count, 37); let has_function = results .iter() @@ -932,7 +932,7 @@ async fn test_nodes_with_repo_filter() -> Result<()> { ) .await?; - assert_eq!(count, 12); + assert_eq!(count, 15); let (empty_count, _) = graph_ops .query_nodes_with_count( diff --git a/ast/src/testing/nextjs/app/test/unit.bounty-queries.test.ts b/ast/src/testing/nextjs/app/test/unit.bounty-queries.test.ts new file mode 100644 index 000000000..019773b44 --- /dev/null +++ b/ast/src/testing/nextjs/app/test/unit.bounty-queries.test.ts @@ -0,0 +1,135 @@ +// @ts-nocheck +import { + bountyKeys, + useGetBounty, + useGetBounties, +} from "../../lib/hooks/useBountyQueries"; + +describe("unit: bountyKeys query key factory", () => { + it("generates correct key for all bounties", () => { + const key = bountyKeys.all; + expect(key).toEqual(["bounties"]); + console.log("bountyKeys.all:", key); + }); + + it("generates correct key for lists", () => { + const key = bountyKeys.lists(); + expect(key).toEqual(["bounties", "list"]); + console.log("bountyKeys.lists():", key); + }); + + it("generates correct key for detail", () => { + const bountyId = "bounty-123"; + const key = bountyKeys.detail(bountyId); + expect(key).toEqual(["bounties", "detail", bountyId]); + console.log("bountyKeys.detail():", key); + }); + + it("generates correct key for details base", () => { + const key = bountyKeys.details(); + expect(key).toEqual(["bounties", "detail"]); + console.log("bountyKeys.details():", key); + }); + + it("generates correct key for workspace", () => { + const workspaceId = "workspace-456"; + const key = bountyKeys.workspace(workspaceId); + expect(key).toEqual(["bounties", "workspace", workspaceId]); + console.log("bountyKeys.workspace():", key); + }); + + it("generates correct key for assignee", () => { + const assigneePubkey = "assignee-pubkey-789"; + const key = bountyKeys.assignee(assigneePubkey); + expect(key).toEqual(["bounties", "assignee", assigneePubkey]); + console.log("bountyKeys.assignee():", key); + }); + + it("generates correct key for creator", () => { + const creatorPubkey = "creator-pubkey-abc"; + const key = bountyKeys.creator(creatorPubkey); + expect(key).toEqual(["bounties", "creator", creatorPubkey]); + console.log("bountyKeys.creator():", key); + }); + + it("generates correct key for list with filters", () => { + const filters = { status: "OPEN" }; + const pagination = { page: 1, limit: 10 }; + const sort = { field: "createdAt", order: "desc" as const }; + const key = bountyKeys.list(filters, pagination, sort); + expect(key).toEqual(["bounties", "list", { filters, pagination, sort }]); + console.log("bountyKeys.list() with params:", key); + }); +}); + +describe("unit: useGetBounty hook", () => { + it("fetches bounty by id", async () => { + const bountyId = "bounty-test-001"; + const query = useGetBounty(bountyId); + + expect(query.isLoading).toBeDefined(); + expect(query.data).toBeNull(); + console.log("useGetBounty initial state:", { + isLoading: query.isLoading, + data: query.data, + }); + }); + + it("does not fetch when disabled", () => { + const bountyId = "bounty-test-002"; + const query = useGetBounty(bountyId, false); + + expect(query.isLoading).toBe(false); + console.log("useGetBounty disabled:", query); + }); + + it("does not fetch when id is empty", () => { + const query = useGetBounty(""); + + expect(query.isLoading).toBe(false); + console.log("useGetBounty empty id:", query); + }); + + it("refetches when called", async () => { + const bountyId = "bounty-test-003"; + const query = useGetBounty(bountyId); + + await query.refetch(); + + expect(query.refetch).toBeDefined(); + console.log("useGetBounty refetch called"); + }); +}); + +describe("unit: useGetBounties hook", () => { + it("fetches bounties list", async () => { + const query = useGetBounties(); + + expect(query.isLoading).toBeDefined(); + console.log("useGetBounties initial state:", query); + }); + + it("fetches with filters", async () => { + const filters = { status: "OPEN" }; + const query = useGetBounties(filters); + + expect(query.data).toBeNull(); + console.log("useGetBounties with filters:", filters); + }); + + it("fetches with pagination", async () => { + const pagination = { page: 1, limit: 20 }; + const query = useGetBounties(undefined, pagination); + + expect(query.data).toBeNull(); + console.log("useGetBounties with pagination:", pagination); + }); + + it("fetches with sort", async () => { + const sort = { field: "amount", order: "desc" as const }; + const query = useGetBounties(undefined, undefined, sort); + + expect(query.data).toBeNull(); + console.log("useGetBounties with sort:", sort); + }); +}); diff --git a/ast/src/testing/nextjs/lib/generics.ts b/ast/src/testing/nextjs/lib/generics.ts new file mode 100644 index 000000000..b0135b461 --- /dev/null +++ b/ast/src/testing/nextjs/lib/generics.ts @@ -0,0 +1,64 @@ +// Server Action Generic Types +export type ActionResult = + | { success: true; data: T } + | { success: false; error: string }; + +export async function handleAction( + action: () => Promise +): Promise> { + try { + const data = await action(); + return { success: true, data }; + } catch (error) { + return { success: false, error: String(error) }; + } +} + +// API Response Types +export interface ApiResponse { + data: T; + metadata: { + total: number; + page: number; + pageSize: number; + }; +} + +export interface PaginatedResult { + items: T[]; + nextCursor: string | null; + hasMore: boolean; +} + +// Generic Service Class +export abstract class BaseService { + abstract findById(id: string): Promise; + abstract create(data: Omit): Promise; + abstract update(id: string, data: Partial): Promise; + abstract delete(id: string): Promise; + + async findOrCreate(id: string, defaults: Omit): Promise { + const existing = await this.findById(id); + if (existing) return existing; + return this.create(defaults); + } +} + +// Cache utilities +export class Cache { + private cache = new Map(); + + set(key: K, value: V, ttlMs: number): void { + this.cache.set(key, { value, expiry: Date.now() + ttlMs }); + } + + get(key: K): V | null { + const entry = this.cache.get(key); + if (!entry) return null; + if (Date.now() > entry.expiry) { + this.cache.delete(key); + return null; + } + return entry.value; + } +} diff --git a/ast/src/testing/nextjs/lib/hooks/useBountyQueries.ts b/ast/src/testing/nextjs/lib/hooks/useBountyQueries.ts new file mode 100644 index 000000000..ade32c9f8 --- /dev/null +++ b/ast/src/testing/nextjs/lib/hooks/useBountyQueries.ts @@ -0,0 +1,179 @@ +import { useState, useEffect } from "react"; + +interface Bounty { + id: string; + title: string; + description: string; + amount: number; + status: string; + workspaceId: string; + creatorPubkey: string; +} + +interface BountyFilters { + status?: string; +} + +interface PaginationParams { + page: number; + limit: number; +} + +interface BountySortParams { + field: string; + order: "asc" | "desc"; +} + +export const bountyKeys = { + all: ["bounties"] as const, + lists: () => [...bountyKeys.all, "list"] as const, + list: ( + filters?: BountyFilters, + pagination?: PaginationParams, + sort?: BountySortParams + ) => [...bountyKeys.lists(), { filters, pagination, sort }] as const, + details: () => [...bountyKeys.all, "detail"] as const, + detail: (id: string) => [...bountyKeys.details(), id] as const, + workspace: (workspaceId: string) => + [...bountyKeys.all, "workspace", workspaceId] as const, + assignee: (assigneePubkey: string) => + [...bountyKeys.all, "assignee", assigneePubkey] as const, + creator: (creatorPubkey: string) => + [...bountyKeys.all, "creator", creatorPubkey] as const, +}; + +interface QueryResult { + data: T | null; + isLoading: boolean; + isSuccess: boolean; + isError: boolean; + error: Error | null; + refetch: () => Promise; +} + +export function useGetBounty(id: string, enabled = true): QueryResult { + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + const [isError, setIsError] = useState(false); + const [error, setError] = useState(null); + + const queryKey = bountyKeys.detail(id); + + const fetchBounty = async () => { + if (!enabled || !id) return; + + setIsLoading(true); + setIsError(false); + setError(null); + + try { + const response = await fetch(`/api/bounties/${id}`); + if (!response.ok) { + throw new Error(`Failed to fetch bounty: ${response.status}`); + } + const bountyData = await response.json(); + setData(bountyData); + setIsSuccess(true); + console.log("fetchBounty:", bountyData, "queryKey:", queryKey); + } catch (err) { + const errorObj = err instanceof Error ? err : new Error("Unknown error"); + setError(errorObj); + setIsError(true); + console.error("fetchBounty error:", errorObj.message); + } finally { + setIsLoading(false); + } + }; + + const refetch = async () => { + console.log("refetch: re-fetching bounty data"); + await fetchBounty(); + }; + + useEffect(() => { + if (enabled && id) { + fetchBounty(); + } + }, [id, enabled]); + + return { + data, + isLoading, + isSuccess, + isError, + error, + refetch, + }; +} + +export function useGetBounties( + filters?: BountyFilters, + pagination?: PaginationParams, + sort?: BountySortParams +): QueryResult { + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + const [isError, setIsError] = useState(false); + const [error, setError] = useState(null); + + const queryKey = bountyKeys.list(filters, pagination, sort); + + const fetchBounties = async () => { + setIsLoading(true); + setIsError(false); + setError(null); + + try { + const params = new URLSearchParams(); + if (filters?.status) params.append("status", filters.status); + if (pagination) { + params.append("page", String(pagination.page)); + params.append("limit", String(pagination.limit)); + } + if (sort) { + params.append("sortField", sort.field); + params.append("sortOrder", sort.order); + } + + const response = await fetch(`/api/bounties?${params.toString()}`); + if (!response.ok) { + throw new Error(`Failed to fetch bounties: ${response.status}`); + } + const bountiesData = await response.json(); + setData(bountiesData); + setIsSuccess(true); + console.log("fetchBounties:", bountiesData, "queryKey:", queryKey); + } catch (err) { + const errorObj = err instanceof Error ? err : new Error("Unknown error"); + setError(errorObj); + setIsError(true); + console.error("fetchBounties error:", errorObj.message); + } finally { + setIsLoading(false); + } + }; + + const refetch = async () => { + console.log("refetch: re-fetching bounties data"); + await fetchBounties(); + }; + + useEffect(() => { + fetchBounties(); + }, [ + JSON.stringify(filters), + JSON.stringify(pagination), + JSON.stringify(sort), + ]); + + return { + data, + isLoading, + isSuccess, + isError, + error, + refetch, + }; +} diff --git a/ast/src/testing/nextjs/mod.rs b/ast/src/testing/nextjs/mod.rs index 581ca26e0..8ed958ce2 100644 --- a/ast/src/testing/nextjs/mod.rs +++ b/ast/src/testing/nextjs/mod.rs @@ -39,7 +39,7 @@ pub async fn test_nextjs_generic() -> Result<()> { let file_nodes = graph.find_nodes_by_type(NodeType::File); nodes += file_nodes.len(); - assert_eq!(file_nodes.len(), 80, "Expected 80 File nodes"); + assert_eq!(file_nodes.len(), 83, "Expected 83 File nodes"); let card_file = file_nodes .iter() @@ -98,13 +98,65 @@ pub async fn test_nextjs_generic() -> Result<()> { assert!(graph.has_edge(&app_dir, &items_dir, EdgeType::Contains)); assert!(graph.has_edge(&items_dir, &items_page, EdgeType::Contains)); + let bounty_file = file_nodes + .iter() + .find(|f| { + f.name == "useBountyQueries.ts" && f.file.ends_with("lib/hooks/useBountyQueries.ts") + }) + .map(|n| Node::new(NodeType::File, n.clone())) + .expect("useBountyQueries.ts file not found"); + + let bounty_keys = graph.find_nodes_by_type(NodeType::Var); + let bounty_keys_var = bounty_keys + .iter() + .find(|v| v.name == "bountyKeys") + .map(|n| Node::new(NodeType::Var, n.clone())) + .expect("bountyKeys variable not found"); + + assert!( + graph.has_edge(&bounty_file, &bounty_keys_var, EdgeType::Contains), + "Expected useBountyQueries.ts to contain bountyKeys" + ); + + let functions = graph.find_nodes_by_type(NodeType::Function); + + let detail_func = functions + .iter() + .find(|f| f.name == "detail" && f.file.ends_with("lib/hooks/useBountyQueries.ts")); + + if let Some(func) = detail_func { + let func_node = Node::new(NodeType::Function, func.clone()); + + assert!( + graph.has_edge(&bounty_file, &func_node, EdgeType::Contains), + "Expected useBountyQueries.ts to contain detail function" + ); + + let tests = graph.find_nodes_by_type(NodeType::UnitTest); + let detail_test = tests + .iter() + .find(|t| { + t.name == "unit: bountyKeys query key factory" + && t.file.ends_with("unit.bounty-queries.test.ts") + }) + .map(|n| Node::new(NodeType::UnitTest, n.clone())) + .expect("bountyKeys Unit Test not found"); + + assert!( + graph.has_edge(&detail_test, &func_node, EdgeType::Calls), + "Test should call detail function" + ); + } else { + panic!("FAILURE: detail function NOT FOUND in graph nodes"); + } + let endpoints = graph.find_nodes_by_type(NodeType::Endpoint); nodes += endpoints.len(); assert_eq!(endpoints.len(), 21, "Expected 21 Endpoint nodes"); let requests = graph.find_nodes_by_type(NodeType::Request); nodes += requests.len(); - assert_eq!(requests.len(), 35, "Expected 35 Request nodes"); + assert_eq!(requests.len(), 37, "Expected 37 Request nodes"); let functions = graph.find_nodes_by_type(NodeType::Function); nodes += functions.len(); @@ -113,8 +165,9 @@ pub async fn test_nextjs_generic() -> Result<()> { } else { assert_eq!( functions.len(), - 180, - "Expected 180 Function nodes without LSP" + 197, + "Expected 197 Function nodes without LSP, found {}", + functions.len() ); } @@ -225,7 +278,7 @@ pub async fn test_nextjs_generic() -> Result<()> { let variables = graph.find_nodes_by_type(NodeType::Var); nodes += variables.len(); - assert_eq!(variables.len(), 16, "Expected 16 Variable nodes"); + assert_eq!(variables.len(), 17, "Expected 17 Variable nodes"); let libraries = graph.find_nodes_by_type(NodeType::Library); nodes += libraries.len(); @@ -234,12 +287,23 @@ pub async fn test_nextjs_generic() -> Result<()> { let calls = graph.count_edges_of_type(EdgeType::Calls); edges += calls; - //TODO: Fix lsp calls edge count : locally, it says 74 but on CI it says something else - // assert_eq!(calls, 74, "Expected 74 Calls edges"); + //TODO: LSP and non-lsp + + if use_lsp { + assert_eq!(calls, 303, "Expected 303 Calls edges"); + } else { + #[cfg(not(feature = "neo4j"))] + assert_eq!(calls, 233, "Expected 233 Calls edges"); + } let contains = graph.count_edges_of_type(EdgeType::Contains); edges += contains; - assert_eq!(contains, 511, "Expected 511 Contains edges"); + + if use_lsp { + assert_eq!(contains, 567, "Expected 567 Contains edges"); + } else { + assert_eq!(contains, 566, "Expected 566 Contains edges"); + } let handlers = graph.count_edges_of_type(EdgeType::Handler); edges += handlers; @@ -247,7 +311,12 @@ pub async fn test_nextjs_generic() -> Result<()> { let tests = graph.find_nodes_by_type(NodeType::UnitTest); nodes += tests.len(); - assert_eq!(tests.len(), 25, "Expected 25 UnitTest nodes"); + assert_eq!( + tests.len(), + 27, + "Expected 27 UnitTest nodes, found {}", + tests.len() + ); #[cfg(not(feature = "neo4j"))] if let Some(_currency_test) = tests.iter().find(|t| { @@ -311,7 +380,7 @@ pub async fn test_nextjs_generic() -> Result<()> { let classes = graph.find_nodes_by_type(NodeType::Class); nodes += classes.len(); - assert_eq!(classes.len(), 8, "Expected 8 Class nodes"); + assert_eq!(classes.len(), 9, "Expected 9 Class nodes"); let calculator_class = classes .iter() @@ -343,7 +412,7 @@ pub async fn test_nextjs_generic() -> Result<()> { nodes += integration_test.len(); assert_eq!( integration_test.len(), - 18, + 19, "Expected 18 IntegrationTest nodes" ); @@ -422,11 +491,11 @@ pub async fn test_nextjs_generic() -> Result<()> { let import_nodes = graph.find_nodes_by_type(NodeType::Import); nodes += import_nodes.len(); - assert_eq!(import_nodes.len(), 46, "Expected 46 Import nodes"); + assert_eq!(import_nodes.len(), 48, "Expected 48 Import nodes"); let datamodels = graph.find_nodes_by_type(NodeType::DataModel); nodes += datamodels.len(); - assert_eq!(datamodels.len(), 23, "Expected 23 DataModel nodes"); + assert_eq!(datamodels.len(), 31, "Expected 31 DataModel nodes"); let uses = graph.count_edges_of_type(EdgeType::Uses); edges += uses; @@ -438,11 +507,11 @@ pub async fn test_nextjs_generic() -> Result<()> { let nested_in = graph.count_edges_of_type(EdgeType::NestedIn); edges += nested_in; - assert_eq!(nested_in, 82, "Expected 82 NestedIn edges"); + assert_eq!(nested_in, 93, "Expected 93 NestedIn edges"); let operand = graph.count_edges_of_type(EdgeType::Operand); edges += operand; - assert_eq!(operand, 31, "Expected 31 Operand edges"); + assert_eq!(operand, 33, "Expected 33 Operand edges"); let renders = graph.count_edges_of_type(EdgeType::Renders); edges += renders; diff --git a/ast/src/testing/react/mod.rs b/ast/src/testing/react/mod.rs index 511618e40..2f7d98cb5 100644 --- a/ast/src/testing/react/mod.rs +++ b/ast/src/testing/react/mod.rs @@ -101,7 +101,7 @@ import NewPerson from "./components/NewPerson";"# ); assert_eq!(import_test_file.body, app_body, "Body of App is incorrect"); - assert_eq!(imports.len(), 18, "Expected 18 imports"); + assert_eq!(imports.len(), 19, "Expected 19 imports"); let people_import = imports .iter() @@ -139,7 +139,7 @@ import NewPerson from "./components/NewPerson";"# if use_lsp == true { assert_eq!(functions.len(), 22, "Expected 21 functions/components"); } else { - assert_eq!(functions.len(), 52, "Expected 52 functions/components"); + assert_eq!(functions.len(), 56, "Expected 56 functions/components"); } let classes = graph.find_nodes_by_type(NodeType::Class); @@ -434,7 +434,7 @@ import NewPerson from "./components/NewPerson";"# let data_models = graph.find_nodes_by_type(NodeType::DataModel); nodes_count += data_models.len(); - assert_eq!(data_models.len(), 22, "Expected 22 data models"); + assert_eq!(data_models.len(), 25, "Expected 25 data models"); let person_data_model = data_models .iter() @@ -485,8 +485,8 @@ import NewPerson from "./components/NewPerson";"# let contains_edges_count = graph.count_edges_of_type(EdgeType::Contains); edges_count += contains_edges_count; assert_eq!( - contains_edges_count, 208, - "Expected 208 contains edges, got {}", + contains_edges_count, 218, + "Expected 218 contains edges, got {}", contains_edges_count ); @@ -523,7 +523,7 @@ import NewPerson from "./components/NewPerson";"# .iter() .filter(|f| f.name.ends_with(".tsx")) .count(); - assert_eq!(tsx_files, 11, "Expected 11 TSX files, got {}", tsx_files); + assert_eq!(tsx_files, 12, "Expected 12 TSX files, got {}", tsx_files); let component_pattern_functions = functions .iter() diff --git a/ast/src/testing/react/src/generics.tsx b/ast/src/testing/react/src/generics.tsx new file mode 100644 index 000000000..014cc5df7 --- /dev/null +++ b/ast/src/testing/react/src/generics.tsx @@ -0,0 +1,68 @@ +import React, { useState, useCallback } from "react"; + +// Generic React Hook +export function useGenericState(initialValue: T): [T, (value: T) => void] { + const [state, setState] = useState(initialValue); + return [state, setState]; +} + +// Generic Component Props +interface ListProps { + items: T[]; + renderItem: (item: T, index: number) => React.ReactNode; + keyExtractor: (item: T) => string; +} + +// Generic Component +export function GenericList({ + items, + renderItem, + keyExtractor, +}: ListProps) { + return ( +
    + {items.map((item, index) => ( +
  • {renderItem(item, index)}
  • + ))} +
+ ); +} + +// Generic Higher-Order Component +export function withLoading( + WrappedComponent: React.ComponentType +): React.FC { + return function WithLoadingComponent({ isLoading, ...props }) { + if (isLoading) return
Loading...
; + return ; + }; +} + +// Generic data fetching hook +export function useFetch(url: string): { + data: T | null; + loading: boolean; + error: Error | null; +} { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useCallback(async () => { + try { + const response = await fetch(url); + const result = await response.json(); + setData(result); + } catch (e) { + setError(e as Error); + } finally { + setLoading(false); + } + }, [url]); + + return { data, loading, error }; +} + +// Generic utility types for React +export type PropsWithChildren

= P & { children?: React.ReactNode }; +export type Dispatch = (action: A) => void; diff --git a/ast/src/testing/ruby/mod.rs b/ast/src/testing/ruby/mod.rs index e144cd752..f2ea2c906 100644 --- a/ast/src/testing/ruby/mod.rs +++ b/ast/src/testing/ruby/mod.rs @@ -812,7 +812,7 @@ pub async fn test_ruby_generic() -> Result<()> { let articles_api_test = integration_tests .iter() - .find(|t| t.name.contains("Articles API")) + .find(|t| t.name.contains("Articles API") && t.file.contains("integration")) .expect("Articles API integration test not found"); assert!( articles_api_test diff --git a/ast/src/testing/typescript/mod.rs b/ast/src/testing/typescript/mod.rs index e912a8522..a28cbcb41 100644 --- a/ast/src/testing/typescript/mod.rs +++ b/ast/src/testing/typescript/mod.rs @@ -47,7 +47,7 @@ pub async fn test_typescript_generic() -> Result<()> { let files = graph.find_nodes_by_type(NodeType::File); nodes_count += files.len(); - assert_eq!(files.len(), 26, "Expected 26 File nodes"); + assert_eq!(files.len(), 27, "Expected 27 File nodes"); let pkg_files = files .iter() @@ -82,7 +82,7 @@ import {{ sequelize }} from "./config.js";"# if use_lsp { assert_eq!(functions.len(), 38, "Expected 38 functions with LSP"); } else { - assert_eq!(functions.len(), 33, "Expected 33 functions without LSP"); + assert_eq!(functions.len(), 36, "Expected 36 functions without LSP"); } let log_fn = functions @@ -105,7 +105,7 @@ import {{ sequelize }} from "./config.js";"# let classes = graph.find_nodes_by_type(NodeType::Class); nodes_count += classes.len(); - assert_eq!(classes.len(), 7, "Expected 7 classes"); + assert_eq!(classes.len(), 9, "Expected 9 classes"); let directories = graph.find_nodes_by_type(NodeType::Directory); nodes_count += directories.len(); @@ -267,11 +267,11 @@ import {{ sequelize }} from "./config.js";"# let data_models = graph.find_nodes_by_type(NodeType::DataModel); nodes_count += data_models.len(); - assert_eq!(data_models.len(), 17, "Expected 17 data models"); + assert_eq!(data_models.len(), 27, "Expected 27 data models"); let trait_nodes = graph.find_nodes_by_type(NodeType::Trait); nodes_count += trait_nodes.len(); - assert_eq!(trait_nodes.len(), 4, "Expected 4 trait nodes"); + assert_eq!(trait_nodes.len(), 6, "Expected 6 trait nodes"); let person_service_trait = trait_nodes .iter() @@ -287,7 +287,7 @@ import {{ sequelize }} from "./config.js";"# let contains = graph.count_edges_of_type(EdgeType::Contains); edges_count += contains; - assert_eq!(contains, 162, "Expected 162 contains edges"); + assert_eq!(contains, 180, "Expected 180 contains edges"); let import_edges_count = graph.count_edges_of_type(EdgeType::Imports); edges_count += import_edges_count; diff --git a/ast/src/testing/typescript/src/generics.ts b/ast/src/testing/typescript/src/generics.ts new file mode 100644 index 000000000..b6c4af2e6 --- /dev/null +++ b/ast/src/testing/typescript/src/generics.ts @@ -0,0 +1,79 @@ +// 1. Generic Functions +export function identity(arg: T): T { + return arg; +} + +export const combine = (a: A, b: B): [A, B] => [a, b]; + +export async function fetchData(url: string): Promise { + const response = await fetch(url); + return response.json(); +} + +// 2. Generic Interfaces +export interface Repository { + findById(id: string): Promise; + findAll(): Promise; + save(entity: T): Promise; + delete(id: string): Promise; +} + +export interface Mapper { + map(input: TInput): TOutput; +} + +// 3. Generic Classes +export class GenericRepository implements Repository { + private items: Map = new Map(); + + async findById(id: string): Promise { + return this.items.get(id) ?? null; + } + + async findAll(): Promise { + return Array.from(this.items.values()); + } + + async save(entity: T): Promise { + return entity; + } + + async delete(id: string): Promise { + return this.items.delete(id); + } +} + +// 4. Generic Type Aliases +export type Nullable = T | null; +export type AsyncResult = Promise<{ data: T; error: Error | null }>; +export type Pair = [A, B]; + +// 5. Generic with Constraints +export function getProperty(obj: T, key: K): T[K] { + return obj[key]; +} + +export class KeyValueStore { + private store: Map = new Map(); + + set(key: K, value: V): void { + this.store.set(key, value); + } + + get(key: K): V | undefined { + return this.store.get(key); + } +} + +// 6. Utility Types Usage +export type UserDTO = { + id: string; + name: string; + email: string; + password: string; +}; + +export type SafeUser = Omit; +export type UserUpdate = Partial; +export type UserKeys = keyof UserDTO; +export type ReadonlyUser = Readonly;