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(
633 &self,
634 duplicates_config: &fallow_config::DuplicatesConfig,
635 retain_complexity_artifacts: bool,
636 changed_files: Option<&FxHashSet<PathBuf>>,
637 ) -> fallow_engine::EngineResult<EditorProjectAnalysisOutput> {
638 self.inner
639 .analyze_project_with_artifacts(
640 duplicates_config,
641 fallow_engine::project_analysis::ProjectAnalysisArtifactOptions {
642 retain_complexity_artifacts,
643 changed_files: changed_files.cloned(),
644 ..fallow_engine::project_analysis::ProjectAnalysisArtifactOptions::default()
645 },
646 )
647 .map(fallow_engine::project_analysis::ProjectAnalysisArtifacts::into_output)
648 .map(EditorProjectAnalysisOutput::from_engine)
649 .map(|output| self.with_resolved_rule_severities(output))
650 }
651
652 pub fn apply_change_scope(
663 &self,
664 output: &mut EditorProjectAnalysisOutput,
665 scope: &ChangeScope,
666 ) {
667 fallow_engine::dead_code::apply_scope(
668 &mut output.dead_code.results,
669 &fallow_engine::dead_code::DeadCodeScope {
670 workspace_roots: None,
671 changes: Some(scope),
672 diff: None,
673 files: None,
674 },
675 self.inner.config(),
676 );
677 fallow_engine::duplicates::apply_scope(
678 &mut output.duplication,
679 &fallow_engine::duplicates::DuplicationScope {
680 changes: Some(scope),
681 diff: None,
682 workspace_roots: None,
683 },
684 &self.inner.config().root,
685 );
686 }
687
688 fn with_resolved_rule_severities(
695 &self,
696 mut output: EditorProjectAnalysisOutput,
697 ) -> EditorProjectAnalysisOutput {
698 fallow_engine::dead_code::apply_rule_severities(
699 &mut output.dead_code.results,
700 self.inner.config(),
701 );
702 output
703 }
704
705 const fn from_engine(inner: fallow_engine::session::AnalysisSession) -> Self {
706 Self { inner }
707 }
708}
709
710#[derive(Debug)]
712pub struct EditorProjectAnalysisOutput {
713 pub dead_code: EditorDeadCodeAnalysisOutput,
715 pub duplication: EditorDuplicationReport,
717}
718
719impl EditorProjectAnalysisOutput {
720 fn from_engine(output: fallow_engine::project_analysis::ProjectAnalysisOutput) -> Self {
721 Self {
722 dead_code: EditorDeadCodeAnalysisOutput::from_engine(output.dead_code),
723 duplication: output.duplication,
724 }
725 }
726}
727
728#[derive(Debug, Default)]
730pub struct EditorAnalysisOutput {
731 pub results: EditorAnalysisResults,
733 pub duplication: EditorDuplicationReport,
735}
736
737impl EditorAnalysisOutput {
738 #[must_use]
740 pub const fn new(results: EditorAnalysisResults, duplication: EditorDuplicationReport) -> Self {
741 Self {
742 results,
743 duplication,
744 }
745 }
746
747 pub fn merge_project_output(&mut self, output: EditorProjectAnalysisOutput) {
749 self.merge_results(output.dead_code.results);
750 self.merge_duplication(output.duplication);
751 }
752
753 pub fn merge_results(&mut self, source: EditorAnalysisResults) {
755 self.results.merge_into(source);
756 }
757
758 pub fn merge_duplication(&mut self, source: EditorDuplicationReport) {
761 self.duplication.clone_groups.extend(source.clone_groups);
762 self.duplication
763 .clone_families
764 .extend(source.clone_families);
765 self.duplication
766 .mirrored_directories
767 .extend(source.mirrored_directories);
768 self.duplication.stats.clone_groups += source.stats.clone_groups;
769 self.duplication.stats.clone_families += source.stats.clone_families;
770 self.duplication.stats.clone_instances += source.stats.clone_instances;
771 self.duplication.stats.total_files += source.stats.total_files;
772 self.duplication.stats.files_with_clones += source.stats.files_with_clones;
773 self.duplication.stats.total_lines += source.stats.total_lines;
774 self.duplication.stats.duplicated_lines += source.stats.duplicated_lines;
775 self.duplication.stats.total_tokens += source.stats.total_tokens;
776 self.duplication.stats.duplicated_tokens += source.stats.duplicated_tokens;
777 self.duplication.stats.clone_groups_below_min_occurrences +=
778 source.stats.clone_groups_below_min_occurrences;
779 self.duplication.stats.clone_groups_ignored += source.stats.clone_groups_ignored;
780 self.duplication.stats.near_candidates_skipped += source.stats.near_candidates_skipped;
781 self.duplication.stats.duplication_percentage = if self.duplication.stats.total_lines > 0 {
782 (self.duplication.stats.duplicated_lines as f64
783 / self.duplication.stats.total_lines as f64)
784 * 100.0
785 } else {
786 0.0
787 };
788 }
789
790 pub fn filter_by_changed_files(&mut self, changed_files: &FxHashSet<PathBuf>, root: &Path) {
799 fallow_engine::changed_files::filter_results_by_changed_files(
800 &mut self.results,
801 changed_files,
802 );
803 fallow_engine::changed_files::filter_duplication_by_changed_files(
804 &mut self.duplication,
805 changed_files,
806 root,
807 );
808 }
809}
810
811#[cfg(test)]
812pub(crate) mod tests {
813 use super::*;
814
815 use fallow_types::duplicates::{CloneFamily, CloneGroup, CloneInstance, DuplicationStats};
816
817 use super::editor_results::{
818 BoundaryViolation, BoundaryViolationFinding, CircularDependency, CircularDependencyFinding,
819 DevDependencyInProduction, DevDependencyInProductionFinding, ExportUsage, SecuritySeverity,
820 TestOnlyDependency, TestOnlyDependencyFinding, TypeOnlyDependency, UnlistedDependency,
821 UnlistedDependencyFinding, UnusedClassMemberFinding, UnusedDependency,
822 UnusedDependencyFinding, UnusedDevDependencyFinding, UnusedEnumMemberFinding, UnusedExport,
823 UnusedExportFinding, UnusedFile, UnusedFileFinding, UnusedMember,
824 UnusedOptionalDependencyFinding, UnusedStoreMemberFinding, UnusedTypeFinding,
825 };
826
827 #[test]
828 fn merges_duplication_stats_and_recomputes_percentage() {
829 let mut output = EditorAnalysisOutput {
830 duplication: EditorDuplicationReport {
831 clone_groups: vec![CloneGroup {
832 instances: vec![CloneInstance {
833 is_symlink: false,
834 file: PathBuf::from("src/a.ts"),
835 start_line: 1,
836 end_line: 4,
837 start_col: 0,
838 end_col: 10,
839 fragment: "const a = 1;".to_string(),
840 }],
841 token_count: 8,
842 line_count: 4,
843 similarity: None,
844 }],
845 clone_families: Vec::new(),
846 mirrored_directories: Vec::new(),
847 stats: DuplicationStats {
848 clone_groups: 1,
849 clone_families: 0,
850 clone_instances: 1,
851 total_files: 1,
852 files_with_clones: 1,
853 total_lines: 20,
854 duplicated_lines: 4,
855 total_tokens: 80,
856 duplicated_tokens: 8,
857 duplication_percentage: 20.0,
858 clone_groups_below_min_occurrences: 1,
859 clone_groups_ignored: 1,
860 near_candidates_skipped: 2,
861 },
862 },
863 ..Default::default()
864 };
865
866 output.merge_duplication(EditorDuplicationReport {
867 clone_groups: Vec::new(),
868 clone_families: Vec::new(),
869 mirrored_directories: Vec::new(),
870 stats: DuplicationStats {
871 clone_groups: 0,
872 clone_families: 0,
873 clone_instances: 0,
874 total_files: 1,
875 files_with_clones: 0,
876 total_lines: 30,
877 duplicated_lines: 6,
878 total_tokens: 120,
879 duplicated_tokens: 12,
880 duplication_percentage: 20.0,
881 clone_groups_below_min_occurrences: 2,
882 clone_groups_ignored: 3,
883 near_candidates_skipped: 4,
884 },
885 });
886
887 assert_eq!(output.duplication.stats.total_lines, 50);
888 assert_eq!(output.duplication.stats.duplicated_lines, 10);
889 assert_eq!(
890 output.duplication.stats.clone_groups_below_min_occurrences,
891 3
892 );
893 assert_eq!(output.duplication.stats.clone_groups_ignored, 4);
894 assert_eq!(output.duplication.stats.near_candidates_skipped, 6);
895 assert!((output.duplication.stats.duplication_percentage - 20.0).abs() < f64::EPSILON);
896 }
897
898 #[test]
899 fn merging_duplication_keeps_the_family_corpus_count_aligned() {
900 let family = |path: &str| CloneFamily {
901 files: vec![PathBuf::from(path)],
902 groups: Vec::new(),
903 total_duplicated_lines: 4,
904 total_duplicated_tokens: 8,
905 suggestions: Vec::new(),
906 };
907 let report = |path: &str, families: usize| EditorDuplicationReport {
908 clone_groups: Vec::new(),
909 clone_families: vec![family(path)],
910 mirrored_directories: Vec::new(),
911 stats: DuplicationStats {
912 clone_families: families,
913 ..DuplicationStats::default()
914 },
915 };
916
917 let mut output = EditorAnalysisOutput {
918 duplication: report("src/a.ts", 3),
919 ..Default::default()
920 };
921 output.merge_duplication(report("src/b.ts", 2));
922
923 assert_eq!(output.duplication.stats.clone_families, 5);
924 assert_eq!(output.duplication.clone_families_shown(), 2);
925 assert_eq!(output.duplication.clone_families_omitted(), 3);
926 assert_eq!(
927 output.duplication.clone_families_total(),
928 output.duplication.stats.clone_families
929 );
930 }
931
932 #[test]
933 fn editor_session_returns_api_owned_project_output() {
934 let temp = tempfile::tempdir().expect("temp project");
935 let root = temp.path();
936 std::fs::create_dir_all(root.join("src")).expect("src dir");
937 std::fs::write(
938 root.join("package.json"),
939 r#"{"name":"editor-api-session","main":"src/index.ts"}"#,
940 )
941 .expect("package.json");
942 std::fs::write(
943 root.join("src/index.ts"),
944 "export const used = 1;\nconsole.log(used);\n",
945 )
946 .expect("source");
947
948 let session = EditorAnalysisSession::load(root, None).expect("session loads");
949 let output = session
950 .analyze_project_with(&fallow_config::DuplicatesConfig::default(), true)
951 .expect("analysis runs");
952
953 assert!(output.dead_code.modules.is_some());
954 assert!(
955 output
956 .dead_code
957 .files
958 .as_ref()
959 .is_some_and(|files| !files.is_empty())
960 );
961 }
962
963 #[test]
967 fn changed_files_scope_runs_after_the_analysis_keeps_all_unused_files() {
968 let temp = tempfile::tempdir().expect("temp project");
969 let root = temp.path().canonicalize().expect("canonical root");
970 let src = root.join("src");
971 std::fs::create_dir_all(&src).expect("src dir");
972 std::fs::write(
973 root.join("package.json"),
974 r#"{"name":"editor-scope-order","main":"src/a.ts"}"#,
975 )
976 .expect("package.json");
977 std::fs::write(src.join("a.ts"), "export const a = 1;\n").expect("source a");
978 std::fs::write(src.join("orphan.ts"), "export const orphan = 1;\n").expect("orphan");
979
980 let session = EditorAnalysisSession::load(&root, None).expect("session loads");
981 let mut changed_files = FxHashSet::default();
982 changed_files.insert(src.join("a.ts"));
983 let mut output = session
984 .analyze_project_with_changed_files(
985 &fallow_config::DuplicatesConfig::default(),
986 false,
987 Some(&changed_files),
988 )
989 .expect("analysis runs");
990 let unused_files = |output: &EditorProjectAnalysisOutput| {
991 output
992 .dead_code
993 .results
994 .unused_files
995 .iter()
996 .map(|finding| finding.file.path.clone())
997 .collect::<Vec<_>>()
998 };
999 assert_eq!(
1000 unused_files(&output),
1001 vec![src.join("orphan.ts")],
1002 "the analysis keeps the unused file outside the changed set"
1003 );
1004
1005 session.apply_change_scope(&mut output, &ChangeScope::changed_files(&changed_files));
1006 assert!(
1007 unused_files(&output).is_empty(),
1008 "the scope removes the unused file outside the changed set: {:?}",
1009 unused_files(&output)
1010 );
1011 }
1012
1013 #[test]
1014 fn editor_session_scopes_duplication_to_changed_files() {
1015 let temp = tempfile::tempdir().expect("temp project");
1016 let root = temp.path();
1017 let src = root.join("src");
1018 std::fs::create_dir_all(&src).expect("src dir");
1019 std::fs::write(
1020 root.join("package.json"),
1021 r#"{"name":"editor-api-session","main":"src/a.ts"}"#,
1022 )
1023 .expect("package.json");
1024 let repeated =
1025 "export function repeated() {\n return ['alpha', 'beta', 'gamma'].join(',');\n}\n";
1026 std::fs::write(src.join("a.ts"), repeated).expect("source a");
1027 std::fs::write(src.join("b.ts"), repeated).expect("source b");
1028
1029 let session = EditorAnalysisSession::load(root, None).expect("session loads");
1030 let mut config = session.config().duplicates.clone();
1031 config.min_tokens = 1;
1032 config.min_lines = 1;
1033 let full = session
1034 .analyze_project_with(&config, false)
1035 .expect("analysis runs");
1036 assert!(!full.duplication.clone_groups.is_empty());
1037
1038 let mut changed_files = FxHashSet::default();
1039 changed_files.insert(src.join("unrelated.ts"));
1040 let scoped = session
1041 .analyze_project_with_changed_files(&config, false, Some(&changed_files))
1042 .expect("analysis runs");
1043 assert!(scoped.duplication.clone_groups.is_empty());
1044 }
1045
1046 #[test]
1049 fn inline_complexity_applies_health_threshold_overrides() {
1050 let temp = tempfile::tempdir().expect("temp project");
1051 let root = temp.path();
1052 std::fs::create_dir_all(root.join("src")).expect("src dir");
1053 std::fs::write(
1054 root.join("package.json"),
1055 r#"{"name":"editor-inline-overrides","main":"src/index.ts"}"#,
1056 )
1057 .expect("package.json");
1058 std::fs::write(
1059 root.join(".fallowrc.json"),
1060 r#"{"health":{"maxCyclomatic":2,"maxCognitive":2,"thresholdOverrides":[{"files":["src/legacy.ts"],"maxCyclomatic":50,"maxCognitive":50}]}}"#,
1061 )
1062 .expect("config");
1063 let branchy = |name: &str| {
1064 format!(
1065 "export function {name}(value: number): number {{\n if (value > 1) {{ return 1; }}\n \
1066 if (value > 2) {{ return 2; }}\n if (value > 3) {{ return 3; }}\n return 0;\n}}\n"
1067 )
1068 };
1069 std::fs::write(root.join("src/app.ts"), branchy("appBranchy")).expect("app");
1070 std::fs::write(root.join("src/legacy.ts"), branchy("legacyBranchy")).expect("legacy");
1071 std::fs::write(
1072 root.join("src/index.ts"),
1073 "export { appBranchy } from \"./app\";\nexport { legacyBranchy } from \"./legacy\";\n",
1074 )
1075 .expect("index");
1076
1077 let session = EditorAnalysisSession::load(root, None).expect("session loads");
1078 let output = session
1079 .analyze_project_with(&session.config().duplicates.clone(), true)
1080 .expect("analysis runs");
1081 let names = collect_inline_complexity(session.config(), &output.dead_code)
1082 .into_iter()
1083 .map(|finding| finding.name)
1084 .collect::<Vec<_>>();
1085
1086 assert_eq!(
1087 names,
1088 vec!["appBranchy".to_string()],
1089 "the override raises the ceilings for src/legacy.ts, as in `fallow health`"
1090 );
1091 }
1092
1093 fn make_inline_finding(path: PathBuf) -> EditorInlineComplexityFinding {
1094 EditorInlineComplexityFinding {
1095 path,
1096 name: "myFn".to_string(),
1097 line: 1,
1098 col: 0,
1099 cyclomatic: 5,
1100 cognitive: 4,
1101 exceeded: EditorInlineComplexityExceeded::Cyclomatic,
1102 }
1103 }
1104
1105 #[test]
1106 fn filter_inline_complexity_keeps_findings_in_changed_set() {
1107 let changed: FxHashSet<PathBuf> = [PathBuf::from("/src/a.ts"), PathBuf::from("/src/b.ts")]
1108 .into_iter()
1109 .collect();
1110 let mut findings = vec![
1111 make_inline_finding(PathBuf::from("/src/a.ts")),
1112 make_inline_finding(PathBuf::from("/src/c.ts")),
1113 ];
1114
1115 filter_inline_complexity_by_changed_files(&mut findings, &changed);
1116
1117 assert_eq!(findings.len(), 1);
1118 assert_eq!(
1119 findings[0].path.to_string_lossy().replace('\\', "/"),
1120 "/src/a.ts"
1121 );
1122 }
1123
1124 #[test]
1125 fn filter_inline_complexity_removes_all_when_changed_set_empty() {
1126 let changed: FxHashSet<PathBuf> = FxHashSet::default();
1127 let mut findings = vec![make_inline_finding(PathBuf::from("/src/a.ts"))];
1128
1129 filter_inline_complexity_by_changed_files(&mut findings, &changed);
1130
1131 assert!(
1132 findings.is_empty(),
1133 "empty changed-files set must drop all inline complexity findings"
1134 );
1135 }
1136
1137 #[test]
1138 fn filter_inline_complexity_keeps_all_when_all_in_changed_set() {
1139 let path_a = PathBuf::from("/src/a.ts");
1140 let path_b = PathBuf::from("/src/b.ts");
1141 let changed: FxHashSet<PathBuf> = [path_a.clone(), path_b.clone()].into_iter().collect();
1142 let mut findings = vec![make_inline_finding(path_a), make_inline_finding(path_b)];
1143
1144 filter_inline_complexity_by_changed_files(&mut findings, &changed);
1145
1146 assert_eq!(
1147 findings.len(),
1148 2,
1149 "all findings in the changed set must be retained"
1150 );
1151 }
1152
1153 #[test]
1154 fn editor_session_applies_per_path_rule_overrides() {
1155 let temp = tempfile::tempdir().expect("temp project");
1159 let root = temp.path();
1160 std::fs::create_dir_all(root.join("src/ui")).expect("ui dir");
1161 std::fs::create_dir_all(root.join("src/lib")).expect("lib dir");
1162 std::fs::write(
1163 root.join("package.json"),
1164 r#"{"name":"editor-override-rules","private":true,"main":"src/index.ts"}"#,
1165 )
1166 .expect("package.json");
1167 std::fs::write(
1168 root.join(".fallowrc.json"),
1169 r#"{
1170 "rules": { "unused-exports": "warn", "private-type-leaks": "warn" },
1171 "overrides": [
1172 {
1173 "files": ["src/ui/**"],
1174 "rules": { "unused-exports": "off", "private-type-leaks": "off" }
1175 }
1176 ]
1177}"#,
1178 )
1179 .expect("config");
1180 std::fs::write(
1181 root.join("src/index.ts"),
1182 "import { kitUsed } from './ui/kit';\nimport { libUsed } from './lib/util';\n\nexport const app = `${kitUsed}${libUsed}`;\n",
1183 )
1184 .expect("index");
1185 std::fs::write(
1186 root.join("src/ui/kit.ts"),
1187 "type Props = { label: string };\n\nexport const kitUsed = 'kit';\n\nexport const Unused = (props: Props) => props.label;\n",
1188 )
1189 .expect("kit");
1190 std::fs::write(
1191 root.join("src/lib/util.ts"),
1192 "type Internal = { id: string };\n\nexport const libUsed = 'lib';\n\nexport const alsoUnused = (value: Internal) => value.id;\n",
1193 )
1194 .expect("util");
1195
1196 let session = EditorAnalysisSession::load(root, None).expect("session loads");
1197 let output = session
1198 .analyze_project_with_changed_files(
1199 &fallow_config::DuplicatesConfig::default(),
1200 false,
1201 None,
1202 )
1203 .expect("analysis runs");
1204 let results = &output.dead_code.results;
1205
1206 let unused_export_paths = || {
1207 results
1208 .unused_exports
1209 .iter()
1210 .map(|finding| finding.export.path.clone())
1211 .collect::<Vec<_>>()
1212 };
1213 let leak_paths = || {
1214 results
1215 .private_type_leaks
1216 .iter()
1217 .map(|finding| finding.leak.path.clone())
1218 .collect::<Vec<_>>()
1219 };
1220
1221 assert!(
1222 !unused_export_paths()
1223 .iter()
1224 .any(|path| path.ends_with("kit.ts")),
1225 "the override turns unused-exports off for src/ui/**: {:?}",
1226 unused_export_paths()
1227 );
1228 assert!(
1229 !leak_paths().iter().any(|path| path.ends_with("kit.ts")),
1230 "the override turns private-type-leaks off for src/ui/**: {:?}",
1231 leak_paths()
1232 );
1233 assert!(
1234 unused_export_paths()
1235 .iter()
1236 .any(|path| path.ends_with("util.ts")),
1237 "paths outside the override keep their unused export: {:?}",
1238 unused_export_paths()
1239 );
1240 assert!(
1241 leak_paths().iter().any(|path| path.ends_with("util.ts")),
1242 "paths outside the override keep their private type leak: {:?}",
1243 leak_paths()
1244 );
1245 }
1246
1247 #[test]
1248 fn merge_results_covers_all_fields() {
1249 let mut output = EditorAnalysisOutput::default();
1250
1251 output.merge_results(merge_test_source_with_all_fields());
1252
1253 let target = &output.results;
1254
1255 assert_eq!(target.unused_files.len(), 1);
1256 assert_eq!(target.unused_exports.len(), 1);
1257 assert_eq!(target.unused_types.len(), 1);
1258 assert_eq!(target.private_type_leaks.len(), 1);
1259 assert_eq!(target.deprecated_exports_in_use.len(), 1);
1260 assert_eq!(target.unused_dependencies.len(), 1);
1261 assert_eq!(target.unused_dev_dependencies.len(), 1);
1262 assert_eq!(target.unused_optional_dependencies.len(), 1);
1263 assert_eq!(target.unused_enum_members.len(), 1);
1264 assert_eq!(target.unused_class_members.len(), 1);
1265 assert_eq!(target.unused_store_members.len(), 1);
1266 assert_eq!(target.unresolved_imports.len(), 1);
1267 assert_eq!(target.unlisted_dependencies.len(), 1);
1268 assert_eq!(target.duplicate_exports.len(), 1);
1269 assert_eq!(target.type_only_dependencies.len(), 1);
1270 assert_eq!(target.test_only_dependencies.len(), 1);
1271 assert_eq!(target.circular_dependencies.len(), 1);
1272 assert_eq!(target.re_export_cycles.len(), 1);
1273 assert_eq!(target.package_cycles.len(), 1);
1274 assert_eq!(target.boundary_violations.len(), 1);
1275 assert_eq!(target.boundary_call_violations.len(), 1);
1276 assert_eq!(target.policy_violations.len(), 1);
1277 assert_eq!(target.stale_suppressions.len(), 1);
1278 assert_eq!(target.unused_catalog_entries.len(), 1);
1279 assert_eq!(target.empty_catalog_groups.len(), 1);
1280 assert_eq!(target.unresolved_catalog_references.len(), 1);
1281 assert_eq!(target.unused_dependency_overrides.len(), 1);
1282 assert_eq!(target.misconfigured_dependency_overrides.len(), 1);
1283 assert_eq!(target.invalid_client_exports.len(), 1);
1284 assert_eq!(target.mixed_client_server_barrels.len(), 1);
1285 assert_eq!(target.misplaced_directives.len(), 1);
1286 assert_eq!(target.export_usages.len(), 1);
1287 assert_eq!(target.feature_flags.len(), 1);
1288 assert_eq!(target.security_findings.len(), 1);
1289 assert_eq!(target.security_unresolved_edge_files, 2);
1290 assert_eq!(target.security_unresolved_callee_diagnostics.len(), 1);
1291 assert_eq!(target.suppression_count, 1);
1292 assert_eq!(target.cascade_hidden, 1);
1293 assert_eq!(target.cascade.suppressed_unused_files.len(), 1);
1294 assert!(target.entry_point_summary.is_some());
1295 assert_eq!(
1296 target
1297 .render_fan_in
1298 .as_ref()
1299 .and_then(|m| m.max_distinct_parents),
1300 Some(3)
1301 );
1302 assert_eq!(target.react_component_intel.len(), 1);
1303 assert_eq!(target.dev_dependencies_in_production.len(), 1);
1304 assert_eq!(target.boundary_coverage_violations.len(), 1);
1305 assert_eq!(target.route_collisions.len(), 1);
1306 assert_eq!(target.dynamic_segment_name_conflicts.len(), 1);
1307 assert_eq!(target.unprovided_injects.len(), 1);
1308 assert_eq!(target.unrendered_components.len(), 1);
1309 assert_eq!(target.unused_component_props.len(), 1);
1310 let candidate = &target.absent_component_props[0];
1311 assert_eq!(candidate.prop.component_name, "Widget");
1312 assert_eq!(candidate.prop.prop_name, "highlight");
1313 assert!(candidate.prop.has_default);
1314 assert_eq!(candidate.prop.framework, "vue");
1315 assert_eq!(
1316 candidate.prop.inspected_call_sites[0].path,
1317 PathBuf::from("/main.vue")
1318 );
1319 assert_eq!(candidate.prop.explanation, "Review optional prop contract");
1320
1321 assert_eq!(target.unused_component_emits.len(), 1);
1322 assert_eq!(target.unused_component_inputs.len(), 1);
1323 assert_eq!(target.unused_component_outputs.len(), 1);
1324 assert_eq!(target.unused_svelte_events.len(), 1);
1325 assert_eq!(target.unused_effect_layers.len(), 1);
1326 assert_eq!(target.unused_tagged_errors.len(), 1);
1327 assert_eq!(target.unproduced_catch_tags.len(), 1);
1328 assert_eq!(target.unused_server_actions.len(), 1);
1329 assert_eq!(target.unused_load_data_keys.len(), 1);
1330 assert!(target.unused_load_data_keys_global_abstain);
1331 assert_eq!(target.prop_drilling_chains.len(), 1);
1332 assert_eq!(target.thin_wrappers.len(), 1);
1333 assert_eq!(target.duplicate_prop_shapes.len(), 1);
1334 assert_eq!(target.active_suppressions.len(), 1);
1335 assert_eq!(target.semantic_framework_contracts.len(), 1);
1336 assert_eq!(target.security_unresolved_callee_sites, 3);
1337 assert_eq!(target.unused_component_props_exempted, 1);
1338 }
1339
1340 #[test]
1341 fn merge_duplication_recomputes_percentage() {
1342 let target = EditorDuplicationReport {
1343 clone_groups: vec![],
1344 clone_families: vec![],
1345 mirrored_directories: vec![],
1346 stats: DuplicationStats {
1347 total_files: 5,
1348 files_with_clones: 1,
1349 total_lines: 200,
1350 duplicated_lines: 20,
1351 total_tokens: 1000,
1352 duplicated_tokens: 100,
1353 clone_groups: 1,
1354 clone_families: 0,
1355 clone_instances: 2,
1356 duplication_percentage: 10.0, clone_groups_below_min_occurrences: 0,
1358 clone_groups_ignored: 0,
1359 near_candidates_skipped: 0,
1360 },
1361 };
1362 let source = EditorDuplicationReport {
1363 clone_groups: vec![],
1364 clone_families: vec![],
1365 mirrored_directories: vec![],
1366 stats: DuplicationStats {
1367 total_files: 3,
1368 files_with_clones: 1,
1369 total_lines: 300,
1370 duplicated_lines: 60,
1371 total_tokens: 1500,
1372 duplicated_tokens: 300,
1373 clone_groups: 2,
1374 clone_families: 0,
1375 clone_instances: 4,
1376 duplication_percentage: 20.0, clone_groups_below_min_occurrences: 0,
1378 clone_groups_ignored: 0,
1379 near_candidates_skipped: 0,
1380 },
1381 };
1382
1383 let mut output = EditorAnalysisOutput::new(EditorAnalysisResults::default(), target);
1384 output.merge_duplication(source);
1385
1386 let target = &output.duplication;
1387 assert_eq!(target.stats.total_files, 8);
1388 assert_eq!(target.stats.files_with_clones, 2);
1389 assert_eq!(target.stats.total_lines, 500);
1390 assert_eq!(target.stats.duplicated_lines, 80);
1391 assert_eq!(target.stats.total_tokens, 2500);
1392 assert_eq!(target.stats.duplicated_tokens, 400);
1393 assert_eq!(target.stats.clone_groups, 3);
1394 assert_eq!(target.stats.clone_instances, 6);
1395 assert!((target.stats.duplication_percentage - 16.0).abs() < f64::EPSILON);
1396 }
1397
1398 #[test]
1399 fn merge_duplication_zero_total_lines_yields_zero_percentage() {
1400 let mut output = EditorAnalysisOutput::default();
1401
1402 output.merge_duplication(EditorDuplicationReport::default());
1403
1404 let target = &output.duplication;
1405
1406 assert_eq!(target.stats.total_lines, 0);
1407 assert!((target.stats.duplication_percentage - 0.0).abs() < f64::EPSILON);
1408 }
1409
1410 fn merge_test_unused_export(
1411 path: &str,
1412 export_name: &str,
1413 is_type_only: bool,
1414 line: u32,
1415 ) -> UnusedExport {
1416 UnusedExport {
1417 path: path.into(),
1418 export_name: export_name.to_string(),
1419 is_type_only,
1420 line,
1421 col: 0,
1422 span_start: 0,
1423 is_re_export: false,
1424 deprecated: false,
1425 deprecated_reason: None,
1426 }
1427 }
1428
1429 fn merge_test_unused_dependency(
1430 package_name: &str,
1431 location: super::editor_results::DependencyLocation,
1432 line: u32,
1433 ) -> UnusedDependency {
1434 UnusedDependency {
1435 package_name: package_name.to_string(),
1436 location,
1437 path: "/pkg.json".into(),
1438 line,
1439 used_in_workspaces: Vec::new(),
1440 declared_and_imported_in: Vec::new(),
1441 }
1442 }
1443
1444 fn merge_test_unused_member(
1445 parent_name: &str,
1446 member_name: &str,
1447 kind: super::editor_extract::MemberKind,
1448 line: u32,
1449 ) -> UnusedMember {
1450 UnusedMember {
1451 path: "/f.ts".into(),
1452 parent_name: parent_name.to_string(),
1453 member_name: member_name.to_string(),
1454 kind,
1455 line,
1456 col: 0,
1457 }
1458 }
1459
1460 #[expect(
1461 clippy::too_many_lines,
1462 reason = "intentionally names every EditorAnalysisResults field (no ..Default::default()) so a new field is a compile error here; see #444"
1463 )]
1464 pub fn merge_test_source_with_all_fields() -> EditorAnalysisResults {
1465 EditorAnalysisResults {
1466 unused_files: vec![UnusedFileFinding::with_actions(UnusedFile {
1467 path: "/f.ts".into(),
1468 })],
1469 unused_exports: vec![UnusedExportFinding::with_actions(merge_test_unused_export(
1470 "/f.ts", "e", false, 1,
1471 ))],
1472 unused_types: vec![UnusedTypeFinding::with_actions(merge_test_unused_export(
1473 "/f.ts", "T", true, 2,
1474 ))],
1475 unused_dependencies: vec![UnusedDependencyFinding::with_actions(
1476 merge_test_unused_dependency(
1477 "dep",
1478 super::editor_results::DependencyLocation::Dependencies,
1479 3,
1480 ),
1481 )],
1482 unused_dev_dependencies: vec![UnusedDevDependencyFinding::with_actions(
1483 merge_test_unused_dependency(
1484 "dev-dep",
1485 super::editor_results::DependencyLocation::DevDependencies,
1486 4,
1487 ),
1488 )],
1489 unused_optional_dependencies: vec![UnusedOptionalDependencyFinding::with_actions(
1490 merge_test_unused_dependency(
1491 "opt-dep",
1492 super::editor_results::DependencyLocation::OptionalDependencies,
1493 5,
1494 ),
1495 )],
1496 unused_enum_members: vec![UnusedEnumMemberFinding::with_actions(
1497 merge_test_unused_member(
1498 "E",
1499 "A",
1500 super::editor_extract::MemberKind::EnumMember,
1501 6,
1502 ),
1503 )],
1504 unused_class_members: vec![UnusedClassMemberFinding::with_actions(
1505 merge_test_unused_member(
1506 "C",
1507 "m",
1508 super::editor_extract::MemberKind::ClassMethod,
1509 7,
1510 ),
1511 )],
1512 unused_store_members: vec![UnusedStoreMemberFinding::with_actions(
1513 merge_test_unused_member(
1514 "S",
1515 "a",
1516 super::editor_extract::MemberKind::StoreMember,
1517 7,
1518 ),
1519 )],
1520 unresolved_imports: vec![
1521 super::editor_results::UnresolvedImportFinding::with_actions(
1522 super::editor_results::UnresolvedImport {
1523 path: "/f.ts".into(),
1524 specifier: "./gone".to_string(),
1525 line: 8,
1526 col: 0,
1527 specifier_col: 10,
1528 },
1529 ),
1530 ],
1531 unlisted_dependencies: vec![UnlistedDependencyFinding::with_actions(
1532 UnlistedDependency {
1533 package_name: "unlisted".to_string(),
1534 imported_from: vec![],
1535 },
1536 )],
1537 duplicate_exports: vec![super::editor_results::DuplicateExportFinding::with_actions(
1538 super::editor_results::DuplicateExport {
1539 export_name: "dup".to_string(),
1540 locations: vec![],
1541 },
1542 )],
1543 type_only_dependencies: vec![
1544 super::editor_results::TypeOnlyDependencyFinding::with_actions(
1545 TypeOnlyDependency {
1546 package_name: "type-only".to_string(),
1547 path: "/pkg.json".into(),
1548 line: 9,
1549 },
1550 ),
1551 ],
1552 circular_dependencies: vec![CircularDependencyFinding::with_actions(
1553 CircularDependency {
1554 files: vec!["/a.ts".into(), "/b.ts".into()],
1555 length: 2,
1556 line: 10,
1557 col: 0,
1558 edges: Vec::new(),
1559 is_cross_package: false,
1560 },
1561 )],
1562 test_only_dependencies: vec![TestOnlyDependencyFinding::with_actions(
1563 TestOnlyDependency {
1564 package_name: "test-only".to_string(),
1565 path: "/pkg.json".into(),
1566 line: 11,
1567 },
1568 )],
1569 dev_dependencies_in_production: vec![DevDependencyInProductionFinding::with_actions(
1570 DevDependencyInProduction {
1571 package_name: "dev-in-prod".to_string(),
1572 path: "/pkg.json".into(),
1573 line: 12,
1574 },
1575 )],
1576 boundary_violations: vec![BoundaryViolationFinding::with_actions(BoundaryViolation {
1577 from_path: "/a.ts".into(),
1578 to_path: "/b.ts".into(),
1579 from_zone: "ui".to_string(),
1580 to_zone: "data".to_string(),
1581 import_specifier: "../data/db".to_string(),
1582 line: 12,
1583 col: 0,
1584 via_path: None,
1585 })],
1586 boundary_coverage_violations: vec![
1587 super::editor_results::BoundaryCoverageViolationFinding::with_actions(
1588 super::editor_results::BoundaryCoverageViolation {
1589 path: "/unzoned.ts".into(),
1590 line: 13,
1591 col: 0,
1592 },
1593 ),
1594 ],
1595 boundary_call_violations: vec![
1596 super::editor_results::BoundaryCallViolationFinding::with_actions(
1597 super::editor_results::BoundaryCallViolation {
1598 path: "/zoned.ts".into(),
1599 line: 14,
1600 col: 0,
1601 zone: "domain".to_string(),
1602 callee: "console.log".to_string(),
1603 pattern: "console.*".to_string(),
1604 },
1605 ),
1606 ],
1607 policy_violations: vec![super::editor_results::PolicyViolationFinding::with_actions(
1608 super::editor_results::PolicyViolation {
1609 path: "/zoned.ts".into(),
1610 line: 15,
1611 col: 0,
1612 pack: "team-policy".to_string(),
1613 rule_id: "no-console".to_string(),
1614 kind: super::editor_results::PolicyRuleKind::BannedCall,
1615 matched: "console.log".to_string(),
1616 severity: super::editor_results::PolicyViolationSeverity::Warn,
1617 message: None,
1618 },
1619 )],
1620 export_usages: vec![ExportUsage {
1621 path: "/f.ts".into(),
1622 export_name: "used".to_string(),
1623 line: 15,
1624 col: 0,
1625 reference_count: 3,
1626 reference_locations: vec![],
1627 }],
1628 private_type_leaks: vec![super::editor_results::PrivateTypeLeakFinding::with_actions(
1629 super::editor_results::PrivateTypeLeak {
1630 path: "/f.ts".into(),
1631 export_name: "pub_fn".to_string(),
1632 type_name: "Secret".to_string(),
1633 line: 14,
1634 col: 0,
1635 span_start: 0,
1636 semantic: None,
1637 },
1638 )],
1639 deprecated_exports_in_use: vec![
1640 super::editor_results::DeprecatedExportInUseFinding::with_actions(
1641 super::editor_results::DeprecatedExportInUse {
1642 path: "/f.ts".into(),
1643 export_name: "old".to_string(),
1644 is_type_only: false,
1645 line: 16,
1646 col: 0,
1647 span_start: 0,
1648 deprecated_reason: None,
1649 consumer_count: 1,
1650 consumers: vec![super::editor_results::DeprecatedExportConsumer {
1651 path: "/g.ts".into(),
1652 line: 1,
1653 col: 0,
1654 kind: super::editor_results::DeprecatedConsumerKind::NamedImport,
1655 }],
1656 public_api: false,
1657 },
1658 ),
1659 ],
1660 re_export_cycles: vec![super::editor_results::ReExportCycleFinding::with_actions(
1661 super::editor_results::ReExportCycle {
1662 files: vec!["/barrel.ts".into()],
1663 kind: super::editor_results::ReExportCycleKind::SelfLoop,
1664 },
1665 )],
1666 package_cycles: vec![super::editor_results::PackageCycleFinding::with_actions(
1667 super::editor_results::PackageCycle {
1668 packages: vec!["a".into(), "b".into()],
1669 package_roots: Vec::new(),
1670 length: 2,
1671 edges: vec![super::editor_results::PackageCycleEdge {
1672 from_package: "a".into(),
1673 to_package: "b".into(),
1674 path: "/a/x.ts".into(),
1675 target_path: "/b/y.ts".into(),
1676 line: 1,
1677 col: 0,
1678 type_only: false,
1679 }],
1680 group_truncated: false,
1681 },
1682 )],
1683 stale_suppressions: vec![super::editor_results::StaleSuppression {
1684 finding_id: None,
1685 path: "/f.ts".into(),
1686 line: 15,
1687 col: 0,
1688 origin: super::editor_results::SuppressionOrigin::Comment {
1689 issue_kind: None,
1690 reason: None,
1691 is_file_level: false,
1692 kind_known: true,
1693 },
1694 missing_reason: false,
1695 actions: super::editor_results::StaleSuppression::actions_for(false),
1696 effective_severity: None,
1697 }],
1698 unused_catalog_entries: vec![
1699 super::editor_results::UnusedCatalogEntryFinding::with_actions(
1700 super::editor_results::UnusedCatalogEntry {
1701 entry_name: "react".to_string(),
1702 catalog_name: "default".to_string(),
1703 path: "/pnpm-workspace.yaml".into(),
1704 line: 16,
1705 hardcoded_consumers: vec![],
1706 },
1707 ),
1708 ],
1709 empty_catalog_groups: vec![
1710 super::editor_results::EmptyCatalogGroupFinding::with_actions(
1711 super::editor_results::EmptyCatalogGroup {
1712 catalog_name: "ui".to_string(),
1713 path: "/pnpm-workspace.yaml".into(),
1714 line: 17,
1715 },
1716 ),
1717 ],
1718 unresolved_catalog_references: vec![
1719 super::editor_results::UnresolvedCatalogReferenceFinding::with_actions(
1720 super::editor_results::UnresolvedCatalogReference {
1721 entry_name: "vue".to_string(),
1722 catalog_name: "default".to_string(),
1723 path: "/pkg.json".into(),
1724 line: 18,
1725 available_in_catalogs: vec![],
1726 },
1727 ),
1728 ],
1729 unused_dependency_overrides: vec![
1730 super::editor_results::UnusedDependencyOverrideFinding::with_actions(
1731 super::editor_results::UnusedDependencyOverride {
1732 raw_key: "react".to_string(),
1733 target_package: "react".to_string(),
1734 parent_package: None,
1735 version_constraint: None,
1736 version_range: "18".to_string(),
1737 source: super::editor_results::DependencyOverrideSource::PnpmWorkspaceYaml,
1738 path: "/pnpm-workspace.yaml".into(),
1739 line: 19,
1740 hint: None,
1741 },
1742 ),
1743 ],
1744 misconfigured_dependency_overrides: vec![
1745 super::editor_results::MisconfiguredDependencyOverrideFinding::with_actions(
1746 super::editor_results::MisconfiguredDependencyOverride {
1747 raw_key: "bad>".to_string(),
1748 target_package: None,
1749 raw_value: String::new(),
1750 reason:
1751 super::editor_results::DependencyOverrideMisconfigReason::EmptyValue,
1752 source: super::editor_results::DependencyOverrideSource::PnpmPackageJson,
1753 path: "/pkg.json".into(),
1754 line: 20,
1755 },
1756 ),
1757 ],
1758 invalid_client_exports: vec![
1759 super::editor_results::InvalidClientExportFinding::with_actions(
1760 super::editor_results::InvalidClientExport {
1761 path: "/app/page.tsx".into(),
1762 export_name: "metadata".to_string(),
1763 directive: "use client".to_string(),
1764 line: 22,
1765 col: 0,
1766 },
1767 ),
1768 ],
1769 mixed_client_server_barrels: vec![
1770 super::editor_results::MixedClientServerBarrelFinding::with_actions(
1771 super::editor_results::MixedClientServerBarrel {
1772 path: "/app/components/index.ts".into(),
1773 client_origin: "./Button".to_string(),
1774 server_origin: "./fetchUser".to_string(),
1775 line: 23,
1776 col: 0,
1777 },
1778 ),
1779 ],
1780 misplaced_directives: vec![
1781 super::editor_results::MisplacedDirectiveFinding::with_actions(
1782 super::editor_results::MisplacedDirective {
1783 path: "/app/widget.tsx".into(),
1784 directive: "use client".to_string(),
1785 line: 24,
1786 col: 0,
1787 },
1788 ),
1789 ],
1790 unprovided_injects: vec![
1791 super::editor_results::UnprovidedInjectFinding::with_actions(
1792 super::editor_results::UnprovidedInject {
1793 path: "/Comp.vue".into(),
1794 key_name: "ApiKey".to_string(),
1795 framework: "vue".to_string(),
1796 line: 25,
1797 col: 0,
1798 },
1799 ),
1800 ],
1801 unrendered_components: vec![
1802 super::editor_results::UnrenderedComponentFinding::with_actions(
1803 super::editor_results::UnrenderedComponent {
1804 path: "/Widget.vue".into(),
1805 component_name: "Widget".to_string(),
1806 framework: "vue".to_string(),
1807 reachable_via: None,
1808 line: 26,
1809 col: 0,
1810 },
1811 ),
1812 ],
1813 unused_component_props: vec![
1814 super::editor_results::UnusedComponentPropFinding::with_actions(
1815 super::editor_results::UnusedComponentProp {
1816 path: "/Widget.vue".into(),
1817 component_name: "Widget".to_string(),
1818 prop_name: "size".to_string(),
1819 line: 27,
1820 col: 0,
1821 },
1822 ),
1823 ],
1824 absent_component_props: vec![
1825 super::editor_results::AbsentComponentPropFinding::with_actions(
1826 super::editor_results::AbsentComponentProp {
1827 path: "/Widget.vue".into(),
1828 component_name: "Widget".to_string(),
1829 framework: "vue".to_string(),
1830 prop_name: "highlight".to_string(),
1831 line: 27,
1832 col: 0,
1833 has_default: true,
1834 inspected_call_sites: vec![super::editor_results::ComponentPropCallSite {
1835 path: "/main.vue".into(),
1836 line: 1,
1837 col: 0,
1838 }],
1839 explanation: "Review optional prop contract".to_string(),
1840 },
1841 ),
1842 ],
1843 unused_component_emits: vec![
1844 super::editor_results::UnusedComponentEmitFinding::with_actions(
1845 super::editor_results::UnusedComponentEmit {
1846 path: "/Widget.vue".into(),
1847 component_name: "Widget".to_string(),
1848 emit_name: "change".to_string(),
1849 line: 28,
1850 col: 0,
1851 },
1852 ),
1853 ],
1854 unused_component_inputs: vec![
1855 super::editor_results::UnusedComponentInputFinding::with_actions(
1856 super::editor_results::UnusedComponentInput {
1857 path: "/widget.component.ts".into(),
1858 component_name: "WidgetComponent".to_string(),
1859 input_name: "size".to_string(),
1860 line: 29,
1861 col: 0,
1862 },
1863 ),
1864 ],
1865 unused_component_outputs: vec![
1866 super::editor_results::UnusedComponentOutputFinding::with_actions(
1867 super::editor_results::UnusedComponentOutput {
1868 path: "/widget.component.ts".into(),
1869 component_name: "WidgetComponent".to_string(),
1870 output_name: "change".to_string(),
1871 line: 30,
1872 col: 0,
1873 },
1874 ),
1875 ],
1876 unused_svelte_events: vec![
1877 super::editor_results::UnusedSvelteEventFinding::with_actions(
1878 super::editor_results::UnusedSvelteEvent {
1879 path: "/Child.svelte".into(),
1880 component_name: "Child".to_string(),
1881 event_name: "dead".to_string(),
1882 line: 31,
1883 col: 0,
1884 },
1885 ),
1886 ],
1887 unused_effect_layers: vec![
1888 super::editor_results::UnusedEffectLayerFinding::with_actions(
1889 super::editor_results::UnusedEffectLayer {
1890 path: "/services.ts".into(),
1891 name: "CacheLive".to_string(),
1892 kind: super::editor_results::EffectLayerKind::TopLevel,
1893 line: 33,
1894 col: 0,
1895 dead_consumers: vec!["OrphanLive".to_string()],
1896 },
1897 ),
1898 ],
1899 unused_tagged_errors: vec![
1900 super::editor_results::UnusedTaggedErrorFinding::with_actions(
1901 super::editor_results::UnusedTaggedError {
1902 path: "/errors.ts".into(),
1903 name: "Expired".to_string(),
1904 tag: "Expired".to_string(),
1905 line: 34,
1906 col: 0,
1907 catch_sites: Vec::new(),
1908 },
1909 ),
1910 ],
1911 unproduced_catch_tags: vec![
1912 super::editor_results::UnproducedCatchTagFinding::with_actions(
1913 super::editor_results::UnproducedCatchTag {
1914 path: "/main.ts".into(),
1915 tag: "Expired".to_string(),
1916 line: 35,
1917 col: 0,
1918 candidate_classes: vec!["Expired".to_string()],
1919 },
1920 ),
1921 ],
1922 unused_server_actions: vec![
1923 super::editor_results::UnusedServerActionFinding::with_actions(
1924 super::editor_results::UnusedServerAction {
1925 path: "/app/actions.ts".into(),
1926 action_name: "createUser".to_string(),
1927 line: 32,
1928 col: 0,
1929 },
1930 ),
1931 ],
1932 unused_load_data_keys: vec![
1933 super::editor_results::UnusedLoadDataKeyFinding::with_actions(
1934 super::editor_results::UnusedLoadDataKey {
1935 path: "/src/routes/blog/+page.server.ts".into(),
1936 key_name: "posts".to_string(),
1937 line: 33,
1938 col: 0,
1939 route_dir: None,
1940 },
1941 ),
1942 ],
1943 unused_load_data_keys_global_abstain: true,
1944 prop_drilling_chains: vec![
1945 super::editor_results::PropDrillingChainFinding::with_actions(
1946 super::editor_results::PropDrillingChain {
1947 prop: "user".to_string(),
1948 depth: 1,
1949 hops: vec![super::editor_results::PropDrillHop {
1950 file: "/Hop.tsx".into(),
1951 line: 34,
1952 component: "Hop".to_string(),
1953 }],
1954 },
1955 ),
1956 ],
1957 thin_wrappers: vec![super::editor_results::ThinWrapperFinding::with_actions(
1958 super::editor_results::ThinWrapper {
1959 file: "/Wrapper.tsx".into(),
1960 line: 35,
1961 component: "Wrapper".to_string(),
1962 child_component: "Child".to_string(),
1963 },
1964 )],
1965 duplicate_prop_shapes: vec![
1966 super::editor_results::DuplicatePropShapeFinding::with_actions(
1967 super::editor_results::DuplicatePropShape {
1968 file: "/Card.tsx".into(),
1969 line: 36,
1970 component: "Card".to_string(),
1971 shape: vec!["subtitle".to_string(), "title".to_string()],
1972 group_size: 2,
1973 sharing_components: vec![],
1974 },
1975 ),
1976 ],
1977 route_collisions: vec![super::editor_results::RouteCollisionFinding::with_actions(
1978 super::editor_results::RouteCollision {
1979 path: "/app/(a)/about/page.tsx".into(),
1980 url: "/about".to_string(),
1981 conflicting_paths: vec!["/app/(b)/about/page.tsx".into()],
1982 line: 1,
1983 col: 0,
1984 },
1985 )],
1986 dynamic_segment_name_conflicts: vec![
1987 super::editor_results::DynamicSegmentNameConflictFinding::with_actions(
1988 super::editor_results::DynamicSegmentNameConflict {
1989 path: "/app/shop/[id]/page.tsx".into(),
1990 position: "/shop".to_string(),
1991 conflicting_segments: vec!["[id]".to_string(), "[slug]".to_string()],
1992 conflicting_paths: vec!["/app/shop/[slug]/edit/page.tsx".into()],
1993 line: 1,
1994 col: 0,
1995 },
1996 ),
1997 ],
1998 cascade_hidden: 1,
1999 cascade: fallow_types::results::CascadeState {
2000 suppressed_unused_files: vec!["/f.ts".into()],
2001 ..fallow_types::results::CascadeState::default()
2002 },
2003 suppression_count: 1,
2004 unused_component_props_exempted: 1,
2005 active_suppressions: vec![super::editor_results::ActiveSuppression {
2006 path: "/f.ts".into(),
2007 kind: Some("unused-export".to_string()),
2008 is_file_level: false,
2009 reason: None,
2010 comment_line: 37,
2011 }],
2012 feature_flags: vec![super::editor_results::FeatureFlag {
2013 path: "/f.ts".into(),
2014 flag_name: "ENABLE_X".to_string(),
2015 kind: super::editor_results::FlagKind::EnvironmentVariable,
2016 confidence: super::editor_results::FlagConfidence::High,
2017 line: 21,
2018 col: 0,
2019 guard_span_start: None,
2020 guard_span_end: None,
2021 sdk_name: None,
2022 guard_line_start: None,
2023 guard_line_end: None,
2024 guarded_dead_exports: vec![],
2025 }],
2026 entry_point_summary: Some(super::editor_results::EntryPointSummary {
2027 total: 0,
2028 by_source: vec![],
2029 }),
2030 security_findings: vec![super::editor_results::SecurityFinding {
2031 finding_id: String::new(),
2032 candidate: super::editor_results::SecurityCandidate::default(),
2033 taint_flow: None,
2034 attack_surface: None,
2035 kind: super::editor_results::SecurityFindingKind::ClientServerLeak,
2036 category: None,
2037 cwe: None,
2038 path: "/client.tsx".into(),
2039 line: 1,
2040 col: 0,
2041 evidence: "transitively reaches DATABASE_URL".to_string(),
2042 source_backed: false,
2043 source_read: None,
2044 severity: SecuritySeverity::Low,
2045 trace: vec![],
2046 actions: vec![],
2047 dead_code: None,
2048 reachability: None,
2049 runtime: None,
2050 }],
2051 security_unresolved_edge_files: 2,
2052 security_unresolved_callee_sites: 3,
2053 security_unresolved_callee_diagnostics: vec![
2054 super::editor_results::SecurityUnresolvedCalleeDiagnostic {
2055 path: "/client.tsx".into(),
2056 line: 2,
2057 col: 0,
2058 reason: super::editor_extract::SkippedSecurityCalleeReason::DynamicDispatch,
2059 expression_kind:
2060 super::editor_extract::SkippedSecurityCalleeExpressionKind::Other,
2061 },
2062 ],
2063 render_fan_in: Some(super::editor_results::RenderFanInMetric {
2064 per_component: vec![super::editor_results::RenderFanInComponent {
2065 file: "/Button.tsx".into(),
2066 component: "Button".to_string(),
2067 render_sites: 6,
2068 distinct_parents: 3,
2069 }],
2070 p95_distinct_parents: Some(3),
2071 high_pct: Some(0.0),
2072 max_distinct_parents: Some(3),
2073 }),
2074 react_component_intel: vec![super::editor_results::ReactComponentIntel {
2075 path: "/Button.tsx".into(),
2076 component_name: "Button".to_string(),
2077 anchor_line: 1,
2078 anchor_col: 0,
2079 render_sites: 6,
2080 distinct_parents: 3,
2081 prop_count: 1,
2082 hooks: super::editor_results::ReactHookSummary::default(),
2083 props: Vec::new(),
2084 }],
2085 semantic_framework_contracts: vec![fallow_types::semantic::SemanticFrameworkContract {
2086 framework: "lit".to_string(),
2087 package: "lit".to_string(),
2088 heritage_symbol: "LitElement".to_string(),
2089 heritage_names: vec!["LitElement".to_string()],
2090 relation: fallow_types::semantic::SemanticFrameworkRelation::Extends,
2091 members: vec!["render".to_string()],
2092 }],
2093 }
2094 }
2095}