1use std::path::PathBuf;
8
9use fallow_types::extract::{FlagUse, FlagUseKind, ModuleInfo, byte_offset_to_line_col};
10use fallow_types::results::{AnalysisResults, FeatureFlag, FlagConfidence, FlagKind};
11
12use crate::graph::ModuleGraph;
13
14#[deprecated(
20 since = "2.76.0",
21 note = "fallow_core is internal; use fallow_api::run_feature_flags for typed output; serialize with fallow_api::serialize_feature_flags_programmatic_json for JSON output. See docs/fallow-core-migration.md and ADR-008."
22)]
23pub fn collect_feature_flags(modules: &[ModuleInfo], graph: &ModuleGraph) -> Vec<FeatureFlag> {
24 let mut flags = Vec::new();
25
26 for module in modules {
27 if module.flag_uses.is_empty() {
28 continue;
29 }
30
31 let idx = module.file_id.0 as usize;
32 let Some(node) = graph.modules.get(idx) else {
33 continue;
34 };
35
36 for flag_use in &module.flag_uses {
37 let mut flag = flag_use_to_feature_flag(flag_use, node.path.clone());
38
39 if let (Some(start), Some(end)) = (flag_use.guard_span_start, flag_use.guard_span_end)
40 && !module.line_offsets.is_empty()
41 {
42 let (start_line, _) = byte_offset_to_line_col(&module.line_offsets, start);
43 let (end_line, _) = byte_offset_to_line_col(&module.line_offsets, end);
44 flag.guard_line_start = Some(start_line);
45 flag.guard_line_end = Some(end_line);
46 }
47
48 flags.push(flag);
49 }
50 }
51
52 flags
53}
54
55#[deprecated(
61 since = "2.76.0",
62 note = "fallow_core is internal; use fallow_api::run_feature_flags for typed output; serialize with fallow_api::serialize_feature_flags_programmatic_json for JSON output. The `guarded_dead_exports` field carries the same correlation. See docs/fallow-core-migration.md and ADR-008."
63)]
64pub fn correlate_with_dead_code(flags: &mut [FeatureFlag], results: &AnalysisResults) {
65 if results.unused_exports.is_empty() && results.unused_types.is_empty() {
66 return;
67 }
68
69 for flag in flags.iter_mut() {
70 let (Some(guard_start), Some(guard_end)) = (flag.guard_line_start, flag.guard_line_end)
71 else {
72 continue;
73 };
74
75 for export in &results.unused_exports {
76 if export.export.path == flag.path
77 && export.export.line >= guard_start
78 && export.export.line <= guard_end
79 {
80 flag.guarded_dead_exports
81 .push(export.export.export_name.clone());
82 }
83 }
84
85 for export in &results.unused_types {
86 if export.export.path == flag.path
87 && export.export.line >= guard_start
88 && export.export.line <= guard_end
89 {
90 flag.guarded_dead_exports
91 .push(export.export.export_name.clone());
92 }
93 }
94 }
95}
96
97fn flag_use_to_feature_flag(flag_use: &FlagUse, path: PathBuf) -> FeatureFlag {
99 let (kind, confidence) = match flag_use.kind {
100 FlagUseKind::EnvVar => (FlagKind::EnvironmentVariable, FlagConfidence::High),
101 FlagUseKind::SdkCall => (FlagKind::SdkCall, FlagConfidence::High),
102 FlagUseKind::ConfigObject => (FlagKind::ConfigObject, FlagConfidence::Low),
103 };
104
105 FeatureFlag {
106 path,
107 flag_name: flag_use.flag_name.clone(),
108 kind,
109 confidence,
110 line: flag_use.line,
111 col: flag_use.col,
112 guard_span_start: flag_use.guard_span_start,
113 guard_span_end: flag_use.guard_span_end,
114 sdk_name: flag_use.sdk_name.clone(),
115 guard_line_start: None,
116 guard_line_end: None,
117 guarded_dead_exports: Vec::new(),
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use fallow_types::discover::{DiscoveredFile, EntryPoint, FileId};
124 use fallow_types::extract::compute_line_offsets;
125 use fallow_types::output_dead_code::UnusedExportFinding;
126 use fallow_types::results::{AnalysisResults, UnusedExport};
127
128 use crate::graph::ModuleGraph;
129 use crate::resolve::ResolvedModule;
130
131 use super::*;
132
133 fn graph_with_module(file_id: FileId, path: PathBuf) -> ModuleGraph {
141 let files = vec![DiscoveredFile {
142 id: file_id,
143 path: path.clone(),
144 size_bytes: 0,
145 }];
146 let resolved = vec![ResolvedModule {
147 file_id,
148 path,
149 ..Default::default()
150 }];
151 let entry_points: Vec<EntryPoint> = vec![];
152 ModuleGraph::build(&resolved, &entry_points, &files)
153 }
154
155 fn module_with_flags(file_id: FileId, flag_uses: Vec<FlagUse>) -> ModuleInfo {
158 ModuleInfo {
159 file_id,
160 flag_uses,
161 exports: Vec::new(),
162 imports: Vec::new(),
163 re_exports: Vec::new(),
164 dynamic_imports: Vec::new(),
165 dynamic_import_patterns: Vec::new(),
166 require_calls: Vec::new(),
167 package_path_references: Box::default(),
168 member_accesses: Vec::new(),
169 semantic_facts: Box::default(),
170 whole_object_uses: Box::default(),
171 has_cjs_exports: false,
172 has_angular_component_template_url: false,
173 content_hash: 0,
174 suppressions: Vec::new(),
175 unknown_suppression_kinds: Vec::new(),
176 unused_import_bindings: Vec::new(),
177 type_referenced_import_bindings: Vec::new(),
178 value_referenced_import_bindings: Vec::new(),
179 line_offsets: Vec::new(),
180 complexity: Vec::new(),
181 class_heritage: Vec::new(),
182 exported_factory_returns: Box::default(),
183 injection_tokens: Vec::new(),
184 local_type_declarations: Vec::new(),
185 public_signature_type_references: Vec::new(),
186 namespace_object_aliases: Vec::new(),
187 iconify_prefixes: Vec::new(),
188 iconify_icon_names: Vec::new(),
189 auto_import_candidates: Vec::new(),
190 directives: Vec::new(),
191 client_only_dynamic_import_spans: Vec::new(),
192 security_sinks: Vec::new(),
193 security_sinks_skipped: 0,
194 security_unresolved_callee_sites: Vec::new(),
195 tainted_bindings: Vec::new(),
196 sanitized_sink_args: Vec::new(),
197 security_control_sites: Vec::new(),
198 callee_uses: Vec::new(),
199 misplaced_directives: Vec::new(),
200 inline_server_action_exports: Vec::new(),
201 di_key_sites: Vec::new(),
202 has_dynamic_provide: false,
203 referenced_import_bindings: Vec::new(),
204 component_props: Vec::new(),
205 has_props_attrs_fallthrough: false,
206 has_define_expose: false,
207 has_define_model: false,
208 has_unharvestable_props: false,
209 component_emits: Vec::new(),
210 angular_inputs: Vec::new(),
211 angular_outputs: Vec::new(),
212 has_unharvestable_emits: false,
213 has_dynamic_emit: false,
214 has_emit_whole_object_use: false,
215 load_return_keys: Vec::new(),
216 has_unharvestable_load: false,
217 has_load_data_whole_use: false,
218 has_page_data_store_whole_use: false,
219 component_functions: Vec::new(),
220 react_props: Vec::new(),
221 hook_uses: Vec::new(),
222 render_edges: Vec::new(),
223 svelte_dispatched_events: Vec::new(),
224 svelte_listened_events: Vec::new(),
225 angular_component_selectors: Vec::new(),
226 registered_custom_elements: Vec::new(),
227 used_custom_element_tags: Vec::new(),
228 angular_used_selectors: Vec::new(),
229 angular_entry_component_refs: Vec::new(),
230 has_dynamic_component_render: false,
231 has_dynamic_dispatch: false,
232 }
233 }
234
235 fn make_unused_export(path: PathBuf, export_name: &str, line: u32) -> UnusedExportFinding {
236 UnusedExportFinding::with_actions(UnusedExport {
237 path,
238 export_name: export_name.to_string(),
239 is_type_only: false,
240 line,
241 col: 0,
242 span_start: 0,
243 is_re_export: false,
244 })
245 }
246
247 #[test]
253 #[expect(deprecated, reason = "testing the deprecated public API")]
254 fn collect_feature_flags_empty_flag_uses_skipped() {
255 let graph = graph_with_module(FileId(0), PathBuf::from("/project/src/empty.ts"));
256 let module = module_with_flags(FileId(0), vec![]);
257 let flags = collect_feature_flags(&[module], &graph);
258 assert!(
259 flags.is_empty(),
260 "module with no flag_uses should produce no flags"
261 );
262 }
263
264 #[test]
266 #[expect(deprecated, reason = "testing the deprecated public API")]
267 fn collect_feature_flags_missing_graph_node_skipped() {
268 let path = PathBuf::from("/project/src/file.ts");
270 let graph = graph_with_module(FileId(0), path);
271 let flag_use = FlagUse {
272 flag_name: "MY_FLAG".to_string(),
273 kind: FlagUseKind::EnvVar,
274 line: 1,
275 col: 0,
276 guard_span_start: None,
277 guard_span_end: None,
278 sdk_name: None,
279 };
280 let module = module_with_flags(FileId(5), vec![flag_use]);
282 let flags = collect_feature_flags(&[module], &graph);
283 assert!(
284 flags.is_empty(),
285 "module whose file_id has no graph node should be skipped"
286 );
287 }
288
289 #[test]
291 #[expect(deprecated, reason = "testing the deprecated public API")]
292 fn collect_feature_flags_produces_flag_from_env_var() {
293 let graph = graph_with_module(FileId(0), PathBuf::from("/project/src/config.ts"));
294 let flag_use = FlagUse {
295 flag_name: "ENABLE_DARK_MODE".to_string(),
296 kind: FlagUseKind::EnvVar,
297 line: 3,
298 col: 8,
299 guard_span_start: None,
300 guard_span_end: None,
301 sdk_name: None,
302 };
303 let module = module_with_flags(FileId(0), vec![flag_use]);
304 let flags = collect_feature_flags(&[module], &graph);
305 assert_eq!(flags.len(), 1);
306 let flag = &flags[0];
307 assert_eq!(flag.flag_name, "ENABLE_DARK_MODE");
308 assert_eq!(flag.kind, FlagKind::EnvironmentVariable);
309 assert_eq!(flag.confidence, FlagConfidence::High);
310 assert_eq!(flag.line, 3);
311 assert_eq!(flag.col, 8);
312 assert_eq!(
314 flag.path.to_string_lossy().replace('\\', "/"),
315 "/project/src/config.ts"
316 );
317 }
318
319 #[test]
321 #[expect(deprecated, reason = "testing the deprecated public API")]
322 fn collect_feature_flags_sdk_call_maps_sdk_name() {
323 let graph = graph_with_module(FileId(0), PathBuf::from("/project/src/feature.ts"));
324 let flag_use = FlagUse {
325 flag_name: "new-onboarding".to_string(),
326 kind: FlagUseKind::SdkCall,
327 line: 7,
328 col: 0,
329 guard_span_start: None,
330 guard_span_end: None,
331 sdk_name: Some("Unleash".to_string()),
332 };
333 let module = module_with_flags(FileId(0), vec![flag_use]);
334 let flags = collect_feature_flags(&[module], &graph);
335 assert_eq!(flags.len(), 1);
336 assert_eq!(flags[0].kind, FlagKind::SdkCall);
337 assert_eq!(flags[0].confidence, FlagConfidence::High);
338 assert_eq!(flags[0].sdk_name.as_deref(), Some("Unleash"));
339 }
340
341 #[test]
343 #[expect(deprecated, reason = "testing the deprecated public API")]
344 fn collect_feature_flags_config_object_has_low_confidence() {
345 let graph = graph_with_module(FileId(0), PathBuf::from("/project/src/flags.ts"));
346 let flag_use = FlagUse {
347 flag_name: "feature.beta".to_string(),
348 kind: FlagUseKind::ConfigObject,
349 line: 12,
350 col: 4,
351 guard_span_start: None,
352 guard_span_end: None,
353 sdk_name: None,
354 };
355 let module = module_with_flags(FileId(0), vec![flag_use]);
356 let flags = collect_feature_flags(&[module], &graph);
357 assert_eq!(flags.len(), 1);
358 assert_eq!(flags[0].confidence, FlagConfidence::Low);
359 }
360
361 #[test]
363 #[expect(deprecated, reason = "testing the deprecated public API")]
364 fn collect_feature_flags_multiple_uses_in_one_module() {
365 let graph = graph_with_module(FileId(0), PathBuf::from("/project/src/multi.ts"));
366 let flag_uses = vec![
367 FlagUse {
368 flag_name: "FLAG_A".to_string(),
369 kind: FlagUseKind::EnvVar,
370 line: 1,
371 col: 0,
372 guard_span_start: None,
373 guard_span_end: None,
374 sdk_name: None,
375 },
376 FlagUse {
377 flag_name: "FLAG_B".to_string(),
378 kind: FlagUseKind::SdkCall,
379 line: 2,
380 col: 0,
381 guard_span_start: None,
382 guard_span_end: None,
383 sdk_name: None,
384 },
385 ];
386 let module = module_with_flags(FileId(0), flag_uses);
387 let flags = collect_feature_flags(&[module], &graph);
388 assert_eq!(flags.len(), 2);
389 let names: Vec<&str> = flags.iter().map(|f| f.flag_name.as_str()).collect();
390 assert!(names.contains(&"FLAG_A"));
391 assert!(names.contains(&"FLAG_B"));
392 }
393
394 #[test]
398 #[expect(deprecated, reason = "testing the deprecated public API")]
399 fn collect_feature_flags_resolves_guard_span_to_line_numbers() {
400 let source = "abc\ndef\n";
401 let line_offsets = compute_line_offsets(source);
402
403 let graph = graph_with_module(FileId(0), PathBuf::from("/project/src/guarded.ts"));
404 let flag_use = FlagUse {
405 flag_name: "GUARDED_FLAG".to_string(),
406 kind: FlagUseKind::EnvVar,
407 line: 1,
408 col: 0,
409 guard_span_start: Some(1),
410 guard_span_end: Some(5),
411 sdk_name: None,
412 };
413 let mut module = module_with_flags(FileId(0), vec![flag_use]);
414 module.line_offsets = line_offsets;
415
416 let flags = collect_feature_flags(&[module], &graph);
417 assert_eq!(flags.len(), 1);
418 let flag = &flags[0];
419 assert!(
420 flag.guard_line_start.is_some(),
421 "guard_line_start should be resolved from byte offset"
422 );
423 assert!(
424 flag.guard_line_end.is_some(),
425 "guard_line_end should be resolved from byte offset"
426 );
427 }
428
429 #[test]
431 #[expect(deprecated, reason = "testing the deprecated public API")]
432 fn collect_feature_flags_no_guard_lines_when_offsets_empty() {
433 let graph = graph_with_module(FileId(0), PathBuf::from("/project/src/no_offsets.ts"));
434 let flag_use = FlagUse {
435 flag_name: "NO_OFFSETS_FLAG".to_string(),
436 kind: FlagUseKind::EnvVar,
437 line: 1,
438 col: 0,
439 guard_span_start: Some(10),
440 guard_span_end: Some(50),
441 sdk_name: None,
442 };
443 let module = module_with_flags(FileId(0), vec![flag_use]);
445 let flags = collect_feature_flags(&[module], &graph);
446 assert_eq!(flags.len(), 1);
447 assert!(
448 flags[0].guard_line_start.is_none(),
449 "without line_offsets, guard lines stay None"
450 );
451 assert!(
452 flags[0].guard_line_end.is_none(),
453 "without line_offsets, guard lines stay None"
454 );
455 }
456
457 #[test]
460 #[expect(deprecated, reason = "testing the deprecated public API")]
461 fn collect_feature_flags_no_guard_lines_when_span_absent() {
462 let graph = graph_with_module(FileId(0), PathBuf::from("/project/src/no_span.ts"));
463 let flag_use = FlagUse {
464 flag_name: "NO_SPAN_FLAG".to_string(),
465 kind: FlagUseKind::SdkCall,
466 line: 5,
467 col: 0,
468 guard_span_start: None,
469 guard_span_end: None,
470 sdk_name: None,
471 };
472 let mut module = module_with_flags(FileId(0), vec![flag_use]);
473 module.line_offsets = compute_line_offsets("some\ncontent\nhere\n");
474
475 let flags = collect_feature_flags(&[module], &graph);
476 assert_eq!(flags.len(), 1);
477 assert!(flags[0].guard_line_start.is_none());
478 assert!(flags[0].guard_line_end.is_none());
479 }
480
481 #[test]
488 #[expect(deprecated, reason = "testing the deprecated public API")]
489 fn correlate_with_dead_code_early_return_when_results_empty() {
490 let mut flags = vec![FeatureFlag {
491 path: PathBuf::from("/project/src/a.ts"),
492 flag_name: "EARLY".to_string(),
493 kind: FlagKind::EnvironmentVariable,
494 confidence: FlagConfidence::High,
495 line: 1,
496 col: 0,
497 guard_span_start: Some(0),
498 guard_span_end: Some(100),
499 sdk_name: None,
500 guard_line_start: Some(1),
501 guard_line_end: Some(5),
502 guarded_dead_exports: Vec::new(),
503 }];
504 let results = AnalysisResults::default();
505 correlate_with_dead_code(&mut flags, &results);
506 assert!(
507 flags[0].guarded_dead_exports.is_empty(),
508 "no dead exports should be added when results are empty"
509 );
510 }
511
512 #[test]
514 #[expect(deprecated, reason = "testing the deprecated public API")]
515 fn correlate_with_dead_code_flag_without_guard_lines_is_skipped() {
516 let path = PathBuf::from("/project/src/b.ts");
517 let mut flags = vec![FeatureFlag {
518 path: path.clone(),
519 flag_name: "NO_GUARD".to_string(),
520 kind: FlagKind::EnvironmentVariable,
521 confidence: FlagConfidence::High,
522 line: 2,
523 col: 0,
524 guard_span_start: None,
525 guard_span_end: None,
526 sdk_name: None,
527 guard_line_start: None,
528 guard_line_end: None,
529 guarded_dead_exports: Vec::new(),
530 }];
531 let mut results = AnalysisResults::default();
532 results
533 .unused_exports
534 .push(make_unused_export(path, "someExport", 5));
535
536 correlate_with_dead_code(&mut flags, &results);
537 assert!(
538 flags[0].guarded_dead_exports.is_empty(),
539 "flag without guard lines should not accumulate dead exports"
540 );
541 }
542
543 #[test]
545 #[expect(deprecated, reason = "testing the deprecated public API")]
546 fn correlate_with_dead_code_unused_export_within_guard_span_is_credited() {
547 let path = PathBuf::from("/project/src/feature.ts");
548 let mut flags = vec![FeatureFlag {
549 path: path.clone(),
550 flag_name: "MY_FEATURE".to_string(),
551 kind: FlagKind::EnvironmentVariable,
552 confidence: FlagConfidence::High,
553 line: 1,
554 col: 0,
555 guard_span_start: Some(0),
556 guard_span_end: Some(200),
557 sdk_name: None,
558 guard_line_start: Some(10),
559 guard_line_end: Some(20),
560 guarded_dead_exports: Vec::new(),
561 }];
562 let mut results = AnalysisResults::default();
564 results
565 .unused_exports
566 .push(make_unused_export(path, "myExport", 15));
567
568 correlate_with_dead_code(&mut flags, &results);
569 assert_eq!(flags[0].guarded_dead_exports, vec!["myExport"]);
570 }
571
572 #[test]
574 #[expect(deprecated, reason = "testing the deprecated public API")]
575 fn correlate_with_dead_code_export_on_different_path_not_credited() {
576 let other_path = PathBuf::from("/project/src/other.ts");
577 let mut flags = vec![FeatureFlag {
578 path: PathBuf::from("/project/src/feature.ts"),
579 flag_name: "MY_FEATURE".to_string(),
580 kind: FlagKind::EnvironmentVariable,
581 confidence: FlagConfidence::High,
582 line: 1,
583 col: 0,
584 guard_span_start: Some(0),
585 guard_span_end: Some(200),
586 sdk_name: None,
587 guard_line_start: Some(1),
588 guard_line_end: Some(50),
589 guarded_dead_exports: Vec::new(),
590 }];
591 let mut results = AnalysisResults::default();
592 results
593 .unused_exports
594 .push(make_unused_export(other_path, "wrongFile", 10));
595
596 correlate_with_dead_code(&mut flags, &results);
597 assert!(
598 flags[0].guarded_dead_exports.is_empty(),
599 "export from a different path should not be credited"
600 );
601 }
602
603 #[test]
605 #[expect(deprecated, reason = "testing the deprecated public API")]
606 fn correlate_with_dead_code_export_outside_line_range_not_credited() {
607 let path = PathBuf::from("/project/src/feature.ts");
608 let mut flags = vec![FeatureFlag {
609 path: path.clone(),
610 flag_name: "MY_FEATURE".to_string(),
611 kind: FlagKind::EnvironmentVariable,
612 confidence: FlagConfidence::High,
613 line: 1,
614 col: 0,
615 guard_span_start: Some(0),
616 guard_span_end: Some(200),
617 sdk_name: None,
618 guard_line_start: Some(10),
619 guard_line_end: Some(20),
620 guarded_dead_exports: Vec::new(),
621 }];
622 let mut results = AnalysisResults::default();
623 results
625 .unused_exports
626 .push(make_unused_export(path, "outsideExport", 99));
627
628 correlate_with_dead_code(&mut flags, &results);
629 assert!(
630 flags[0].guarded_dead_exports.is_empty(),
631 "export outside guard line range should not be credited"
632 );
633 }
634
635 #[test]
637 #[expect(deprecated, reason = "testing the deprecated public API")]
638 fn correlate_with_dead_code_unused_type_within_guard_span_is_credited() {
639 use fallow_types::output_dead_code::UnusedTypeFinding;
640
641 let path = PathBuf::from("/project/src/types.ts");
642 let mut flags = vec![FeatureFlag {
643 path: path.clone(),
644 flag_name: "TYPE_FLAG".to_string(),
645 kind: FlagKind::SdkCall,
646 confidence: FlagConfidence::High,
647 line: 1,
648 col: 0,
649 guard_span_start: Some(0),
650 guard_span_end: Some(500),
651 sdk_name: None,
652 guard_line_start: Some(5),
653 guard_line_end: Some(30),
654 guarded_dead_exports: Vec::new(),
655 }];
656 let unused_type = UnusedTypeFinding::with_actions(UnusedExport {
657 path,
658 export_name: "MyInterface".to_string(),
659 is_type_only: true,
660 line: 10,
661 col: 0,
662 span_start: 0,
663 is_re_export: false,
664 });
665 let mut results = AnalysisResults::default();
666 results.unused_types.push(unused_type);
667
668 correlate_with_dead_code(&mut flags, &results);
669 assert_eq!(flags[0].guarded_dead_exports, vec!["MyInterface"]);
670 }
671
672 #[test]
674 #[expect(deprecated, reason = "testing the deprecated public API")]
675 fn correlate_with_dead_code_combines_exports_and_types() {
676 use fallow_types::output_dead_code::UnusedTypeFinding;
677
678 let path = PathBuf::from("/project/src/combined.ts");
679 let mut flags = vec![FeatureFlag {
680 path: path.clone(),
681 flag_name: "COMBO".to_string(),
682 kind: FlagKind::EnvironmentVariable,
683 confidence: FlagConfidence::High,
684 line: 1,
685 col: 0,
686 guard_span_start: Some(0),
687 guard_span_end: Some(1000),
688 sdk_name: None,
689 guard_line_start: Some(1),
690 guard_line_end: Some(100),
691 guarded_dead_exports: Vec::new(),
692 }];
693 let mut results = AnalysisResults::default();
694 results
695 .unused_exports
696 .push(make_unused_export(path.clone(), "valueExport", 10));
697 results
698 .unused_types
699 .push(UnusedTypeFinding::with_actions(UnusedExport {
700 path,
701 export_name: "TypeExport".to_string(),
702 is_type_only: true,
703 line: 50,
704 col: 0,
705 span_start: 0,
706 is_re_export: false,
707 }));
708
709 correlate_with_dead_code(&mut flags, &results);
710 assert!(
711 flags[0]
712 .guarded_dead_exports
713 .contains(&"valueExport".to_string())
714 );
715 assert!(
716 flags[0]
717 .guarded_dead_exports
718 .contains(&"TypeExport".to_string())
719 );
720 }
721
722 #[test]
724 #[expect(deprecated, reason = "testing the deprecated public API")]
725 fn correlate_with_dead_code_export_at_guard_start_is_credited() {
726 let path = PathBuf::from("/project/src/boundary.ts");
727 let mut flags = vec![FeatureFlag {
728 path: path.clone(),
729 flag_name: "BOUNDARY_FLAG".to_string(),
730 kind: FlagKind::EnvironmentVariable,
731 confidence: FlagConfidence::High,
732 line: 1,
733 col: 0,
734 guard_span_start: Some(0),
735 guard_span_end: Some(200),
736 sdk_name: None,
737 guard_line_start: Some(10),
738 guard_line_end: Some(20),
739 guarded_dead_exports: Vec::new(),
740 }];
741 let mut results = AnalysisResults::default();
742 results
743 .unused_exports
744 .push(make_unused_export(path, "atStart", 10));
745
746 correlate_with_dead_code(&mut flags, &results);
747 assert!(
748 flags[0]
749 .guarded_dead_exports
750 .contains(&"atStart".to_string()),
751 "export exactly at guard_line_start should be credited (inclusive lower bound)"
752 );
753 }
754
755 #[test]
757 #[expect(deprecated, reason = "testing the deprecated public API")]
758 fn correlate_with_dead_code_export_at_guard_end_is_credited() {
759 let path = PathBuf::from("/project/src/boundary.ts");
760 let mut flags = vec![FeatureFlag {
761 path: path.clone(),
762 flag_name: "BOUNDARY_FLAG".to_string(),
763 kind: FlagKind::EnvironmentVariable,
764 confidence: FlagConfidence::High,
765 line: 1,
766 col: 0,
767 guard_span_start: Some(0),
768 guard_span_end: Some(200),
769 sdk_name: None,
770 guard_line_start: Some(10),
771 guard_line_end: Some(20),
772 guarded_dead_exports: Vec::new(),
773 }];
774 let mut results = AnalysisResults::default();
775 results
776 .unused_exports
777 .push(make_unused_export(path, "atEnd", 20));
778
779 correlate_with_dead_code(&mut flags, &results);
780 assert!(
781 flags[0].guarded_dead_exports.contains(&"atEnd".to_string()),
782 "export exactly at guard_line_end should be credited (inclusive upper bound)"
783 );
784 }
785
786 #[test]
788 #[expect(deprecated, reason = "testing the deprecated public API")]
789 fn correlate_with_dead_code_multiple_flags_independent() {
790 let path = PathBuf::from("/project/src/multi_flag.ts");
791 let mut flags = vec![
792 FeatureFlag {
793 path: path.clone(),
794 flag_name: "FLAG_1".to_string(),
795 kind: FlagKind::EnvironmentVariable,
796 confidence: FlagConfidence::High,
797 line: 1,
798 col: 0,
799 guard_span_start: Some(0),
800 guard_span_end: Some(200),
801 sdk_name: None,
802 guard_line_start: Some(1),
803 guard_line_end: Some(10),
804 guarded_dead_exports: Vec::new(),
805 },
806 FeatureFlag {
807 path: path.clone(),
808 flag_name: "FLAG_2".to_string(),
809 kind: FlagKind::EnvironmentVariable,
810 confidence: FlagConfidence::High,
811 line: 15,
812 col: 0,
813 guard_span_start: Some(200),
814 guard_span_end: Some(400),
815 sdk_name: None,
816 guard_line_start: Some(20),
817 guard_line_end: Some(30),
818 guarded_dead_exports: Vec::new(),
819 },
820 ];
821 let mut results = AnalysisResults::default();
822 results
824 .unused_exports
825 .push(make_unused_export(path.clone(), "exportForFlag1", 5));
826 results
828 .unused_exports
829 .push(make_unused_export(path, "exportForFlag2", 25));
830
831 correlate_with_dead_code(&mut flags, &results);
832 assert_eq!(flags[0].guarded_dead_exports, vec!["exportForFlag1"]);
833 assert_eq!(flags[1].guarded_dead_exports, vec!["exportForFlag2"]);
834 }
835
836 #[test]
841 fn flag_use_to_feature_flag_env_var() {
842 let flag_use = FlagUse {
843 flag_name: "FEATURE_X".to_string(),
844 kind: FlagUseKind::EnvVar,
845 line: 10,
846 col: 4,
847 guard_span_start: Some(100),
848 guard_span_end: Some(200),
849 sdk_name: None,
850 };
851
852 let result = flag_use_to_feature_flag(&flag_use, PathBuf::from("src/config.ts"));
853 assert_eq!(result.flag_name, "FEATURE_X");
854 assert_eq!(result.kind, FlagKind::EnvironmentVariable);
855 assert_eq!(result.confidence, FlagConfidence::High);
856 assert_eq!(result.line, 10);
857 assert!(result.guard_span_start.is_some());
858 }
859
860 #[test]
861 fn flag_use_to_feature_flag_sdk_call() {
862 let flag_use = FlagUse {
863 flag_name: "new-checkout".to_string(),
864 kind: FlagUseKind::SdkCall,
865 line: 5,
866 col: 0,
867 guard_span_start: None,
868 guard_span_end: None,
869 sdk_name: Some("LaunchDarkly".to_string()),
870 };
871
872 let result = flag_use_to_feature_flag(&flag_use, PathBuf::from("src/hooks.ts"));
873 assert_eq!(result.kind, FlagKind::SdkCall);
874 assert_eq!(result.confidence, FlagConfidence::High);
875 assert_eq!(result.sdk_name.as_deref(), Some("LaunchDarkly"));
876 }
877
878 #[test]
879 fn flag_use_to_feature_flag_config_object() {
880 let flag_use = FlagUse {
881 flag_name: "features.newCheckout".to_string(),
882 kind: FlagUseKind::ConfigObject,
883 line: 42,
884 col: 8,
885 guard_span_start: None,
886 guard_span_end: None,
887 sdk_name: None,
888 };
889
890 let result = flag_use_to_feature_flag(&flag_use, PathBuf::from("src/app.ts"));
891 assert_eq!(result.kind, FlagKind::ConfigObject);
892 assert_eq!(result.confidence, FlagConfidence::Low);
893 }
894}