Skip to main content

code_moniker_core/lang/c/
mod.rs

1use std::collections::BTreeSet;
2use std::sync::Arc;
3
4use tree_sitter::{Language, Parser, Tree};
5
6use crate::core::code_graph::CodeGraph;
7use crate::core::moniker::Moniker;
8use crate::core::shape::Shape;
9
10use crate::lang::{ExtractionContext, KindSpec, LangExtractor, ParsedDocument};
11
12mod kinds;
13mod sdk_pipeline;
14
15#[derive(Clone, Debug, Default)]
16pub struct Presets {
17	/// Project-relative compiler include roots, in search order.
18	pub include_paths: Vec<String>,
19	/// Project-relative paths known to exist in the indexed source root.
20	pub workspace_files: Arc<BTreeSet<String>>,
21	/// Package owning unresolved quoted headers declared by the build system.
22	pub external_include_package: Option<String>,
23}
24
25pub fn parse(source: &str) -> Tree {
26	let mut parser = Parser::new();
27	let language: Language = tree_sitter_c::LANGUAGE.into();
28	parser.set_language(&language).unwrap_or_else(|err| {
29		panic!("failed to load tree-sitter C grammar: {err}");
30	});
31	parser.parse(source, None).unwrap_or_else(|| {
32		panic!("tree-sitter parse returned None on a non-cancelled call");
33	})
34}
35
36pub fn extract(
37	uri: &str,
38	source: &str,
39	anchor: &Moniker,
40	deep: bool,
41	presets: &Presets,
42) -> CodeGraph {
43	<Lang as LangExtractor>::extract(uri, source, anchor, deep, presets)
44}
45
46pub struct Lang;
47
48const DEF_KINDS: &[&str] = &[
49	"struct",
50	"enum",
51	"type",
52	"func",
53	"macro",
54	"field",
55	"enum_constant",
56	"var",
57	"const",
58];
59
60const DEF_KIND_SPECS: &[KindSpec] = &[
61	KindSpec::new("struct", Shape::Type, 20, "struct"),
62	KindSpec::new("enum", Shape::Type, 21, "enum"),
63	KindSpec::new("type", Shape::Type, 22, "typedef"),
64	KindSpec::new("enum_constant", Shape::Value, 30, "enum constant"),
65	KindSpec::new("field", Shape::Value, 31, "field"),
66	KindSpec::new("func", Shape::Callable, 40, "function"),
67	KindSpec::new("macro", Shape::Callable, 41, "macro"),
68	KindSpec::new("const", Shape::Value, 60, "define"),
69	KindSpec::new("var", Shape::Value, 61, "variable"),
70];
71
72impl crate::lang::LangExtractor for Lang {
73	type Presets = Presets;
74	const LANG_TAG: &'static str = "c";
75	const ALLOWED_KINDS: &'static [&'static str] = DEF_KINDS;
76	const KIND_SPECS: &'static [KindSpec] = DEF_KIND_SPECS;
77	const ALLOWED_VISIBILITIES: &'static [&'static str] = &["public", "module"];
78
79	fn parse(_uri: &str, source: &str) -> ParsedDocument {
80		ParsedDocument::new(parse(source))
81	}
82
83	fn file_root(uri: &str, anchor: &Moniker) -> Option<Moniker> {
84		Some(sdk_pipeline::compute_module_moniker(anchor, uri))
85	}
86
87	fn extract_parsed(
88		context: ExtractionContext<'_, Self::Presets>,
89		document: &ParsedDocument,
90	) -> CodeGraph {
91		sdk_pipeline::extract(
92			context.uri,
93			context.source,
94			document,
95			context.anchor,
96			context.deep,
97			context.presets,
98		)
99	}
100}
101
102#[cfg(test)]
103mod tests {
104	use super::*;
105	use crate::core::moniker::MonikerBuilder;
106	use crate::lang::assert_conformance;
107
108	fn make_anchor() -> Moniker {
109		MonikerBuilder::new().project(b"app").build()
110	}
111
112	fn extract_default(uri: &str, source: &str, anchor: &Moniker, deep: bool) -> CodeGraph {
113		let g = extract(uri, source, anchor, deep, &Presets::default());
114		assert_conformance::<super::Lang>(&g, anchor);
115		g
116	}
117
118	#[test]
119	fn parse_empty_returns_translation_unit() {
120		let tree = parse("");
121		assert_eq!(tree.root_node().kind(), "translation_unit");
122	}
123
124	#[test]
125	fn extract_module_strips_c_extension_and_keeps_h() {
126		let g = extract_default("src/util/text.c", "int x;\n", &make_anchor(), false);
127		let expected = MonikerBuilder::new()
128			.project(b"app")
129			.segment(b"lang", b"c")
130			.segment(b"dir", b"src")
131			.segment(b"dir", b"util")
132			.segment(b"module", b"text")
133			.build();
134		assert_eq!(g.root(), &expected);
135
136		let h = extract_default("src/util/text.h", "int y;\n", &make_anchor(), false);
137		let expected_h = MonikerBuilder::new()
138			.project(b"app")
139			.segment(b"lang", b"c")
140			.segment(b"dir", b"src")
141			.segment(b"dir", b"util")
142			.segment(b"module", b"text.h")
143			.build();
144		assert_eq!(h.root(), &expected_h);
145	}
146
147	#[test]
148	fn extract_function_definition_with_pointer_params() {
149		let src = "int run(char *name, int n) { return n; }\n";
150		let g = extract_default("main.c", src, &make_anchor(), false);
151		let run = MonikerBuilder::new()
152			.project(b"app")
153			.segment(b"lang", b"c")
154			.segment(b"module", b"main")
155			.segment(b"func", b"run(name:char*,n:int)")
156			.build();
157		assert!(
158			g.contains(&run),
159			"function def expected; defs: {:?}",
160			g.def_monikers()
161		);
162	}
163
164	#[test]
165	fn extract_static_function_is_module_visible() {
166		let src = "static void helper(void) {}\n";
167		let g = extract_default("main.c", src, &make_anchor(), false);
168		let def = g.defs().find(|d| d.kind == b"func").expect("func def");
169		assert_eq!(def.visibility, b"module".to_vec());
170		let name = def
171			.moniker
172			.as_view()
173			.segments()
174			.last()
175			.unwrap()
176			.name
177			.to_vec();
178		assert_eq!(name, b"helper()".to_vec(), "(void) collapses to zero slots");
179	}
180
181	#[test]
182	fn extract_struct_with_fields_and_typedef() {
183		let src = "typedef struct obj { int refcount; void *ptr; } obj;\n";
184		let g = extract_default("obj.c", src, &make_anchor(), false);
185		let strukt = MonikerBuilder::new()
186			.project(b"app")
187			.segment(b"lang", b"c")
188			.segment(b"module", b"obj")
189			.segment(b"struct", b"obj")
190			.build();
191		let field = MonikerBuilder::new()
192			.project(b"app")
193			.segment(b"lang", b"c")
194			.segment(b"module", b"obj")
195			.segment(b"struct", b"obj")
196			.segment(b"field", b"refcount")
197			.build();
198		assert!(g.contains(&strukt));
199		assert!(g.contains(&field));
200	}
201
202	#[test]
203	fn extract_anonymous_typedef_struct_owns_fields() {
204		let src = "typedef struct { int len; char buf[8]; } sds;\n";
205		let g = extract_default("sds.c", src, &make_anchor(), false);
206		let ty = MonikerBuilder::new()
207			.project(b"app")
208			.segment(b"lang", b"c")
209			.segment(b"module", b"sds")
210			.segment(b"type", b"sds")
211			.build();
212		let field = MonikerBuilder::new()
213			.project(b"app")
214			.segment(b"lang", b"c")
215			.segment(b"module", b"sds")
216			.segment(b"type", b"sds")
217			.segment(b"field", b"len")
218			.build();
219		assert!(g.contains(&ty));
220		assert!(g.contains(&field));
221	}
222
223	#[test]
224	fn extract_enum_constants_are_defs() {
225		let src = "enum color { RED, GREEN = 2 };\n";
226		let g = extract_default("color.c", src, &make_anchor(), false);
227		let red = MonikerBuilder::new()
228			.project(b"app")
229			.segment(b"lang", b"c")
230			.segment(b"module", b"color")
231			.segment(b"enum", b"color")
232			.segment(b"enum_constant", b"RED")
233			.build();
234		assert!(g.contains(&red));
235	}
236
237	#[test]
238	fn extract_macros_as_defs() {
239		let src = "#define MAX_LEN 128\n#define MIN(a, b) ((a) < (b) ? (a) : (b))\n";
240		let g = extract_default("m.c", src, &make_anchor(), false);
241		let object = MonikerBuilder::new()
242			.project(b"app")
243			.segment(b"lang", b"c")
244			.segment(b"module", b"m")
245			.segment(b"const", b"MAX_LEN")
246			.build();
247		let function_like = MonikerBuilder::new()
248			.project(b"app")
249			.segment(b"lang", b"c")
250			.segment(b"module", b"m")
251			.segment(b"macro", b"MIN(a,b)")
252			.build();
253		assert!(g.contains(&object));
254		assert!(g.contains(&function_like));
255	}
256
257	#[test]
258	fn extract_header_guard_is_not_a_definition() {
259		let src = "#ifndef UTIL_H\n#define UTIL_H\n#define MAX_LEN 128\n#endif\n";
260		let g = extract_default("util.h", src, &make_anchor(), false);
261		let names = g
262			.defs()
263			.filter_map(|definition| {
264				definition
265					.moniker
266					.as_view()
267					.segments()
268					.last()
269					.map(|segment| segment.name.to_vec())
270			})
271			.collect::<Vec<_>>();
272		assert!(!names.iter().any(|name| name == b"UTIL_H"));
273		assert!(names.iter().any(|name| name == b"MAX_LEN"));
274	}
275
276	#[test]
277	fn extract_pointer_fields_and_vars_keep_pointer_signature() {
278		let src = "struct item { char *name; }; extern struct item *current;";
279		let g = extract_default("types.c", src, &make_anchor(), false);
280		let field = g
281			.defs()
282			.find(|definition| definition.kind == b"field")
283			.expect("field definition");
284		let variable = g
285			.defs()
286			.find(|definition| definition.kind == b"var")
287			.expect("variable definition");
288		assert_eq!(field.signature, b"char*");
289		assert_eq!(variable.signature, b"structitem*");
290	}
291
292	#[test]
293	fn extract_field_before_trailing_attribute_macro() {
294		let source = "typedef struct Table { int *value field_attr(ignore); int count field_attr(ignore); } Table;\n";
295		let graph = extract_default("table.h", source, &make_anchor(), false);
296
297		assert!(graph.defs().any(|definition| {
298			definition.kind == b"field"
299				&& definition
300					.moniker
301					.as_view()
302					.segments()
303					.last()
304					.is_some_and(|segment| segment.name == b"value")
305		}));
306		assert!(graph.defs().any(|definition| {
307			definition.kind == b"field"
308				&& definition
309					.moniker
310					.as_view()
311					.segments()
312					.last()
313					.is_some_and(|segment| segment.name == b"count")
314		}));
315	}
316
317	#[test]
318	fn extract_system_include_is_external_and_local_include_internal() {
319		let src = "#include <stdio.h>\n#include <project/api.h>\n#include \"vendor/util.h\"\n";
320		let presets = Presets {
321			include_paths: vec![String::new()],
322			workspace_files: Arc::new(BTreeSet::from([
323				"main.c".to_string(),
324				"project/api.h".to_string(),
325				"vendor/util.h".to_string(),
326			])),
327			..Presets::default()
328		};
329		let g = extract("main.c", src, &make_anchor(), false, &presets);
330		assert_conformance::<super::Lang>(&g, &make_anchor());
331		let external = g
332			.refs()
333			.find(|r| r.kind == b"imports_module" && r.confidence == b"external".to_vec())
334			.expect("system include ref");
335		assert!(
336			external
337				.target
338				.as_view()
339				.segments()
340				.any(|s| s.kind == b"sdk" && s.name == b"c"),
341		);
342		assert!(
343			external
344				.target
345				.as_view()
346				.segments()
347				.any(|s| s.kind == b"path" && s.name == b"stdio")
348		);
349		let internal = g
350			.refs()
351			.find(|r| r.kind == b"imports_module" && r.confidence == b"imported".to_vec())
352			.expect("local include ref");
353		assert!(
354			internal
355				.target
356				.as_view()
357				.segments()
358				.any(|s| s.kind == b"module" && s.name == b"api.h"),
359		);
360		let quoted = g
361			.refs()
362			.find(|r| {
363				r.kind == b"imports_module"
364					&& r.target
365						.as_view()
366						.segments()
367						.any(|s| s.kind == b"module" && s.name == b"util.h")
368			})
369			.expect("quoted include ref");
370		assert!(
371			quoted
372				.target
373				.as_view()
374				.segments()
375				.any(|segment| { segment.kind == b"dir" && segment.name == b"vendor" })
376		);
377	}
378
379	#[test]
380	fn extract_missing_angle_include_is_external_dependency() {
381		let graph = extract_default(
382			"main.c",
383			"#include <protobuf-c/protobuf-c.h>\n",
384			&make_anchor(),
385			false,
386		);
387		let include = graph
388			.refs()
389			.find(|reference| reference.kind == b"imports_module")
390			.expect("include reference");
391
392		assert_eq!(include.confidence, b"external");
393		assert!(
394			include.target.as_view().segments().any(|segment| {
395				segment.kind == b"external_pkg" && segment.name == b"protobuf-c"
396			})
397		);
398	}
399
400	#[test]
401	fn pgxs_only_claims_known_postgresql_quoted_headers() {
402		let presets = Presets {
403			external_include_package: Some("postgresql".to_string()),
404			..Presets::default()
405		};
406		let graph = extract(
407			"extension.c",
408			"#include \"postgres.h\"\n#include \"local_generated.h\"\n",
409			&make_anchor(),
410			false,
411			&presets,
412		);
413		let includes = graph.refs().collect::<Vec<_>>();
414
415		assert!(includes.iter().any(|reference| {
416			reference.receiver_hint == b"c_build_dependency"
417				&& reference
418					.target
419					.as_view()
420					.segments()
421					.any(|segment| segment.name == b"postgres")
422		}));
423		assert!(includes.iter().any(|reference| {
424			reference.receiver_hint.is_empty()
425				&& reference
426					.target
427					.as_view()
428					.segments()
429					.any(|segment| segment.name == b"local_generated.h")
430		}));
431	}
432
433	#[test]
434	fn extract_quoted_include_cannot_escape_workspace_root() {
435		let graph = extract_default(
436			"src/main.c",
437			"#include \"../../secret.h\"\n",
438			&make_anchor(),
439			false,
440		);
441		let include = graph
442			.refs()
443			.find(|reference| reference.kind == b"imports_module")
444			.expect("include reference");
445
446		assert_eq!(include.confidence, b"external");
447		assert!(
448			include.target.as_view().segments().any(|segment| {
449				segment.kind == b"external_pkg" && segment.name == b"filesystem"
450			})
451		);
452	}
453
454	#[test]
455	fn extract_quoted_include_uses_configured_search_root_when_not_source_relative() {
456		let src = "#include \"protobuf/model.h\"\n";
457		let presets = Presets {
458			include_paths: vec![String::new()],
459			workspace_files: Arc::new(BTreeSet::from([
460				"examples/main.c".to_string(),
461				"protobuf/model.h".to_string(),
462			])),
463			..Presets::default()
464		};
465		let graph = extract("examples/main.c", src, &make_anchor(), false, &presets);
466		let include = graph
467			.refs()
468			.find(|reference| reference.kind == b"imports_module")
469			.expect("include reference");
470		let segments = include.target.as_view().segments().collect::<Vec<_>>();
471
472		assert!(
473			segments
474				.iter()
475				.any(|segment| segment.kind == b"dir" && segment.name == b"protobuf")
476		);
477		assert!(
478			!segments
479				.iter()
480				.any(|segment| segment.kind == b"dir" && segment.name == b"examples")
481		);
482	}
483
484	#[test]
485	fn extract_function_definition_replaces_forward_declaration_slice() {
486		let src = "static int work(int value);\nstatic int work(int value) { return value; }\n";
487		let graph = extract_default("main.c", src, &make_anchor(), false);
488		let work = graph
489			.defs()
490			.find(|definition| definition.kind == b"func")
491			.expect("work definition");
492		assert_eq!(
493			work.position.expect("definition position").0,
494			src.rfind("static int work").unwrap() as u32,
495		);
496	}
497
498	#[test]
499	fn extract_same_file_call_resolves_and_libc_call_is_external() {
500		let src = "#include <string.h>\nstatic int helper(int a) { return a; }\nint run(void) { helper(1); return strlen(\"x\");}\n";
501		let g = extract_default("main.c", src, &make_anchor(), false);
502		let resolved = g
503			.refs()
504			.find(|r| {
505				r.kind == b"calls"
506					&& r.target.as_view().segments().last().unwrap().name == b"helper(a:int)"
507			})
508			.expect("resolved same-file call");
509		assert_eq!(resolved.confidence, b"resolved".to_vec());
510		let libc = g
511			.refs()
512			.find(|r| {
513				r.kind == b"calls"
514					&& r.target.as_view().segments().last().unwrap().name == b"strlen"
515			})
516			.expect("libc call");
517		assert_eq!(libc.confidence, b"external".to_vec());
518		assert!(
519			libc.target
520				.as_view()
521				.segments()
522				.any(|s| s.kind == b"sdk" && s.name == b"c"),
523		);
524		assert!(
525			libc.target
526				.as_view()
527				.segments()
528				.any(|s| s.kind == b"path" && s.name == b"libc")
529		);
530	}
531
532	#[test]
533	fn extract_unresolved_bare_call_stays_name_match_with_hints() {
534		let src = "int run(void) { return listLength(0); }\n";
535		let g = extract_default("main.c", src, &make_anchor(), false);
536		let r = g
537			.refs()
538			.find(|r| {
539				r.kind == b"calls"
540					&& r.target.as_view().segments().last().unwrap().name == b"listLength"
541			})
542			.expect("bare unresolved call");
543		assert_eq!(r.confidence, b"name_match".to_vec());
544		assert_eq!(r.call_name, b"listLength".to_vec());
545		assert_eq!(r.call_arity, Some(1));
546	}
547
548	#[test]
549	fn extract_typed_receiver_field_call_targets_the_field() {
550		let src =
551			"typedef struct vt { void (*free)(void *p); } vt;\nvoid run(vt *v) { v->free(0); }\n";
552		let g = extract_default("vt.c", src, &make_anchor(), false);
553		let call = g
554			.refs()
555			.find(|r| r.kind == b"calls" && r.call_name == b"free".to_vec())
556			.expect("field call");
557		assert_eq!(call.confidence, b"resolved".to_vec());
558		let target_kinds: Vec<&[u8]> = call.target.as_view().segments().map(|s| s.kind).collect();
559		assert!(target_kinds.contains(&b"field".as_slice()));
560	}
561
562	#[test]
563	fn extract_untyped_field_call_is_method_call_fact() {
564		let src = "int run(void *o) { return ((obj *)o)->count; }\nvoid go(void *h) { handler(h)->emit(1); }\n";
565		let g = extract_default("d.c", src, &make_anchor(), false);
566		let dynamic = g
567			.refs()
568			.find(|r| r.kind == b"method_call" && r.call_name == b"emit".to_vec())
569			.expect("untyped fn-pointer call kept as method_call fact");
570		assert_eq!(dynamic.confidence, b"name_match".to_vec());
571	}
572
573	#[test]
574	fn extract_local_type_flow_respects_nested_shadowing() {
575		let src = r#"
576typedef struct first { void (*run)(void); } first;
577typedef struct second { void (*stop)(void); } second;
578int use(first *value) {
579	value->run();
580	{ second *value; value->stop(); }
581	value->run();
582	return 0;
583}
584"#;
585		let graph = extract_default("flow.c", src, &make_anchor(), false);
586		let calls = graph
587			.refs()
588			.filter(|reference| reference.kind == b"calls")
589			.collect::<Vec<_>>();
590		assert_eq!(
591			calls
592				.iter()
593				.filter(|reference| {
594					reference
595						.target
596						.as_view()
597						.segments()
598						.last()
599						.is_some_and(|segment| segment.name == b"run")
600				})
601				.count(),
602			2,
603		);
604		assert!(calls.iter().any(|reference| {
605			reference
606				.target
607				.as_view()
608				.segments()
609				.last()
610				.is_some_and(|segment| segment.name == b"stop")
611		}));
612	}
613
614	#[test]
615	fn extract_global_and_macro_value_reads_are_accounted() {
616		let src =
617			"#define MAX_LEN 10\nint count;\nint run(void) { count = MAX_LEN; return count; }\n";
618		let graph = extract_default("reads.c", src, &make_anchor(), false);
619		let read_targets = graph
620			.refs()
621			.filter(|reference| reference.kind == b"reads")
622			.filter_map(|reference| {
623				reference
624					.target
625					.as_view()
626					.segments()
627					.last()
628					.map(|segment| segment.name.to_vec())
629			})
630			.collect::<Vec<_>>();
631		assert!(read_targets.iter().any(|name| name == b"MAX_LEN"));
632		assert!(read_targets.iter().any(|name| name == b"count"));
633	}
634
635	#[test]
636	fn extract_shallow_skips_param_and_local_defs() {
637		let src = "int run(int x) { int y = 1; return x + y; }\n";
638		let g = extract_default("main.c", src, &make_anchor(), false);
639		assert!(
640			g.defs().all(|d| d.kind != b"param" && d.kind != b"local"),
641			"shallow extraction must not emit param/local defs"
642		);
643	}
644
645	#[test]
646	fn extract_deep_emits_param_and_local_defs() {
647		let src = "int run(int x) { int y = 1; return x + y; }\n";
648		let g = extract_default("main.c", src, &make_anchor(), true);
649		let param = MonikerBuilder::new()
650			.project(b"app")
651			.segment(b"lang", b"c")
652			.segment(b"module", b"main")
653			.segment(b"func", b"run(x:int)")
654			.segment(b"param", b"x")
655			.build();
656		let local = MonikerBuilder::new()
657			.project(b"app")
658			.segment(b"lang", b"c")
659			.segment(b"module", b"main")
660			.segment(b"func", b"run(x:int)")
661			.segment(b"local", b"y")
662			.build();
663		assert!(g.contains(&param));
664		assert!(g.contains(&local));
665	}
666
667	#[test]
668	fn extract_defs_inside_preproc_conditionals() {
669		let src = "#ifdef __linux__\nstatic int only_linux(void) { return 1; }\n#else\nstatic int fallback(void) { return 2; }\n#endif\n";
670		let g = extract_default("port.c", src, &make_anchor(), false);
671		let names: Vec<Vec<u8>> = g
672			.defs()
673			.filter(|d| d.kind == b"func")
674			.map(|d| d.moniker.as_view().segments().last().unwrap().name.to_vec())
675			.collect();
676		assert!(names.contains(&b"only_linux()".to_vec()));
677		assert!(names.contains(&b"fallback()".to_vec()));
678	}
679
680	#[test]
681	fn extract_defs_inside_parser_recovery_nodes() {
682		let src = "#ifndef RECOVERY_H\n#define RECOVERY_H\nBROKEN(\n#define RECOVERED_VALUE 1\n)\n#endif\n";
683		let graph = extract_default("recovery.h", src, &make_anchor(), false);
684
685		assert!(graph.defs().any(|definition| {
686			definition.kind == b"const"
687				&& definition
688					.moniker
689					.as_view()
690					.segments()
691					.last()
692					.is_some_and(|segment| segment.name == b"RECOVERED_VALUE")
693		}));
694		assert!(!graph.defs().any(|definition| {
695			definition
696				.moniker
697				.as_view()
698				.segments()
699				.last()
700				.is_some_and(|segment| segment.name == b"RECOVERY_H")
701		}));
702	}
703
704	#[test]
705	fn extract_defs_inside_conditional_cpp_linkage_block() {
706		let src = "#if defined(__cplusplus)\nextern \"C\" {\n#endif\n#define PUBLIC_API 1\ntypedef struct api_state api_state;\n#if defined(__cplusplus)\n}\n#endif\n";
707		let graph = extract_default("api.h", src, &make_anchor(), false);
708		let names = graph
709			.defs()
710			.filter_map(|definition| {
711				definition
712					.moniker
713					.as_view()
714					.segments()
715					.last()
716					.map(|segment| segment.name.to_vec())
717			})
718			.collect::<Vec<_>>();
719
720		assert!(names.contains(&b"PUBLIC_API".to_vec()));
721		assert!(names.contains(&b"api_state".to_vec()));
722	}
723
724	#[test]
725	fn extract_generated_union_across_line_directives() {
726		let src = r#"
727#if ! defined YYSTYPE
728typedef union YYSTYPE
729#line 233 "gram.y"
730{
731	int ival;
732	void *node;
733}
734#line 1425 "gram.c"
735YYSTYPE;
736#endif
737int read_value(YYSTYPE *value) { return value->ival; }
738"#;
739		let g = extract_default("gram.c", src, &make_anchor(), true);
740		let union = MonikerBuilder::new()
741			.project(b"app")
742			.segment(b"lang", b"c")
743			.segment(b"module", b"gram")
744			.segment(b"struct", b"YYSTYPE")
745			.build();
746		let field = MonikerBuilder::new()
747			.project(b"app")
748			.segment(b"lang", b"c")
749			.segment(b"module", b"gram")
750			.segment(b"struct", b"YYSTYPE")
751			.segment(b"field", b"ival")
752			.build();
753		assert!(
754			g.contains(&union),
755			"generated union expected; defs: {:?}",
756			g.def_monikers()
757		);
758		assert!(
759			g.contains(&field),
760			"generated union field expected; defs: {:?}",
761			g.def_monikers()
762		);
763	}
764
765	#[test]
766	fn extract_prototype_in_header_is_a_func_def() {
767		let src = "struct list;\nstruct list *listCreate(void);\n";
768		let g = extract_default("list.h", src, &make_anchor(), false);
769		let proto = MonikerBuilder::new()
770			.project(b"app")
771			.segment(b"lang", b"c")
772			.segment(b"module", b"list.h")
773			.segment(b"func", b"listCreate()")
774			.build();
775		assert!(g.contains(&proto));
776	}
777
778	#[test]
779	fn extract_function_pointer_variable_is_var_not_func() {
780		let src = "int (*handler)(int) = 0;\n";
781		let g = extract_default("h.c", src, &make_anchor(), false);
782		let var = MonikerBuilder::new()
783			.project(b"app")
784			.segment(b"lang", b"c")
785			.segment(b"module", b"h")
786			.segment(b"var", b"handler")
787			.build();
788		assert!(g.contains(&var), "defs: {:?}", g.def_monikers());
789		assert!(g.defs().all(|d| d.kind != b"func"));
790	}
791
792	#[test]
793	fn extract_shallow_function_pointer_call_remains_accounted() {
794		let source = "int run(void) { int (*handler)(void) = 0; return handler(); }\n";
795		let graph = extract_default("main.c", source, &make_anchor(), false);
796		let call = graph
797			.refs()
798			.find(|reference| {
799				reference.kind == b"calls" && reference.call_name == b"handler".to_vec()
800			})
801			.expect("function-pointer call fact");
802		assert_eq!(call.confidence, b"name_match".to_vec());
803		let target = call
804			.target
805			.as_view()
806			.segments()
807			.last()
808			.expect("local target");
809		assert_eq!(target.kind, b"local");
810		assert_eq!(target.name, b"handler");
811	}
812}