1use std::path::{Path, PathBuf};
4
5use rustc_hash::FxHashSet;
6
7use fallow_types::{discover::DiscoveredFile, extract::ModuleInfo};
8
9use crate::{ChangeScope, ChangeScopeRequest, PackageBaselineError};
10
11pub type EditorCloneFamily = fallow_types::duplicates::CloneFamily;
13pub type EditorCloneGroup = fallow_types::duplicates::CloneGroup;
15pub type EditorCloneInstance = fallow_types::duplicates::CloneInstance;
17pub type EditorDuplicationReport = fallow_types::duplicates::DuplicationReport;
19pub type EditorDuplicationStats = fallow_types::duplicates::DuplicationStats;
21pub type EditorMirroredDirectory = fallow_types::duplicates::MirroredDirectory;
23pub type EditorRefactoringKind = fallow_types::duplicates::RefactoringKind;
25pub type EditorRefactoringSuggestion = fallow_types::duplicates::RefactoringSuggestion;
27
28#[derive(Debug, Clone)]
30pub struct EditorCloneFingerprintSet {
31 inner: fallow_engine::duplicates::CloneFingerprintSet,
32}
33
34impl EditorCloneFingerprintSet {
35 #[must_use]
37 pub fn from_groups(groups: &[EditorCloneGroup]) -> Self {
38 Self {
39 inner: fallow_engine::duplicates::CloneFingerprintSet::from_groups(groups),
40 }
41 }
42
43 #[must_use]
45 pub fn fingerprint_for_group(&self, group: &EditorCloneGroup) -> String {
46 self.inner.fingerprint_for_group(group)
47 }
48
49 #[must_use]
51 pub fn fingerprint_for_parts(
52 &self,
53 instances: &[EditorCloneInstance],
54 token_count: usize,
55 line_count: usize,
56 ) -> String {
57 self.inner
58 .fingerprint_for_parts(instances, token_count, line_count)
59 }
60
61 #[must_use]
63 pub fn find_group<'a>(
64 &self,
65 groups: &'a [EditorCloneGroup],
66 fingerprint: &str,
67 ) -> Option<&'a EditorCloneGroup> {
68 self.inner.find_group(groups, fingerprint)
69 }
70}
71
72pub mod editor_duplicates {
75 pub use crate::editor::{
76 EditorCloneFamily as CloneFamily, EditorCloneFingerprintSet as CloneFingerprintSet,
77 EditorCloneGroup as CloneGroup, EditorCloneInstance as CloneInstance,
78 EditorDuplicationReport as DuplicationReport, EditorDuplicationStats as DuplicationStats,
79 EditorMirroredDirectory as MirroredDirectory, EditorRefactoringKind as RefactoringKind,
80 EditorRefactoringSuggestion as RefactoringSuggestion,
81 };
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum ChangedFilesError {
87 InvalidRef(String),
89 GitMissing(String),
91 NotARepository,
93 GitFailed(String),
95}
96
97impl ChangedFilesError {
98 #[must_use]
100 pub fn describe(&self) -> String {
101 match self {
102 Self::InvalidRef(err) => format!("invalid git ref: {err}"),
103 Self::GitMissing(err) => format!("failed to run git: {err}"),
104 Self::NotARepository => "not a git repository".to_owned(),
105 Self::GitFailed(stderr) => {
106 let lower = stderr.to_ascii_lowercase();
107 if lower.contains("not a valid object name")
108 || lower.contains("unknown revision")
109 || lower.contains("ambiguous argument")
110 {
111 format!(
112 "{stderr} (shallow clone? try `git fetch --unshallow`, or set `fetch-depth: 0` on actions/checkout / `GIT_DEPTH: 0` in GitLab CI)"
113 )
114 } else {
115 stderr.clone()
116 }
117 }
118 }
119 }
120}
121
122impl From<fallow_engine::changed_files::ChangedFilesError> for ChangedFilesError {
123 fn from(error: fallow_engine::changed_files::ChangedFilesError) -> Self {
124 match error {
125 fallow_engine::changed_files::ChangedFilesError::InvalidRef(err) => {
126 Self::InvalidRef(err)
127 }
128 fallow_engine::changed_files::ChangedFilesError::GitMissing(err) => {
129 Self::GitMissing(err)
130 }
131 fallow_engine::changed_files::ChangedFilesError::NotARepository => Self::NotARepository,
132 fallow_engine::changed_files::ChangedFilesError::GitFailed(stderr) => {
133 Self::GitFailed(stderr)
134 }
135 }
136 }
137}
138
139pub fn resolve_git_toplevel(cwd: &Path) -> Result<PathBuf, ChangedFilesError> {
146 fallow_engine::changed_files::resolve_git_toplevel(cwd).map_err(ChangedFilesError::from)
147}
148
149pub fn try_get_changed_files_with_toplevel(
156 cwd: &Path,
157 toplevel: &Path,
158 git_ref: &str,
159) -> Result<FxHashSet<PathBuf>, ChangedFilesError> {
160 fallow_engine::changed_files::try_get_changed_files_with_toplevel(cwd, toplevel, git_ref)
161 .map_err(ChangedFilesError::from)
162}
163
164pub mod editor_extract {
167 pub use fallow_types::extract::{
168 AngularComponentSelector, AngularInputMember, AngularOutputMember,
169 AngularTemplateMemberAccessFact, AngularThisSpreadFact, CalleeUse, ClassHeritageInfo,
170 ComplexityContribution, ComplexityContributionKind, ComplexityMetric, ComponentEmit,
171 ComponentFunction, ComponentFunctionKind, ComponentProp, CssAnalytics, CssDeclarationBlock,
172 CssRuleMetric, DiFramework, DiKeySite, DiRole, DispatchedEvent,
173 DynamicCustomElementRenderFact, DynamicImportInfo, DynamicImportPattern, ExportInfo,
174 ExportName, FactoryCallMemberAccessFact, FactoryFnMemberAccessFact, FactoryReturnExport,
175 FlagUse, FlagUseKind, FluentChainMemberAccessFact, FluentChainNewMemberAccessFact,
176 ForwardAttr, FunctionComplexity, HookUse, HookUseKind, ImportInfo, ImportedName,
177 InstanceExportBindingFact, LoadReturnKey, LocalTypeDeclaration, MemberAccess, MemberInfo,
178 MemberKind, MisplacedDirectiveSite, ModuleInfo, NamespaceObjectAlias, PUBLIC_ENV_EXACT,
179 PUBLIC_ENV_METADATA_TOKENS, PUBLIC_ENV_PREFIXES, ParseResult, PlaywrightFixtureAliasFact,
180 PlaywrightFixtureDefinitionFact, PlaywrightFixtureTypeFact, PlaywrightFixtureUseFact,
181 PublicSignatureTypeReference, ReExportInfo, RegisteredCustomElement, RenderEdge,
182 RequireCallInfo, SECRET_ENV_TOKENS, SanitizedSinkArg, SanitizerScope, SecurityControlKind,
183 SecurityControlSite, SecurityUrlShape, SemanticFact, SemanticFactView, SinkArgKind,
184 SinkLiteralValue, SinkObjectProperty, SinkShape, SinkSite,
185 SkippedSecurityCalleeExpressionKind, SkippedSecurityCalleeReason,
186 SkippedSecurityCalleeSite, TaintedBinding, VisibilityTag,
187 };
188}
189
190pub mod editor_results {
193 pub use fallow_types::output_dead_code::{
194 AbsentComponentPropFinding, BoundaryCallViolationFinding, BoundaryCoverageViolationFinding,
195 BoundaryViolationFinding, CircularDependencyFinding, DeprecatedExportInUseFinding,
196 DevDependencyInProductionFinding, DuplicateExportFinding, DuplicatePropShapeFinding,
197 DynamicSegmentNameConflictFinding, EmptyCatalogGroupFinding, InvalidClientExportFinding,
198 MisconfiguredDependencyOverrideFinding, MisplacedDirectiveFinding,
199 MixedClientServerBarrelFinding, PackageCycleFinding, PolicyViolationFinding,
200 PrivateTypeLeakFinding, PropDrillingChainFinding, ReExportCycleFinding,
201 RouteCollisionFinding, TestOnlyDependencyFinding, ThinWrapperFinding,
202 TypeOnlyDependencyFinding, UnlistedDependencyFinding, UnprovidedInjectFinding,
203 UnrenderedComponentFinding, UnresolvedCatalogReferenceFinding, UnresolvedImportFinding,
204 UnusedCatalogEntryFinding, UnusedClassMemberFinding, UnusedComponentEmitFinding,
205 UnusedComponentInputFinding, UnusedComponentOutputFinding, UnusedComponentPropFinding,
206 UnusedDependencyFinding, UnusedDependencyOverrideFinding, UnusedDevDependencyFinding,
207 UnusedEnumMemberFinding, UnusedExportFinding, UnusedFileFinding, UnusedLoadDataKeyFinding,
208 UnusedOptionalDependencyFinding, UnusedServerActionFinding, UnusedStoreMemberFinding,
209 UnusedSvelteEventFinding, UnusedTypeFinding,
210 };
211 pub use fallow_types::output_dead_code::{
212 UnproducedCatchTagFinding, UnusedEffectLayerFinding, UnusedTaggedErrorFinding,
213 };
214 pub use fallow_types::results::{
215 AbsentComponentProp, ActiveSuppression, AnalysisResults, BoundaryCallViolation,
216 BoundaryCoverageViolation, BoundaryViolation, CircularDependency, CircularDependencyEdge,
217 ComponentPropCallSite, DependencyLocation, DependencyOverrideMisconfigReason,
218 DependencyOverrideSource, DeprecatedConsumerKind, DeprecatedExportConsumer,
219 DeprecatedExportInUse, DevDependencyInProduction, DuplicateExport, DuplicateLocation,
220 DuplicatePropShape, DuplicatePropShapeMember, DynamicSegmentNameConflict,
221 EmptyCatalogGroup, EntryPointSummary, ExportUsage, FeatureFlag, FlagConfidence, FlagKind,
222 ImportSite, InvalidClientExport, MisconfiguredDependencyOverride, MisplacedDirective,
223 MixedClientServerBarrel, PackageCycle, PackageCycleEdge, PolicyRuleKind, PolicyViolation,
224 PolicyViolationSeverity, PrivateTypeLeak, PropDrillHop, PropDrillingChain, ReExportCycle,
225 ReExportCycleKind, ReactComponentIntel, ReactHookSummary, ReactPropDrill, ReactPropIntel,
226 ReferenceLocation, RenderFanInComponent, RenderFanInMetric, RouteCollision,
227 SecurityAttackSurfaceEntry, SecurityCandidate, SecurityCandidateBoundary,
228 SecurityCandidateSink, SecurityDeadCodeContext, SecurityDeadCodeKind,
229 SecurityDefensiveBoundary, SecurityDefensiveControl, SecurityFinding, SecurityFindingKind,
230 SecurityNetworkContext, SecurityReachability, SecurityRuntimeContext, SecurityRuntimeState,
231 SecuritySeverity, SecurityTaintFlow, SecurityUnresolvedCalleeDiagnostic,
232 SecurityZoneCrossing, StaleSuppression, SuppressionOrigin, TaintConfidence, TaintEndpoint,
233 TaintPath, TestOnlyDependency, ThinWrapper, TraceHop, TraceHopRole, TypeOnlyDependency,
234 UnlistedDependency, UnprovidedInject, UnrenderedComponent, UnresolvedCatalogReference,
235 UnresolvedImport, UnusedCatalogEntry, UnusedComponentEmit, UnusedComponentInput,
236 UnusedComponentOutput, UnusedComponentProp, UnusedDependency, UnusedDependencyOverride,
237 UnusedExport, UnusedFile, UnusedLoadDataKey, UnusedMember, UnusedServerAction,
238 UnusedSvelteEvent,
239 };
240 pub use fallow_types::results::{
241 EffectCatchSite, EffectLayerKind, UnproducedCatchTag, UnusedEffectLayer, UnusedTaggedError,
242 };
243}
244
245pub mod editor_security {
247 #[must_use]
249 pub fn security_catalogue_title(kind: &str) -> Option<&'static str> {
250 fallow_engine::dead_code::security_catalogue_title(kind)
251 }
252}
253
254pub mod editor_suppress {
256 pub use fallow_types::suppress::{IssueKind, is_suppressed};
257}
258
259pub type EditorAnalysisResults = fallow_types::results::AnalysisResults;
261
262#[derive(Debug)]
267pub struct EditorDeadCodeAnalysisOutput {
268 pub results: EditorAnalysisResults,
270 pub modules: Option<Vec<ModuleInfo>>,
273 pub files: Option<Vec<DiscoveredFile>>,
275}
276
277impl EditorDeadCodeAnalysisOutput {
278 fn from_engine(output: fallow_engine::dead_code::DeadCodeAnalysisOutput) -> Self {
279 Self {
280 results: output.results,
281 modules: output.modules,
282 files: output.files,
283 }
284 }
285}
286
287#[derive(Debug, Clone, PartialEq, Eq)]
293pub struct EditorInlineComplexityFinding {
294 pub path: PathBuf,
296 pub name: String,
298 pub line: u32,
300 pub col: u32,
302 pub cyclomatic: u16,
304 pub cognitive: u16,
306 pub exceeded: EditorInlineComplexityExceeded,
308}
309
310#[derive(Debug, Clone, Copy, PartialEq, Eq)]
312pub enum EditorInlineComplexityExceeded {
313 Cyclomatic,
315 Cognitive,
317 CyclomaticAndCognitive,
319}
320
321#[must_use]
327pub fn collect_inline_complexity(
328 config: &fallow_config::ResolvedConfig,
329 output: &EditorDeadCodeAnalysisOutput,
330) -> Vec<EditorInlineComplexityFinding> {
331 let (Some(modules), Some(files)) = (output.modules.as_ref(), output.files.as_ref()) else {
332 return Vec::new();
333 };
334 fallow_engine::health::inline_complexity(config, modules, files)
335 .into_iter()
336 .map(|finding| EditorInlineComplexityFinding {
337 exceeded: match (finding.exceeds_cyclomatic, finding.exceeds_cognitive) {
338 (true, true) => EditorInlineComplexityExceeded::CyclomaticAndCognitive,
339 (true, false) => EditorInlineComplexityExceeded::Cyclomatic,
340 (false, _) => EditorInlineComplexityExceeded::Cognitive,
341 },
342 path: finding.path,
343 name: finding.name,
344 line: finding.line,
345 col: finding.col,
346 cyclomatic: finding.cyclomatic,
347 cognitive: finding.cognitive,
348 })
349 .collect()
350}
351
352#[allow(
354 clippy::implicit_hasher,
355 reason = "editor analysis changed-file sets use the workspace FxHashSet convention"
356)]
357pub fn filter_inline_complexity_by_changed_files(
358 findings: &mut Vec<EditorInlineComplexityFinding>,
359 changed_files: &FxHashSet<PathBuf>,
360) {
361 findings.retain(|finding| changed_files.contains(&finding.path));
362}
363
364pub fn filter_inline_complexity_by_change_scope(
366 findings: &mut Vec<EditorInlineComplexityFinding>,
367 scope: &ChangeScope,
368) {
369 findings.retain(|finding| scope.contains(&finding.path));
370}
371
372pub type EditorSessionParseCounts = fallow_engine::session::SessionParseCounts;
375
376#[derive(Debug)]
378pub struct EditorAnalysisSession {
379 inner: fallow_engine::session::AnalysisSession,
380}
381
382impl EditorAnalysisSession {
383 pub fn load(root: &Path, config_path: Option<&Path>) -> fallow_engine::EngineResult<Self> {
389 fallow_engine::session::AnalysisSession::load(root, config_path).map(Self::from_engine)
390 }
391
392 pub fn load_with_config(
398 root: &Path,
399 config_path: Option<&Path>,
400 configure: impl FnOnce(&mut fallow_config::ResolvedConfig),
401 ) -> fallow_engine::EngineResult<Self> {
402 fallow_engine::session::AnalysisSession::load_with_config(root, config_path, configure)
403 .map(Self::from_engine)
404 }
405
406 pub fn load_with_config_options(
413 root: &Path,
414 config_path: Option<&Path>,
415 load_options: fallow_config::ConfigLoadOptions,
416 configure: impl FnOnce(&mut fallow_config::ResolvedConfig),
417 ) -> fallow_engine::EngineResult<Self> {
418 fallow_engine::session::AnalysisSession::load_with_config_options(
419 root,
420 config_path,
421 load_options,
422 configure,
423 )
424 .map(Self::from_engine)
425 }
426
427 #[must_use]
429 pub fn load_default(root: &Path) -> Self {
430 Self::from_engine(fallow_engine::session::AnalysisSession::load_default(root))
431 }
432
433 #[must_use]
436 pub fn load_default_with_config(
437 root: &Path,
438 configure: impl FnOnce(&mut fallow_config::ResolvedConfig),
439 ) -> Self {
440 Self::from_engine(
441 fallow_engine::session::AnalysisSession::load_default_with_config(root, configure),
442 )
443 }
444
445 #[must_use]
454 pub fn with_cancellation(
455 self,
456 cancellation: std::sync::Arc<std::sync::atomic::AtomicBool>,
457 ) -> Self {
458 Self::from_engine(self.inner.with_cancellation(cancellation))
459 }
460
461 pub fn set_cancellation(
463 &mut self,
464 cancellation: std::sync::Arc<std::sync::atomic::AtomicBool>,
465 ) {
466 self.inner.set_cancellation(cancellation);
467 }
468
469 pub fn refresh_discovery(&mut self) -> bool {
474 self.inner.refresh_discovery()
475 }
476
477 #[must_use]
481 pub fn config_inputs(&self) -> &fallow_config::ConfigInputs {
482 self.inner.config_inputs()
483 }
484
485 #[must_use]
488 pub fn config_inputs_before_resolve(&self) -> &fallow_config::ConfigInputsSnapshot {
489 self.inner.config_inputs_before_resolve()
490 }
491
492 #[must_use]
495 pub fn retained_bytes_estimate(&self) -> u64 {
496 self.inner.retained_bytes_estimate()
497 }
498
499 pub fn flush_parse_cache(&self) {
502 self.inner.flush_parse_cache();
503 }
504
505 pub fn prewarm(&self, retain_complexity_artifacts: bool) -> fallow_engine::EngineResult<()> {
512 self.inner
513 .prewarm_parsed_modules(retain_complexity_artifacts)
514 }
515
516 #[must_use]
518 pub fn parse_counts(&self) -> EditorSessionParseCounts {
519 self.inner.parse_counts()
520 }
521
522 #[must_use]
524 pub fn config(&self) -> &fallow_config::ResolvedConfig {
525 self.inner.config()
526 }
527
528 pub fn change_scope(
536 &self,
537 request: ChangeScopeRequest<'_>,
538 ) -> Result<ChangeScope, PackageBaselineError> {
539 ChangeScope::resolve(request, self.inner.config(), self.inner.workspaces())
540 }
541
542 #[must_use]
547 pub fn unmatched_config_patterns(&self) -> Vec<fallow_config::WorkspaceDiagnostic> {
548 fallow_engine::dead_code::config_pattern_diagnostics(self.inner.config(), true)
549 }
550
551 #[must_use]
553 pub fn config_path(&self) -> Option<&Path> {
554 self.inner.config_path()
555 }
556
557 pub fn refine_type_aware_dead_code(
565 &self,
566 options: &crate::TypeAwareOptions,
567 filters: &crate::DeadCodeFilters,
568 output: &mut EditorDeadCodeAnalysisOutput,
569 ) -> Result<Option<fallow_types::envelope::TypeAwareMeta>, crate::ProgrammaticError> {
570 let meta = crate::type_aware::refine_programmatic_dead_code(
571 options,
572 filters,
573 &self.inner,
574 &mut output.results,
575 )?;
576 fallow_engine::dead_code::apply_rule_severities(&mut output.results, self.inner.config());
580 Ok(meta)
581 }
582
583 pub fn refine_type_aware_dead_code_in_session(
585 &self,
586 semantic_session: &mut crate::TypeAwareSession,
587 changes: Option<&crate::TypeAwareFileChanges>,
588 options: &crate::TypeAwareOptions,
589 filters: &crate::DeadCodeFilters,
590 output: &mut EditorDeadCodeAnalysisOutput,
591 ) -> Result<Option<fallow_types::envelope::TypeAwareMeta>, crate::ProgrammaticError> {
592 let meta = crate::type_aware::refine_programmatic_dead_code_in_session(
593 semantic_session,
594 changes,
595 options,
596 filters,
597 &self.inner,
598 &mut output.results,
599 )?;
600 fallow_engine::dead_code::apply_rule_severities(&mut output.results, self.inner.config());
601 Ok(meta)
602 }
603
604 pub fn analyze_project_with(
610 &self,
611 duplicates_config: &fallow_config::DuplicatesConfig,
612 retain_complexity_artifacts: bool,
613 ) -> fallow_engine::EngineResult<EditorProjectAnalysisOutput> {
614 self.inner
615 .analyze_project_with(duplicates_config, retain_complexity_artifacts)
616 .map(EditorProjectAnalysisOutput::from_engine)
617 .map(|output| self.with_resolved_rule_severities(output))
618 }
619
620 pub fn analyze_project_with_changed_files(
631 &self,
632 duplicates_config: &fallow_config::DuplicatesConfig,
633 retain_complexity_artifacts: bool,
634 changed_files: Option<&FxHashSet<PathBuf>>,
635 ) -> fallow_engine::EngineResult<EditorProjectAnalysisOutput> {
636 self.inner
637 .analyze_project_with_artifacts(
638 duplicates_config,
639 fallow_engine::project_analysis::ProjectAnalysisArtifactOptions {
640 retain_complexity_artifacts,
641 changed_files: changed_files.cloned(),
642 ..fallow_engine::project_analysis::ProjectAnalysisArtifactOptions::default()
643 },
644 )
645 .map(fallow_engine::project_analysis::ProjectAnalysisArtifacts::into_output)
646 .map(EditorProjectAnalysisOutput::from_engine)
647 .map(|output| self.with_resolved_rule_severities(output))
648 }
649
650 pub fn apply_change_scope(
658 &self,
659 output: &mut EditorProjectAnalysisOutput,
660 scope: &ChangeScope,
661 ) {
662 fallow_engine::dead_code::apply_scope(
663 &mut output.dead_code.results,
664 &fallow_engine::dead_code::DeadCodeScope {
665 workspace_roots: None,
666 changes: Some(scope),
667 diff: None,
668 files: None,
669 },
670 self.inner.config(),
671 );
672 fallow_engine::duplicates::apply_scope(
673 &mut output.duplication,
674 &fallow_engine::duplicates::DuplicationScope {
675 changes: Some(scope),
676 diff: None,
677 workspace_roots: None,
678 },
679 &self.inner.config().root,
680 );
681 }
682
683 fn with_resolved_rule_severities(
690 &self,
691 mut output: EditorProjectAnalysisOutput,
692 ) -> EditorProjectAnalysisOutput {
693 fallow_engine::dead_code::apply_rule_severities(
694 &mut output.dead_code.results,
695 self.inner.config(),
696 );
697 output
698 }
699
700 const fn from_engine(inner: fallow_engine::session::AnalysisSession) -> Self {
701 Self { inner }
702 }
703}
704
705#[derive(Debug)]
707pub struct EditorProjectAnalysisOutput {
708 pub dead_code: EditorDeadCodeAnalysisOutput,
710 pub duplication: EditorDuplicationReport,
712}
713
714impl EditorProjectAnalysisOutput {
715 fn from_engine(output: fallow_engine::project_analysis::ProjectAnalysisOutput) -> Self {
716 Self {
717 dead_code: EditorDeadCodeAnalysisOutput::from_engine(output.dead_code),
718 duplication: output.duplication,
719 }
720 }
721}
722
723#[derive(Debug, Default)]
725pub struct EditorAnalysisOutput {
726 pub results: EditorAnalysisResults,
728 pub duplication: EditorDuplicationReport,
730}
731
732impl EditorAnalysisOutput {
733 #[must_use]
735 pub const fn new(results: EditorAnalysisResults, duplication: EditorDuplicationReport) -> Self {
736 Self {
737 results,
738 duplication,
739 }
740 }
741
742 pub fn merge_project_output(&mut self, output: EditorProjectAnalysisOutput) {
744 self.merge_results(output.dead_code.results);
745 self.merge_duplication(output.duplication);
746 }
747
748 pub fn merge_results(&mut self, source: EditorAnalysisResults) {
750 self.results.merge_into(source);
751 }
752
753 pub fn merge_duplication(&mut self, source: EditorDuplicationReport) {
756 self.duplication.clone_groups.extend(source.clone_groups);
757 self.duplication
758 .clone_families
759 .extend(source.clone_families);
760 self.duplication
761 .mirrored_directories
762 .extend(source.mirrored_directories);
763 self.duplication.stats.clone_groups += source.stats.clone_groups;
764 self.duplication.stats.clone_families += source.stats.clone_families;
765 self.duplication.stats.clone_instances += source.stats.clone_instances;
766 self.duplication.stats.total_files += source.stats.total_files;
767 self.duplication.stats.files_with_clones += source.stats.files_with_clones;
768 self.duplication.stats.total_lines += source.stats.total_lines;
769 self.duplication.stats.duplicated_lines += source.stats.duplicated_lines;
770 self.duplication.stats.total_tokens += source.stats.total_tokens;
771 self.duplication.stats.duplicated_tokens += source.stats.duplicated_tokens;
772 self.duplication.stats.clone_groups_below_min_occurrences +=
773 source.stats.clone_groups_below_min_occurrences;
774 self.duplication.stats.clone_groups_ignored += source.stats.clone_groups_ignored;
775 self.duplication.stats.near_candidates_skipped += source.stats.near_candidates_skipped;
776 self.duplication.stats.duplication_percentage = if self.duplication.stats.total_lines > 0 {
777 (self.duplication.stats.duplicated_lines as f64
778 / self.duplication.stats.total_lines as f64)
779 * 100.0
780 } else {
781 0.0
782 };
783 }
784
785 pub fn filter_by_changed_files(&mut self, changed_files: &FxHashSet<PathBuf>, root: &Path) {
791 fallow_engine::changed_files::filter_results_by_changed_files(
792 &mut self.results,
793 changed_files,
794 );
795 fallow_engine::changed_files::filter_duplication_by_changed_files(
796 &mut self.duplication,
797 changed_files,
798 root,
799 );
800 }
801}
802
803#[cfg(test)]
804pub(crate) mod tests {
805 use super::*;
806
807 use fallow_types::duplicates::{CloneFamily, CloneGroup, CloneInstance, DuplicationStats};
808
809 use super::editor_results::{
810 BoundaryViolation, BoundaryViolationFinding, CircularDependency, CircularDependencyFinding,
811 DevDependencyInProduction, DevDependencyInProductionFinding, ExportUsage, SecuritySeverity,
812 TestOnlyDependency, TestOnlyDependencyFinding, TypeOnlyDependency, UnlistedDependency,
813 UnlistedDependencyFinding, UnusedClassMemberFinding, UnusedDependency,
814 UnusedDependencyFinding, UnusedDevDependencyFinding, UnusedEnumMemberFinding, UnusedExport,
815 UnusedExportFinding, UnusedFile, UnusedFileFinding, UnusedMember,
816 UnusedOptionalDependencyFinding, UnusedStoreMemberFinding, UnusedTypeFinding,
817 };
818
819 #[test]
820 fn merges_duplication_stats_and_recomputes_percentage() {
821 let mut output = EditorAnalysisOutput {
822 duplication: EditorDuplicationReport {
823 clone_groups: vec![CloneGroup {
824 instances: vec![CloneInstance {
825 is_symlink: false,
826 file: PathBuf::from("src/a.ts"),
827 start_line: 1,
828 end_line: 4,
829 start_col: 0,
830 end_col: 10,
831 fragment: "const a = 1;".to_string(),
832 }],
833 token_count: 8,
834 line_count: 4,
835 similarity: None,
836 }],
837 clone_families: Vec::new(),
838 mirrored_directories: Vec::new(),
839 stats: DuplicationStats {
840 clone_groups: 1,
841 clone_families: 0,
842 clone_instances: 1,
843 total_files: 1,
844 files_with_clones: 1,
845 total_lines: 20,
846 duplicated_lines: 4,
847 total_tokens: 80,
848 duplicated_tokens: 8,
849 duplication_percentage: 20.0,
850 clone_groups_below_min_occurrences: 1,
851 clone_groups_ignored: 1,
852 near_candidates_skipped: 2,
853 },
854 },
855 ..Default::default()
856 };
857
858 output.merge_duplication(EditorDuplicationReport {
859 clone_groups: Vec::new(),
860 clone_families: Vec::new(),
861 mirrored_directories: Vec::new(),
862 stats: DuplicationStats {
863 clone_groups: 0,
864 clone_families: 0,
865 clone_instances: 0,
866 total_files: 1,
867 files_with_clones: 0,
868 total_lines: 30,
869 duplicated_lines: 6,
870 total_tokens: 120,
871 duplicated_tokens: 12,
872 duplication_percentage: 20.0,
873 clone_groups_below_min_occurrences: 2,
874 clone_groups_ignored: 3,
875 near_candidates_skipped: 4,
876 },
877 });
878
879 assert_eq!(output.duplication.stats.total_lines, 50);
880 assert_eq!(output.duplication.stats.duplicated_lines, 10);
881 assert_eq!(
882 output.duplication.stats.clone_groups_below_min_occurrences,
883 3
884 );
885 assert_eq!(output.duplication.stats.clone_groups_ignored, 4);
886 assert_eq!(output.duplication.stats.near_candidates_skipped, 6);
887 assert!((output.duplication.stats.duplication_percentage - 20.0).abs() < f64::EPSILON);
888 }
889
890 #[test]
891 fn merging_duplication_keeps_the_family_corpus_count_aligned() {
892 let family = |path: &str| CloneFamily {
893 files: vec![PathBuf::from(path)],
894 groups: Vec::new(),
895 total_duplicated_lines: 4,
896 total_duplicated_tokens: 8,
897 suggestions: Vec::new(),
898 };
899 let report = |path: &str, families: usize| EditorDuplicationReport {
900 clone_groups: Vec::new(),
901 clone_families: vec![family(path)],
902 mirrored_directories: Vec::new(),
903 stats: DuplicationStats {
904 clone_families: families,
905 ..DuplicationStats::default()
906 },
907 };
908
909 let mut output = EditorAnalysisOutput {
910 duplication: report("src/a.ts", 3),
911 ..Default::default()
912 };
913 output.merge_duplication(report("src/b.ts", 2));
914
915 assert_eq!(output.duplication.stats.clone_families, 5);
916 assert_eq!(output.duplication.clone_families_shown(), 2);
917 assert_eq!(output.duplication.clone_families_omitted(), 3);
918 assert_eq!(
919 output.duplication.clone_families_total(),
920 output.duplication.stats.clone_families
921 );
922 }
923
924 #[test]
925 fn editor_session_returns_api_owned_project_output() {
926 let temp = tempfile::tempdir().expect("temp project");
927 let root = temp.path();
928 std::fs::create_dir_all(root.join("src")).expect("src dir");
929 std::fs::write(
930 root.join("package.json"),
931 r#"{"name":"editor-api-session","main":"src/index.ts"}"#,
932 )
933 .expect("package.json");
934 std::fs::write(
935 root.join("src/index.ts"),
936 "export const used = 1;\nconsole.log(used);\n",
937 )
938 .expect("source");
939
940 let session = EditorAnalysisSession::load(root, None).expect("session loads");
941 let output = session
942 .analyze_project_with(&fallow_config::DuplicatesConfig::default(), true)
943 .expect("analysis runs");
944
945 assert!(output.dead_code.modules.is_some());
946 assert!(
947 output
948 .dead_code
949 .files
950 .as_ref()
951 .is_some_and(|files| !files.is_empty())
952 );
953 }
954
955 #[test]
957 fn changed_files_scope_runs_after_the_analysis_keeps_all_unused_files() {
958 let temp = tempfile::tempdir().expect("temp project");
959 let root = temp.path().canonicalize().expect("canonical root");
960 let src = root.join("src");
961 std::fs::create_dir_all(&src).expect("src dir");
962 std::fs::write(
963 root.join("package.json"),
964 r#"{"name":"editor-scope-order","main":"src/a.ts"}"#,
965 )
966 .expect("package.json");
967 std::fs::write(src.join("a.ts"), "export const a = 1;\n").expect("source a");
968 std::fs::write(src.join("orphan.ts"), "export const orphan = 1;\n").expect("orphan");
969
970 let session = EditorAnalysisSession::load(&root, None).expect("session loads");
971 let mut changed_files = FxHashSet::default();
972 changed_files.insert(src.join("a.ts"));
973 let mut output = session
974 .analyze_project_with_changed_files(
975 &fallow_config::DuplicatesConfig::default(),
976 false,
977 Some(&changed_files),
978 )
979 .expect("analysis runs");
980 let unused_files = |output: &EditorProjectAnalysisOutput| {
981 output
982 .dead_code
983 .results
984 .unused_files
985 .iter()
986 .map(|finding| finding.file.path.clone())
987 .collect::<Vec<_>>()
988 };
989 assert_eq!(
990 unused_files(&output),
991 vec![src.join("orphan.ts")],
992 "the analysis keeps the unused file outside the changed set"
993 );
994
995 session.apply_change_scope(&mut output, &ChangeScope::changed_files(&changed_files));
996 assert!(
997 unused_files(&output).is_empty(),
998 "the scope removes the unused file outside the changed set: {:?}",
999 unused_files(&output)
1000 );
1001 }
1002
1003 #[test]
1004 fn editor_session_scopes_duplication_to_changed_files() {
1005 let temp = tempfile::tempdir().expect("temp project");
1006 let root = temp.path();
1007 let src = root.join("src");
1008 std::fs::create_dir_all(&src).expect("src dir");
1009 std::fs::write(
1010 root.join("package.json"),
1011 r#"{"name":"editor-api-session","main":"src/a.ts"}"#,
1012 )
1013 .expect("package.json");
1014 let repeated =
1015 "export function repeated() {\n return ['alpha', 'beta', 'gamma'].join(',');\n}\n";
1016 std::fs::write(src.join("a.ts"), repeated).expect("source a");
1017 std::fs::write(src.join("b.ts"), repeated).expect("source b");
1018
1019 let session = EditorAnalysisSession::load(root, None).expect("session loads");
1020 let mut config = session.config().duplicates.clone();
1021 config.min_tokens = 1;
1022 config.min_lines = 1;
1023 let full = session
1024 .analyze_project_with(&config, false)
1025 .expect("analysis runs");
1026 assert!(!full.duplication.clone_groups.is_empty());
1027
1028 let mut changed_files = FxHashSet::default();
1029 changed_files.insert(src.join("unrelated.ts"));
1030 let scoped = session
1031 .analyze_project_with_changed_files(&config, false, Some(&changed_files))
1032 .expect("analysis runs");
1033 assert!(scoped.duplication.clone_groups.is_empty());
1034 }
1035
1036 #[test]
1039 fn inline_complexity_applies_health_threshold_overrides() {
1040 let temp = tempfile::tempdir().expect("temp project");
1041 let root = temp.path();
1042 std::fs::create_dir_all(root.join("src")).expect("src dir");
1043 std::fs::write(
1044 root.join("package.json"),
1045 r#"{"name":"editor-inline-overrides","main":"src/index.ts"}"#,
1046 )
1047 .expect("package.json");
1048 std::fs::write(
1049 root.join(".fallowrc.json"),
1050 r#"{"health":{"maxCyclomatic":2,"maxCognitive":2,"thresholdOverrides":[{"files":["src/legacy.ts"],"maxCyclomatic":50,"maxCognitive":50}]}}"#,
1051 )
1052 .expect("config");
1053 let branchy = |name: &str| {
1054 format!(
1055 "export function {name}(value: number): number {{\n if (value > 1) {{ return 1; }}\n \
1056 if (value > 2) {{ return 2; }}\n if (value > 3) {{ return 3; }}\n return 0;\n}}\n"
1057 )
1058 };
1059 std::fs::write(root.join("src/app.ts"), branchy("appBranchy")).expect("app");
1060 std::fs::write(root.join("src/legacy.ts"), branchy("legacyBranchy")).expect("legacy");
1061 std::fs::write(
1062 root.join("src/index.ts"),
1063 "export { appBranchy } from \"./app\";\nexport { legacyBranchy } from \"./legacy\";\n",
1064 )
1065 .expect("index");
1066
1067 let session = EditorAnalysisSession::load(root, None).expect("session loads");
1068 let output = session
1069 .analyze_project_with(&session.config().duplicates.clone(), true)
1070 .expect("analysis runs");
1071 let names = collect_inline_complexity(session.config(), &output.dead_code)
1072 .into_iter()
1073 .map(|finding| finding.name)
1074 .collect::<Vec<_>>();
1075
1076 assert_eq!(
1077 names,
1078 vec!["appBranchy".to_string()],
1079 "the override raises the ceilings for src/legacy.ts, as in `fallow health`"
1080 );
1081 }
1082
1083 fn make_inline_finding(path: PathBuf) -> EditorInlineComplexityFinding {
1084 EditorInlineComplexityFinding {
1085 path,
1086 name: "myFn".to_string(),
1087 line: 1,
1088 col: 0,
1089 cyclomatic: 5,
1090 cognitive: 4,
1091 exceeded: EditorInlineComplexityExceeded::Cyclomatic,
1092 }
1093 }
1094
1095 #[test]
1096 fn filter_inline_complexity_keeps_findings_in_changed_set() {
1097 let changed: FxHashSet<PathBuf> = [PathBuf::from("/src/a.ts"), PathBuf::from("/src/b.ts")]
1098 .into_iter()
1099 .collect();
1100 let mut findings = vec![
1101 make_inline_finding(PathBuf::from("/src/a.ts")),
1102 make_inline_finding(PathBuf::from("/src/c.ts")),
1103 ];
1104
1105 filter_inline_complexity_by_changed_files(&mut findings, &changed);
1106
1107 assert_eq!(findings.len(), 1);
1108 assert_eq!(
1109 findings[0].path.to_string_lossy().replace('\\', "/"),
1110 "/src/a.ts"
1111 );
1112 }
1113
1114 #[test]
1115 fn filter_inline_complexity_removes_all_when_changed_set_empty() {
1116 let changed: FxHashSet<PathBuf> = FxHashSet::default();
1117 let mut findings = vec![make_inline_finding(PathBuf::from("/src/a.ts"))];
1118
1119 filter_inline_complexity_by_changed_files(&mut findings, &changed);
1120
1121 assert!(
1122 findings.is_empty(),
1123 "empty changed-files set must drop all inline complexity findings"
1124 );
1125 }
1126
1127 #[test]
1128 fn filter_inline_complexity_keeps_all_when_all_in_changed_set() {
1129 let path_a = PathBuf::from("/src/a.ts");
1130 let path_b = PathBuf::from("/src/b.ts");
1131 let changed: FxHashSet<PathBuf> = [path_a.clone(), path_b.clone()].into_iter().collect();
1132 let mut findings = vec![make_inline_finding(path_a), make_inline_finding(path_b)];
1133
1134 filter_inline_complexity_by_changed_files(&mut findings, &changed);
1135
1136 assert_eq!(
1137 findings.len(),
1138 2,
1139 "all findings in the changed set must be retained"
1140 );
1141 }
1142
1143 #[test]
1144 fn editor_session_applies_per_path_rule_overrides() {
1145 let temp = tempfile::tempdir().expect("temp project");
1149 let root = temp.path();
1150 std::fs::create_dir_all(root.join("src/ui")).expect("ui dir");
1151 std::fs::create_dir_all(root.join("src/lib")).expect("lib dir");
1152 std::fs::write(
1153 root.join("package.json"),
1154 r#"{"name":"editor-override-rules","private":true,"main":"src/index.ts"}"#,
1155 )
1156 .expect("package.json");
1157 std::fs::write(
1158 root.join(".fallowrc.json"),
1159 r#"{
1160 "rules": { "unused-exports": "warn", "private-type-leaks": "warn" },
1161 "overrides": [
1162 {
1163 "files": ["src/ui/**"],
1164 "rules": { "unused-exports": "off", "private-type-leaks": "off" }
1165 }
1166 ]
1167}"#,
1168 )
1169 .expect("config");
1170 std::fs::write(
1171 root.join("src/index.ts"),
1172 "import { kitUsed } from './ui/kit';\nimport { libUsed } from './lib/util';\n\nexport const app = `${kitUsed}${libUsed}`;\n",
1173 )
1174 .expect("index");
1175 std::fs::write(
1176 root.join("src/ui/kit.ts"),
1177 "type Props = { label: string };\n\nexport const kitUsed = 'kit';\n\nexport const Unused = (props: Props) => props.label;\n",
1178 )
1179 .expect("kit");
1180 std::fs::write(
1181 root.join("src/lib/util.ts"),
1182 "type Internal = { id: string };\n\nexport const libUsed = 'lib';\n\nexport const alsoUnused = (value: Internal) => value.id;\n",
1183 )
1184 .expect("util");
1185
1186 let session = EditorAnalysisSession::load(root, None).expect("session loads");
1187 let output = session
1188 .analyze_project_with_changed_files(
1189 &fallow_config::DuplicatesConfig::default(),
1190 false,
1191 None,
1192 )
1193 .expect("analysis runs");
1194 let results = &output.dead_code.results;
1195
1196 let unused_export_paths = || {
1197 results
1198 .unused_exports
1199 .iter()
1200 .map(|finding| finding.export.path.clone())
1201 .collect::<Vec<_>>()
1202 };
1203 let leak_paths = || {
1204 results
1205 .private_type_leaks
1206 .iter()
1207 .map(|finding| finding.leak.path.clone())
1208 .collect::<Vec<_>>()
1209 };
1210
1211 assert!(
1212 !unused_export_paths()
1213 .iter()
1214 .any(|path| path.ends_with("kit.ts")),
1215 "the override turns unused-exports off for src/ui/**: {:?}",
1216 unused_export_paths()
1217 );
1218 assert!(
1219 !leak_paths().iter().any(|path| path.ends_with("kit.ts")),
1220 "the override turns private-type-leaks off for src/ui/**: {:?}",
1221 leak_paths()
1222 );
1223 assert!(
1224 unused_export_paths()
1225 .iter()
1226 .any(|path| path.ends_with("util.ts")),
1227 "paths outside the override keep their unused export: {:?}",
1228 unused_export_paths()
1229 );
1230 assert!(
1231 leak_paths().iter().any(|path| path.ends_with("util.ts")),
1232 "paths outside the override keep their private type leak: {:?}",
1233 leak_paths()
1234 );
1235 }
1236
1237 #[test]
1238 fn merge_results_covers_all_fields() {
1239 let mut output = EditorAnalysisOutput::default();
1240 output.merge_results(merge_test_source_with_all_fields());
1241 let target = &output.results;
1242
1243 assert_eq!(target.unused_files.len(), 1);
1244 assert_eq!(target.unused_exports.len(), 1);
1245 assert_eq!(target.unused_types.len(), 1);
1246 assert_eq!(target.private_type_leaks.len(), 1);
1247 assert_eq!(target.deprecated_exports_in_use.len(), 1);
1248 assert_eq!(target.unused_dependencies.len(), 1);
1249 assert_eq!(target.unused_dev_dependencies.len(), 1);
1250 assert_eq!(target.unused_optional_dependencies.len(), 1);
1251 assert_eq!(target.unused_enum_members.len(), 1);
1252 assert_eq!(target.unused_class_members.len(), 1);
1253 assert_eq!(target.unused_store_members.len(), 1);
1254 assert_eq!(target.unresolved_imports.len(), 1);
1255 assert_eq!(target.unlisted_dependencies.len(), 1);
1256 assert_eq!(target.duplicate_exports.len(), 1);
1257 assert_eq!(target.type_only_dependencies.len(), 1);
1258 assert_eq!(target.test_only_dependencies.len(), 1);
1259 assert_eq!(target.circular_dependencies.len(), 1);
1260 assert_eq!(target.re_export_cycles.len(), 1);
1261 assert_eq!(target.package_cycles.len(), 1);
1262 assert_eq!(target.boundary_violations.len(), 1);
1263 assert_eq!(target.boundary_call_violations.len(), 1);
1264 assert_eq!(target.policy_violations.len(), 1);
1265 assert_eq!(target.stale_suppressions.len(), 1);
1266 assert_eq!(target.unused_catalog_entries.len(), 1);
1267 assert_eq!(target.empty_catalog_groups.len(), 1);
1268 assert_eq!(target.unresolved_catalog_references.len(), 1);
1269 assert_eq!(target.unused_dependency_overrides.len(), 1);
1270 assert_eq!(target.misconfigured_dependency_overrides.len(), 1);
1271 assert_eq!(target.invalid_client_exports.len(), 1);
1272 assert_eq!(target.mixed_client_server_barrels.len(), 1);
1273 assert_eq!(target.misplaced_directives.len(), 1);
1274 assert_eq!(target.export_usages.len(), 1);
1275 assert_eq!(target.feature_flags.len(), 1);
1276 assert_eq!(target.security_findings.len(), 1);
1277 assert_eq!(target.security_unresolved_edge_files, 2);
1278 assert_eq!(target.security_unresolved_callee_diagnostics.len(), 1);
1279 assert_eq!(target.suppression_count, 1);
1280 assert_eq!(target.cascade_hidden, 1);
1281 assert_eq!(target.cascade.suppressed_unused_files.len(), 1);
1282 assert!(target.entry_point_summary.is_some());
1283 assert_eq!(
1284 target
1285 .render_fan_in
1286 .as_ref()
1287 .and_then(|m| m.max_distinct_parents),
1288 Some(3)
1289 );
1290 assert_eq!(target.react_component_intel.len(), 1);
1291 assert_eq!(target.dev_dependencies_in_production.len(), 1);
1292 assert_eq!(target.boundary_coverage_violations.len(), 1);
1293 assert_eq!(target.route_collisions.len(), 1);
1294 assert_eq!(target.dynamic_segment_name_conflicts.len(), 1);
1295 assert_eq!(target.unprovided_injects.len(), 1);
1296 assert_eq!(target.unrendered_components.len(), 1);
1297 assert_eq!(target.unused_component_props.len(), 1);
1298 let candidate = &target.absent_component_props[0];
1299 assert_eq!(candidate.prop.component_name, "Widget");
1300 assert_eq!(candidate.prop.prop_name, "highlight");
1301 assert!(candidate.prop.has_default);
1302 assert_eq!(candidate.prop.framework, "vue");
1303 assert_eq!(
1304 candidate.prop.inspected_call_sites[0].path,
1305 PathBuf::from("/main.vue")
1306 );
1307 assert_eq!(candidate.prop.explanation, "Review optional prop contract");
1308
1309 assert_eq!(target.unused_component_emits.len(), 1);
1310 assert_eq!(target.unused_component_inputs.len(), 1);
1311 assert_eq!(target.unused_component_outputs.len(), 1);
1312 assert_eq!(target.unused_svelte_events.len(), 1);
1313 assert_eq!(target.unused_effect_layers.len(), 1);
1314 assert_eq!(target.unused_tagged_errors.len(), 1);
1315 assert_eq!(target.unproduced_catch_tags.len(), 1);
1316 assert_eq!(target.unused_server_actions.len(), 1);
1317 assert_eq!(target.unused_load_data_keys.len(), 1);
1318 assert!(target.unused_load_data_keys_global_abstain);
1319 assert_eq!(target.prop_drilling_chains.len(), 1);
1320 assert_eq!(target.thin_wrappers.len(), 1);
1321 assert_eq!(target.duplicate_prop_shapes.len(), 1);
1322 assert_eq!(target.active_suppressions.len(), 1);
1323 assert_eq!(target.semantic_framework_contracts.len(), 1);
1324 assert!(target.semantic_reachable_files.contains(Path::new("/f.ts")));
1325 assert_eq!(target.security_unresolved_callee_sites, 3);
1326 assert_eq!(target.unused_component_props_exempted, 1);
1327 }
1328
1329 #[test]
1330 fn merge_duplication_recomputes_percentage() {
1331 let target = EditorDuplicationReport {
1332 clone_groups: vec![],
1333 clone_families: vec![],
1334 mirrored_directories: vec![],
1335 stats: DuplicationStats {
1336 total_files: 5,
1337 files_with_clones: 1,
1338 total_lines: 200,
1339 duplicated_lines: 20,
1340 total_tokens: 1000,
1341 duplicated_tokens: 100,
1342 clone_groups: 1,
1343 clone_families: 0,
1344 clone_instances: 2,
1345 duplication_percentage: 10.0, clone_groups_below_min_occurrences: 0,
1347 clone_groups_ignored: 0,
1348 near_candidates_skipped: 0,
1349 },
1350 };
1351 let source = EditorDuplicationReport {
1352 clone_groups: vec![],
1353 clone_families: vec![],
1354 mirrored_directories: vec![],
1355 stats: DuplicationStats {
1356 total_files: 3,
1357 files_with_clones: 1,
1358 total_lines: 300,
1359 duplicated_lines: 60,
1360 total_tokens: 1500,
1361 duplicated_tokens: 300,
1362 clone_groups: 2,
1363 clone_families: 0,
1364 clone_instances: 4,
1365 duplication_percentage: 20.0, clone_groups_below_min_occurrences: 0,
1367 clone_groups_ignored: 0,
1368 near_candidates_skipped: 0,
1369 },
1370 };
1371
1372 let mut output = EditorAnalysisOutput::new(EditorAnalysisResults::default(), target);
1373 output.merge_duplication(source);
1374
1375 let target = &output.duplication;
1376 assert_eq!(target.stats.total_files, 8);
1377 assert_eq!(target.stats.files_with_clones, 2);
1378 assert_eq!(target.stats.total_lines, 500);
1379 assert_eq!(target.stats.duplicated_lines, 80);
1380 assert_eq!(target.stats.total_tokens, 2500);
1381 assert_eq!(target.stats.duplicated_tokens, 400);
1382 assert_eq!(target.stats.clone_groups, 3);
1383 assert_eq!(target.stats.clone_instances, 6);
1384 assert!((target.stats.duplication_percentage - 16.0).abs() < f64::EPSILON);
1385 }
1386
1387 #[test]
1388 fn merge_duplication_zero_total_lines_yields_zero_percentage() {
1389 let mut output = EditorAnalysisOutput::default();
1390
1391 output.merge_duplication(EditorDuplicationReport::default());
1392
1393 let target = &output.duplication;
1394
1395 assert_eq!(target.stats.total_lines, 0);
1396 assert!((target.stats.duplication_percentage - 0.0).abs() < f64::EPSILON);
1397 }
1398
1399 fn merge_test_unused_export(
1400 path: &str,
1401 export_name: &str,
1402 is_type_only: bool,
1403 line: u32,
1404 ) -> UnusedExport {
1405 UnusedExport {
1406 path: path.into(),
1407 export_name: export_name.to_string(),
1408 is_type_only,
1409 line,
1410 col: 0,
1411 span_start: 0,
1412 is_re_export: false,
1413 deprecated: false,
1414 deprecated_reason: None,
1415 }
1416 }
1417
1418 fn merge_test_unused_dependency(
1419 package_name: &str,
1420 location: super::editor_results::DependencyLocation,
1421 line: u32,
1422 ) -> UnusedDependency {
1423 UnusedDependency {
1424 package_name: package_name.to_string(),
1425 location,
1426 path: "/pkg.json".into(),
1427 line,
1428 used_in_workspaces: Vec::new(),
1429 declared_and_imported_in: Vec::new(),
1430 }
1431 }
1432
1433 fn merge_test_unused_member(
1434 parent_name: &str,
1435 member_name: &str,
1436 kind: super::editor_extract::MemberKind,
1437 line: u32,
1438 ) -> UnusedMember {
1439 UnusedMember {
1440 path: "/f.ts".into(),
1441 parent_name: parent_name.to_string(),
1442 member_name: member_name.to_string(),
1443 kind,
1444 line,
1445 col: 0,
1446 }
1447 }
1448
1449 #[expect(
1450 clippy::too_many_lines,
1451 reason = "intentionally names every EditorAnalysisResults field (no ..Default::default()) so a new field is a compile error here; see #444"
1452 )]
1453 pub fn merge_test_source_with_all_fields() -> EditorAnalysisResults {
1454 EditorAnalysisResults {
1455 unused_files: vec![UnusedFileFinding::with_actions(UnusedFile {
1456 path: "/f.ts".into(),
1457 })],
1458 unused_exports: vec![UnusedExportFinding::with_actions(merge_test_unused_export(
1459 "/f.ts", "e", false, 1,
1460 ))],
1461 unused_types: vec![UnusedTypeFinding::with_actions(merge_test_unused_export(
1462 "/f.ts", "T", true, 2,
1463 ))],
1464 unused_dependencies: vec![UnusedDependencyFinding::with_actions(
1465 merge_test_unused_dependency(
1466 "dep",
1467 super::editor_results::DependencyLocation::Dependencies,
1468 3,
1469 ),
1470 )],
1471 unused_dev_dependencies: vec![UnusedDevDependencyFinding::with_actions(
1472 merge_test_unused_dependency(
1473 "dev-dep",
1474 super::editor_results::DependencyLocation::DevDependencies,
1475 4,
1476 ),
1477 )],
1478 unused_optional_dependencies: vec![UnusedOptionalDependencyFinding::with_actions(
1479 merge_test_unused_dependency(
1480 "opt-dep",
1481 super::editor_results::DependencyLocation::OptionalDependencies,
1482 5,
1483 ),
1484 )],
1485 unused_enum_members: vec![UnusedEnumMemberFinding::with_actions(
1486 merge_test_unused_member(
1487 "E",
1488 "A",
1489 super::editor_extract::MemberKind::EnumMember,
1490 6,
1491 ),
1492 )],
1493 unused_class_members: vec![UnusedClassMemberFinding::with_actions(
1494 merge_test_unused_member(
1495 "C",
1496 "m",
1497 super::editor_extract::MemberKind::ClassMethod,
1498 7,
1499 ),
1500 )],
1501 unused_store_members: vec![UnusedStoreMemberFinding::with_actions(
1502 merge_test_unused_member(
1503 "S",
1504 "a",
1505 super::editor_extract::MemberKind::StoreMember,
1506 7,
1507 ),
1508 )],
1509 unresolved_imports: vec![
1510 super::editor_results::UnresolvedImportFinding::with_actions(
1511 super::editor_results::UnresolvedImport {
1512 path: "/f.ts".into(),
1513 specifier: "./gone".to_string(),
1514 line: 8,
1515 col: 0,
1516 specifier_col: 10,
1517 },
1518 ),
1519 ],
1520 unlisted_dependencies: vec![UnlistedDependencyFinding::with_actions(
1521 UnlistedDependency {
1522 package_name: "unlisted".to_string(),
1523 imported_from: vec![],
1524 },
1525 )],
1526 duplicate_exports: vec![super::editor_results::DuplicateExportFinding::with_actions(
1527 super::editor_results::DuplicateExport {
1528 export_name: "dup".to_string(),
1529 locations: vec![],
1530 },
1531 )],
1532 type_only_dependencies: vec![
1533 super::editor_results::TypeOnlyDependencyFinding::with_actions(
1534 TypeOnlyDependency {
1535 package_name: "type-only".to_string(),
1536 path: "/pkg.json".into(),
1537 line: 9,
1538 },
1539 ),
1540 ],
1541 circular_dependencies: vec![CircularDependencyFinding::with_actions(
1542 CircularDependency {
1543 files: vec!["/a.ts".into(), "/b.ts".into()],
1544 length: 2,
1545 line: 10,
1546 col: 0,
1547 edges: Vec::new(),
1548 is_cross_package: false,
1549 },
1550 )],
1551 test_only_dependencies: vec![TestOnlyDependencyFinding::with_actions(
1552 TestOnlyDependency {
1553 package_name: "test-only".to_string(),
1554 path: "/pkg.json".into(),
1555 line: 11,
1556 },
1557 )],
1558 dev_dependencies_in_production: vec![DevDependencyInProductionFinding::with_actions(
1559 DevDependencyInProduction {
1560 package_name: "dev-in-prod".to_string(),
1561 path: "/pkg.json".into(),
1562 line: 12,
1563 },
1564 )],
1565 boundary_violations: vec![BoundaryViolationFinding::with_actions(BoundaryViolation {
1566 from_path: "/a.ts".into(),
1567 to_path: "/b.ts".into(),
1568 from_zone: "ui".to_string(),
1569 to_zone: "data".to_string(),
1570 import_specifier: "../data/db".to_string(),
1571 line: 12,
1572 col: 0,
1573 via_path: None,
1574 })],
1575 boundary_coverage_violations: vec![
1576 super::editor_results::BoundaryCoverageViolationFinding::with_actions(
1577 super::editor_results::BoundaryCoverageViolation {
1578 path: "/unzoned.ts".into(),
1579 line: 13,
1580 col: 0,
1581 },
1582 ),
1583 ],
1584 boundary_call_violations: vec![
1585 super::editor_results::BoundaryCallViolationFinding::with_actions(
1586 super::editor_results::BoundaryCallViolation {
1587 path: "/zoned.ts".into(),
1588 line: 14,
1589 col: 0,
1590 zone: "domain".to_string(),
1591 callee: "console.log".to_string(),
1592 pattern: "console.*".to_string(),
1593 },
1594 ),
1595 ],
1596 policy_violations: vec![super::editor_results::PolicyViolationFinding::with_actions(
1597 super::editor_results::PolicyViolation {
1598 path: "/zoned.ts".into(),
1599 line: 15,
1600 col: 0,
1601 pack: "team-policy".to_string(),
1602 rule_id: "no-console".to_string(),
1603 kind: super::editor_results::PolicyRuleKind::BannedCall,
1604 matched: "console.log".to_string(),
1605 severity: super::editor_results::PolicyViolationSeverity::Warn,
1606 message: None,
1607 },
1608 )],
1609 export_usages: vec![ExportUsage {
1610 path: "/f.ts".into(),
1611 export_name: "used".to_string(),
1612 line: 15,
1613 col: 0,
1614 reference_count: 3,
1615 reference_locations: vec![],
1616 }],
1617 private_type_leaks: vec![super::editor_results::PrivateTypeLeakFinding::with_actions(
1618 super::editor_results::PrivateTypeLeak {
1619 path: "/f.ts".into(),
1620 export_name: "pub_fn".to_string(),
1621 type_name: "Secret".to_string(),
1622 line: 14,
1623 col: 0,
1624 span_start: 0,
1625 semantic: None,
1626 },
1627 )],
1628 deprecated_exports_in_use: vec![
1629 super::editor_results::DeprecatedExportInUseFinding::with_actions(
1630 super::editor_results::DeprecatedExportInUse {
1631 path: "/f.ts".into(),
1632 export_name: "old".to_string(),
1633 is_type_only: false,
1634 line: 16,
1635 col: 0,
1636 span_start: 0,
1637 deprecated_reason: None,
1638 consumer_count: 1,
1639 consumers: vec![super::editor_results::DeprecatedExportConsumer {
1640 path: "/g.ts".into(),
1641 line: 1,
1642 col: 0,
1643 kind: super::editor_results::DeprecatedConsumerKind::NamedImport,
1644 }],
1645 public_api: false,
1646 },
1647 ),
1648 ],
1649 re_export_cycles: vec![super::editor_results::ReExportCycleFinding::with_actions(
1650 super::editor_results::ReExportCycle {
1651 files: vec!["/barrel.ts".into()],
1652 kind: super::editor_results::ReExportCycleKind::SelfLoop,
1653 },
1654 )],
1655 package_cycles: vec![super::editor_results::PackageCycleFinding::with_actions(
1656 super::editor_results::PackageCycle {
1657 packages: vec!["a".into(), "b".into()],
1658 package_roots: Vec::new(),
1659 length: 2,
1660 edges: vec![super::editor_results::PackageCycleEdge {
1661 from_package: "a".into(),
1662 to_package: "b".into(),
1663 path: "/a/x.ts".into(),
1664 target_path: "/b/y.ts".into(),
1665 line: 1,
1666 col: 0,
1667 type_only: false,
1668 }],
1669 group_truncated: false,
1670 },
1671 )],
1672 stale_suppressions: vec![super::editor_results::StaleSuppression {
1673 finding_id: None,
1674 path: "/f.ts".into(),
1675 line: 15,
1676 col: 0,
1677 origin: super::editor_results::SuppressionOrigin::Comment {
1678 issue_kind: None,
1679 reason: None,
1680 is_file_level: false,
1681 kind_known: true,
1682 },
1683 missing_reason: false,
1684 actions: super::editor_results::StaleSuppression::actions_for(false),
1685 effective_severity: None,
1686 }],
1687 unused_catalog_entries: vec![
1688 super::editor_results::UnusedCatalogEntryFinding::with_actions(
1689 super::editor_results::UnusedCatalogEntry {
1690 entry_name: "react".to_string(),
1691 catalog_name: "default".to_string(),
1692 path: "/pnpm-workspace.yaml".into(),
1693 line: 16,
1694 hardcoded_consumers: vec![],
1695 },
1696 ),
1697 ],
1698 empty_catalog_groups: vec![
1699 super::editor_results::EmptyCatalogGroupFinding::with_actions(
1700 super::editor_results::EmptyCatalogGroup {
1701 catalog_name: "ui".to_string(),
1702 path: "/pnpm-workspace.yaml".into(),
1703 line: 17,
1704 },
1705 ),
1706 ],
1707 unresolved_catalog_references: vec![
1708 super::editor_results::UnresolvedCatalogReferenceFinding::with_actions(
1709 super::editor_results::UnresolvedCatalogReference {
1710 entry_name: "vue".to_string(),
1711 catalog_name: "default".to_string(),
1712 path: "/pkg.json".into(),
1713 line: 18,
1714 available_in_catalogs: vec![],
1715 },
1716 ),
1717 ],
1718 unused_dependency_overrides: vec![
1719 super::editor_results::UnusedDependencyOverrideFinding::with_actions(
1720 super::editor_results::UnusedDependencyOverride {
1721 raw_key: "react".to_string(),
1722 target_package: "react".to_string(),
1723 parent_package: None,
1724 version_constraint: None,
1725 version_range: "18".to_string(),
1726 source: super::editor_results::DependencyOverrideSource::PnpmWorkspaceYaml,
1727 path: "/pnpm-workspace.yaml".into(),
1728 line: 19,
1729 hint: None,
1730 },
1731 ),
1732 ],
1733 misconfigured_dependency_overrides: vec![
1734 super::editor_results::MisconfiguredDependencyOverrideFinding::with_actions(
1735 super::editor_results::MisconfiguredDependencyOverride {
1736 raw_key: "bad>".to_string(),
1737 target_package: None,
1738 raw_value: String::new(),
1739 reason:
1740 super::editor_results::DependencyOverrideMisconfigReason::EmptyValue,
1741 source: super::editor_results::DependencyOverrideSource::PnpmPackageJson,
1742 path: "/pkg.json".into(),
1743 line: 20,
1744 },
1745 ),
1746 ],
1747 invalid_client_exports: vec![
1748 super::editor_results::InvalidClientExportFinding::with_actions(
1749 super::editor_results::InvalidClientExport {
1750 path: "/app/page.tsx".into(),
1751 export_name: "metadata".to_string(),
1752 directive: "use client".to_string(),
1753 line: 22,
1754 col: 0,
1755 },
1756 ),
1757 ],
1758 mixed_client_server_barrels: vec![
1759 super::editor_results::MixedClientServerBarrelFinding::with_actions(
1760 super::editor_results::MixedClientServerBarrel {
1761 path: "/app/components/index.ts".into(),
1762 client_origin: "./Button".to_string(),
1763 server_origin: "./fetchUser".to_string(),
1764 line: 23,
1765 col: 0,
1766 },
1767 ),
1768 ],
1769 misplaced_directives: vec![
1770 super::editor_results::MisplacedDirectiveFinding::with_actions(
1771 super::editor_results::MisplacedDirective {
1772 path: "/app/widget.tsx".into(),
1773 directive: "use client".to_string(),
1774 line: 24,
1775 col: 0,
1776 },
1777 ),
1778 ],
1779 unprovided_injects: vec![
1780 super::editor_results::UnprovidedInjectFinding::with_actions(
1781 super::editor_results::UnprovidedInject {
1782 path: "/Comp.vue".into(),
1783 key_name: "ApiKey".to_string(),
1784 framework: "vue".to_string(),
1785 line: 25,
1786 col: 0,
1787 },
1788 ),
1789 ],
1790 unrendered_components: vec![
1791 super::editor_results::UnrenderedComponentFinding::with_actions(
1792 super::editor_results::UnrenderedComponent {
1793 path: "/Widget.vue".into(),
1794 component_name: "Widget".to_string(),
1795 framework: "vue".to_string(),
1796 reachable_via: None,
1797 line: 26,
1798 col: 0,
1799 },
1800 ),
1801 ],
1802 unused_component_props: vec![
1803 super::editor_results::UnusedComponentPropFinding::with_actions(
1804 super::editor_results::UnusedComponentProp {
1805 path: "/Widget.vue".into(),
1806 component_name: "Widget".to_string(),
1807 prop_name: "size".to_string(),
1808 line: 27,
1809 col: 0,
1810 },
1811 ),
1812 ],
1813 absent_component_props: vec![
1814 super::editor_results::AbsentComponentPropFinding::with_actions(
1815 super::editor_results::AbsentComponentProp {
1816 path: "/Widget.vue".into(),
1817 component_name: "Widget".to_string(),
1818 framework: "vue".to_string(),
1819 prop_name: "highlight".to_string(),
1820 line: 27,
1821 col: 0,
1822 has_default: true,
1823 inspected_call_sites: vec![super::editor_results::ComponentPropCallSite {
1824 path: "/main.vue".into(),
1825 line: 1,
1826 col: 0,
1827 }],
1828 explanation: "Review optional prop contract".to_string(),
1829 },
1830 ),
1831 ],
1832 unused_component_emits: vec![
1833 super::editor_results::UnusedComponentEmitFinding::with_actions(
1834 super::editor_results::UnusedComponentEmit {
1835 path: "/Widget.vue".into(),
1836 component_name: "Widget".to_string(),
1837 emit_name: "change".to_string(),
1838 line: 28,
1839 col: 0,
1840 },
1841 ),
1842 ],
1843 unused_component_inputs: vec![
1844 super::editor_results::UnusedComponentInputFinding::with_actions(
1845 super::editor_results::UnusedComponentInput {
1846 path: "/widget.component.ts".into(),
1847 component_name: "WidgetComponent".to_string(),
1848 input_name: "size".to_string(),
1849 line: 29,
1850 col: 0,
1851 },
1852 ),
1853 ],
1854 unused_component_outputs: vec![
1855 super::editor_results::UnusedComponentOutputFinding::with_actions(
1856 super::editor_results::UnusedComponentOutput {
1857 path: "/widget.component.ts".into(),
1858 component_name: "WidgetComponent".to_string(),
1859 output_name: "change".to_string(),
1860 line: 30,
1861 col: 0,
1862 },
1863 ),
1864 ],
1865 unused_svelte_events: vec![
1866 super::editor_results::UnusedSvelteEventFinding::with_actions(
1867 super::editor_results::UnusedSvelteEvent {
1868 path: "/Child.svelte".into(),
1869 component_name: "Child".to_string(),
1870 event_name: "dead".to_string(),
1871 line: 31,
1872 col: 0,
1873 },
1874 ),
1875 ],
1876 unused_effect_layers: vec![
1877 super::editor_results::UnusedEffectLayerFinding::with_actions(
1878 super::editor_results::UnusedEffectLayer {
1879 path: "/services.ts".into(),
1880 name: "CacheLive".to_string(),
1881 kind: super::editor_results::EffectLayerKind::TopLevel,
1882 line: 33,
1883 col: 0,
1884 dead_consumers: vec!["OrphanLive".to_string()],
1885 },
1886 ),
1887 ],
1888 unused_tagged_errors: vec![
1889 super::editor_results::UnusedTaggedErrorFinding::with_actions(
1890 super::editor_results::UnusedTaggedError {
1891 path: "/errors.ts".into(),
1892 name: "Expired".to_string(),
1893 tag: "Expired".to_string(),
1894 line: 34,
1895 col: 0,
1896 catch_sites: Vec::new(),
1897 },
1898 ),
1899 ],
1900 unproduced_catch_tags: vec![
1901 super::editor_results::UnproducedCatchTagFinding::with_actions(
1902 super::editor_results::UnproducedCatchTag {
1903 path: "/main.ts".into(),
1904 tag: "Expired".to_string(),
1905 line: 35,
1906 col: 0,
1907 candidate_classes: vec!["Expired".to_string()],
1908 },
1909 ),
1910 ],
1911 unused_server_actions: vec![
1912 super::editor_results::UnusedServerActionFinding::with_actions(
1913 super::editor_results::UnusedServerAction {
1914 path: "/app/actions.ts".into(),
1915 action_name: "createUser".to_string(),
1916 line: 32,
1917 col: 0,
1918 },
1919 ),
1920 ],
1921 unused_load_data_keys: vec![
1922 super::editor_results::UnusedLoadDataKeyFinding::with_actions(
1923 super::editor_results::UnusedLoadDataKey {
1924 path: "/src/routes/blog/+page.server.ts".into(),
1925 key_name: "posts".to_string(),
1926 line: 33,
1927 col: 0,
1928 route_dir: None,
1929 },
1930 ),
1931 ],
1932 unused_load_data_keys_global_abstain: true,
1933 prop_drilling_chains: vec![
1934 super::editor_results::PropDrillingChainFinding::with_actions(
1935 super::editor_results::PropDrillingChain {
1936 prop: "user".to_string(),
1937 depth: 1,
1938 hops: vec![super::editor_results::PropDrillHop {
1939 file: "/Hop.tsx".into(),
1940 line: 34,
1941 component: "Hop".to_string(),
1942 }],
1943 },
1944 ),
1945 ],
1946 thin_wrappers: vec![super::editor_results::ThinWrapperFinding::with_actions(
1947 super::editor_results::ThinWrapper {
1948 file: "/Wrapper.tsx".into(),
1949 line: 35,
1950 component: "Wrapper".to_string(),
1951 child_component: "Child".to_string(),
1952 },
1953 )],
1954 duplicate_prop_shapes: vec![
1955 super::editor_results::DuplicatePropShapeFinding::with_actions(
1956 super::editor_results::DuplicatePropShape {
1957 file: "/Card.tsx".into(),
1958 line: 36,
1959 component: "Card".to_string(),
1960 shape: vec!["subtitle".to_string(), "title".to_string()],
1961 group_size: 2,
1962 sharing_components: vec![],
1963 },
1964 ),
1965 ],
1966 route_collisions: vec![super::editor_results::RouteCollisionFinding::with_actions(
1967 super::editor_results::RouteCollision {
1968 path: "/app/(a)/about/page.tsx".into(),
1969 url: "/about".to_string(),
1970 conflicting_paths: vec!["/app/(b)/about/page.tsx".into()],
1971 line: 1,
1972 col: 0,
1973 },
1974 )],
1975 dynamic_segment_name_conflicts: vec![
1976 super::editor_results::DynamicSegmentNameConflictFinding::with_actions(
1977 super::editor_results::DynamicSegmentNameConflict {
1978 path: "/app/shop/[id]/page.tsx".into(),
1979 position: "/shop".to_string(),
1980 conflicting_segments: vec!["[id]".to_string(), "[slug]".to_string()],
1981 conflicting_paths: vec!["/app/shop/[slug]/edit/page.tsx".into()],
1982 line: 1,
1983 col: 0,
1984 },
1985 ),
1986 ],
1987 cascade_hidden: 1,
1988 cascade: fallow_types::results::CascadeState {
1989 suppressed_unused_files: vec!["/f.ts".into()],
1990 ..fallow_types::results::CascadeState::default()
1991 },
1992 suppression_count: 1,
1993 unused_component_props_exempted: 1,
1994 active_suppressions: vec![super::editor_results::ActiveSuppression {
1995 path: "/f.ts".into(),
1996 kind: Some("unused-export".to_string()),
1997 is_file_level: false,
1998 reason: None,
1999 comment_line: 37,
2000 }],
2001 feature_flags: vec![super::editor_results::FeatureFlag {
2002 path: "/f.ts".into(),
2003 flag_name: "ENABLE_X".to_string(),
2004 kind: super::editor_results::FlagKind::EnvironmentVariable,
2005 confidence: super::editor_results::FlagConfidence::High,
2006 line: 21,
2007 col: 0,
2008 guard_span_start: None,
2009 guard_span_end: None,
2010 sdk_name: None,
2011 guard_line_start: None,
2012 guard_line_end: None,
2013 guarded_dead_exports: vec![],
2014 }],
2015 entry_point_summary: Some(super::editor_results::EntryPointSummary {
2016 total: 0,
2017 by_source: vec![],
2018 }),
2019 security_findings: vec![super::editor_results::SecurityFinding {
2020 finding_id: String::new(),
2021 candidate: super::editor_results::SecurityCandidate::default(),
2022 taint_flow: None,
2023 attack_surface: None,
2024 kind: super::editor_results::SecurityFindingKind::ClientServerLeak,
2025 category: None,
2026 cwe: None,
2027 path: "/client.tsx".into(),
2028 line: 1,
2029 col: 0,
2030 evidence: "transitively reaches DATABASE_URL".to_string(),
2031 source_backed: false,
2032 source_read: None,
2033 severity: SecuritySeverity::Low,
2034 trace: vec![],
2035 actions: vec![],
2036 dead_code: None,
2037 reachability: None,
2038 runtime: None,
2039 }],
2040 security_unresolved_edge_files: 2,
2041 security_unresolved_callee_sites: 3,
2042 security_unresolved_callee_diagnostics: vec![
2043 super::editor_results::SecurityUnresolvedCalleeDiagnostic {
2044 path: "/client.tsx".into(),
2045 line: 2,
2046 col: 0,
2047 reason: super::editor_extract::SkippedSecurityCalleeReason::DynamicDispatch,
2048 expression_kind:
2049 super::editor_extract::SkippedSecurityCalleeExpressionKind::Other,
2050 },
2051 ],
2052 render_fan_in: Some(super::editor_results::RenderFanInMetric {
2053 per_component: vec![super::editor_results::RenderFanInComponent {
2054 file: "/Button.tsx".into(),
2055 component: "Button".to_string(),
2056 render_sites: 6,
2057 distinct_parents: 3,
2058 }],
2059 p95_distinct_parents: Some(3),
2060 high_pct: Some(0.0),
2061 max_distinct_parents: Some(3),
2062 }),
2063 react_component_intel: vec![super::editor_results::ReactComponentIntel {
2064 path: "/Button.tsx".into(),
2065 component_name: "Button".to_string(),
2066 anchor_line: 1,
2067 anchor_col: 0,
2068 render_sites: 6,
2069 distinct_parents: 3,
2070 prop_count: 1,
2071 hooks: super::editor_results::ReactHookSummary::default(),
2072 props: Vec::new(),
2073 }],
2074 semantic_reachable_files: std::iter::once("/f.ts".into()).collect(),
2075 semantic_framework_contracts: vec![fallow_types::semantic::SemanticFrameworkContract {
2076 framework: "lit".to_string(),
2077 package: "lit".to_string(),
2078 heritage_symbol: "LitElement".to_string(),
2079 heritage_names: vec!["LitElement".to_string()],
2080 relation: fallow_types::semantic::SemanticFrameworkRelation::Extends,
2081 members: vec!["render".to_string()],
2082 }],
2083 }
2084 }
2085}