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 BoundaryCallViolationFinding, BoundaryCoverageViolationFinding, BoundaryViolationFinding,
195 CircularDependencyFinding, DeprecatedExportInUseFinding, DevDependencyInProductionFinding,
196 DuplicateExportFinding, DuplicatePropShapeFinding, DynamicSegmentNameConflictFinding,
197 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::results::{
212 ActiveSuppression, AnalysisResults, BoundaryCallViolation, BoundaryCoverageViolation,
213 BoundaryViolation, CircularDependency, CircularDependencyEdge, DependencyLocation,
214 DependencyOverrideMisconfigReason, DependencyOverrideSource, DeprecatedConsumerKind,
215 DeprecatedExportConsumer, DeprecatedExportInUse, DevDependencyInProduction,
216 DuplicateExport, DuplicateLocation, DuplicatePropShape, DuplicatePropShapeMember,
217 DynamicSegmentNameConflict, EmptyCatalogGroup, EntryPointSummary, ExportUsage, FeatureFlag,
218 FlagConfidence, FlagKind, ImportSite, InvalidClientExport, MisconfiguredDependencyOverride,
219 MisplacedDirective, MixedClientServerBarrel, PackageCycle, PackageCycleEdge,
220 PolicyRuleKind, PolicyViolation, PolicyViolationSeverity, PrivateTypeLeak, PropDrillHop,
221 PropDrillingChain, ReExportCycle, ReExportCycleKind, ReactComponentIntel, ReactHookSummary,
222 ReactPropDrill, ReactPropIntel, ReferenceLocation, RenderFanInComponent, RenderFanInMetric,
223 RouteCollision, SecurityAttackSurfaceEntry, SecurityCandidate, SecurityCandidateBoundary,
224 SecurityCandidateSink, SecurityDeadCodeContext, SecurityDeadCodeKind,
225 SecurityDefensiveBoundary, SecurityDefensiveControl, SecurityFinding, SecurityFindingKind,
226 SecurityNetworkContext, SecurityReachability, SecurityRuntimeContext, SecurityRuntimeState,
227 SecuritySeverity, SecurityTaintFlow, SecurityUnresolvedCalleeDiagnostic,
228 SecurityZoneCrossing, StaleSuppression, SuppressionOrigin, TaintConfidence, TaintEndpoint,
229 TaintPath, TestOnlyDependency, ThinWrapper, TraceHop, TraceHopRole, TypeOnlyDependency,
230 UnlistedDependency, UnprovidedInject, UnrenderedComponent, UnresolvedCatalogReference,
231 UnresolvedImport, UnusedCatalogEntry, UnusedComponentEmit, UnusedComponentInput,
232 UnusedComponentOutput, UnusedComponentProp, UnusedDependency, UnusedDependencyOverride,
233 UnusedExport, UnusedFile, UnusedLoadDataKey, UnusedMember, UnusedServerAction,
234 UnusedSvelteEvent,
235 };
236}
237
238pub mod editor_security {
240 #[must_use]
242 pub fn security_catalogue_title(kind: &str) -> Option<&'static str> {
243 fallow_engine::dead_code::security_catalogue_title(kind)
244 }
245}
246
247pub mod editor_suppress {
249 pub use fallow_types::suppress::{IssueKind, is_suppressed};
250}
251
252pub type EditorAnalysisResults = fallow_types::results::AnalysisResults;
254
255#[derive(Debug)]
260pub struct EditorDeadCodeAnalysisOutput {
261 pub results: EditorAnalysisResults,
263 pub modules: Option<Vec<ModuleInfo>>,
266 pub files: Option<Vec<DiscoveredFile>>,
268}
269
270impl EditorDeadCodeAnalysisOutput {
271 fn from_engine(output: fallow_engine::dead_code::DeadCodeAnalysisOutput) -> Self {
272 Self {
273 results: output.results,
274 modules: output.modules,
275 files: output.files,
276 }
277 }
278}
279
280#[derive(Debug, Clone, PartialEq, Eq)]
286pub struct EditorInlineComplexityFinding {
287 pub path: PathBuf,
289 pub name: String,
291 pub line: u32,
293 pub col: u32,
295 pub cyclomatic: u16,
297 pub cognitive: u16,
299 pub exceeded: EditorInlineComplexityExceeded,
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq)]
305pub enum EditorInlineComplexityExceeded {
306 Cyclomatic,
308 Cognitive,
310 CyclomaticAndCognitive,
312}
313
314#[must_use]
320pub fn collect_inline_complexity(
321 config: &fallow_config::ResolvedConfig,
322 output: &EditorDeadCodeAnalysisOutput,
323) -> Vec<EditorInlineComplexityFinding> {
324 let (Some(modules), Some(files)) = (output.modules.as_ref(), output.files.as_ref()) else {
325 return Vec::new();
326 };
327 fallow_engine::health::inline_complexity(config, modules, files)
328 .into_iter()
329 .map(|finding| EditorInlineComplexityFinding {
330 exceeded: match (finding.exceeds_cyclomatic, finding.exceeds_cognitive) {
331 (true, true) => EditorInlineComplexityExceeded::CyclomaticAndCognitive,
332 (true, false) => EditorInlineComplexityExceeded::Cyclomatic,
333 (false, _) => EditorInlineComplexityExceeded::Cognitive,
334 },
335 path: finding.path,
336 name: finding.name,
337 line: finding.line,
338 col: finding.col,
339 cyclomatic: finding.cyclomatic,
340 cognitive: finding.cognitive,
341 })
342 .collect()
343}
344
345#[allow(
347 clippy::implicit_hasher,
348 reason = "editor analysis changed-file sets use the workspace FxHashSet convention"
349)]
350pub fn filter_inline_complexity_by_changed_files(
351 findings: &mut Vec<EditorInlineComplexityFinding>,
352 changed_files: &FxHashSet<PathBuf>,
353) {
354 findings.retain(|finding| changed_files.contains(&finding.path));
355}
356
357pub fn filter_inline_complexity_by_change_scope(
359 findings: &mut Vec<EditorInlineComplexityFinding>,
360 scope: &ChangeScope,
361) {
362 findings.retain(|finding| scope.contains(&finding.path));
363}
364
365pub type EditorSessionParseCounts = fallow_engine::session::SessionParseCounts;
368
369#[derive(Debug)]
371pub struct EditorAnalysisSession {
372 inner: fallow_engine::session::AnalysisSession,
373}
374
375impl EditorAnalysisSession {
376 pub fn load(root: &Path, config_path: Option<&Path>) -> fallow_engine::EngineResult<Self> {
382 fallow_engine::session::AnalysisSession::load(root, config_path).map(Self::from_engine)
383 }
384
385 pub fn load_with_config(
391 root: &Path,
392 config_path: Option<&Path>,
393 configure: impl FnOnce(&mut fallow_config::ResolvedConfig),
394 ) -> fallow_engine::EngineResult<Self> {
395 fallow_engine::session::AnalysisSession::load_with_config(root, config_path, configure)
396 .map(Self::from_engine)
397 }
398
399 pub fn load_with_config_options(
406 root: &Path,
407 config_path: Option<&Path>,
408 load_options: fallow_config::ConfigLoadOptions,
409 configure: impl FnOnce(&mut fallow_config::ResolvedConfig),
410 ) -> fallow_engine::EngineResult<Self> {
411 fallow_engine::session::AnalysisSession::load_with_config_options(
412 root,
413 config_path,
414 load_options,
415 configure,
416 )
417 .map(Self::from_engine)
418 }
419
420 #[must_use]
422 pub fn load_default(root: &Path) -> Self {
423 Self::from_engine(fallow_engine::session::AnalysisSession::load_default(root))
424 }
425
426 #[must_use]
429 pub fn load_default_with_config(
430 root: &Path,
431 configure: impl FnOnce(&mut fallow_config::ResolvedConfig),
432 ) -> Self {
433 Self::from_engine(
434 fallow_engine::session::AnalysisSession::load_default_with_config(root, configure),
435 )
436 }
437
438 #[must_use]
447 pub fn with_cancellation(
448 self,
449 cancellation: std::sync::Arc<std::sync::atomic::AtomicBool>,
450 ) -> Self {
451 Self::from_engine(self.inner.with_cancellation(cancellation))
452 }
453
454 pub fn set_cancellation(
456 &mut self,
457 cancellation: std::sync::Arc<std::sync::atomic::AtomicBool>,
458 ) {
459 self.inner.set_cancellation(cancellation);
460 }
461
462 pub fn refresh_discovery(&mut self) -> bool {
467 self.inner.refresh_discovery()
468 }
469
470 #[must_use]
474 pub fn config_inputs(&self) -> &fallow_config::ConfigInputs {
475 self.inner.config_inputs()
476 }
477
478 #[must_use]
481 pub fn config_inputs_before_resolve(&self) -> &fallow_config::ConfigInputsSnapshot {
482 self.inner.config_inputs_before_resolve()
483 }
484
485 #[must_use]
488 pub fn retained_bytes_estimate(&self) -> u64 {
489 self.inner.retained_bytes_estimate()
490 }
491
492 pub fn flush_parse_cache(&self) {
495 self.inner.flush_parse_cache();
496 }
497
498 pub fn prewarm(&self, retain_complexity_artifacts: bool) -> fallow_engine::EngineResult<()> {
505 self.inner
506 .prewarm_parsed_modules(retain_complexity_artifacts)
507 }
508
509 #[must_use]
511 pub fn parse_counts(&self) -> EditorSessionParseCounts {
512 self.inner.parse_counts()
513 }
514
515 #[must_use]
517 pub fn config(&self) -> &fallow_config::ResolvedConfig {
518 self.inner.config()
519 }
520
521 pub fn change_scope(
529 &self,
530 request: ChangeScopeRequest<'_>,
531 ) -> Result<ChangeScope, PackageBaselineError> {
532 ChangeScope::resolve(request, self.inner.config(), self.inner.workspaces())
533 }
534
535 #[must_use]
540 pub fn unmatched_config_patterns(&self) -> Vec<fallow_config::WorkspaceDiagnostic> {
541 fallow_engine::dead_code::config_pattern_diagnostics(self.inner.config(), true)
542 }
543
544 #[must_use]
546 pub fn config_path(&self) -> Option<&Path> {
547 self.inner.config_path()
548 }
549
550 pub fn refine_type_aware_dead_code(
558 &self,
559 options: &crate::TypeAwareOptions,
560 filters: &crate::DeadCodeFilters,
561 output: &mut EditorDeadCodeAnalysisOutput,
562 ) -> Result<Option<fallow_types::envelope::TypeAwareMeta>, crate::ProgrammaticError> {
563 let meta = crate::type_aware::refine_programmatic_dead_code(
564 options,
565 filters,
566 &self.inner,
567 &mut output.results,
568 )?;
569 fallow_engine::dead_code::apply_rule_severities(&mut output.results, self.inner.config());
573 Ok(meta)
574 }
575
576 pub fn refine_type_aware_dead_code_in_session(
578 &self,
579 semantic_session: &mut crate::TypeAwareSession,
580 changes: Option<&crate::TypeAwareFileChanges>,
581 options: &crate::TypeAwareOptions,
582 filters: &crate::DeadCodeFilters,
583 output: &mut EditorDeadCodeAnalysisOutput,
584 ) -> Result<Option<fallow_types::envelope::TypeAwareMeta>, crate::ProgrammaticError> {
585 let meta = crate::type_aware::refine_programmatic_dead_code_in_session(
586 semantic_session,
587 changes,
588 options,
589 filters,
590 &self.inner,
591 &mut output.results,
592 )?;
593 fallow_engine::dead_code::apply_rule_severities(&mut output.results, self.inner.config());
594 Ok(meta)
595 }
596
597 pub fn analyze_project_with(
603 &self,
604 duplicates_config: &fallow_config::DuplicatesConfig,
605 retain_complexity_artifacts: bool,
606 ) -> fallow_engine::EngineResult<EditorProjectAnalysisOutput> {
607 self.inner
608 .analyze_project_with(duplicates_config, retain_complexity_artifacts)
609 .map(EditorProjectAnalysisOutput::from_engine)
610 .map(|output| self.with_resolved_rule_severities(output))
611 }
612
613 pub fn analyze_project_with_changed_files(
626 &self,
627 duplicates_config: &fallow_config::DuplicatesConfig,
628 retain_complexity_artifacts: bool,
629 changed_files: Option<&FxHashSet<PathBuf>>,
630 ) -> fallow_engine::EngineResult<EditorProjectAnalysisOutput> {
631 self.inner
632 .analyze_project_with_artifacts(
633 duplicates_config,
634 fallow_engine::project_analysis::ProjectAnalysisArtifactOptions {
635 retain_complexity_artifacts,
636 changed_files: changed_files.cloned(),
637 ..fallow_engine::project_analysis::ProjectAnalysisArtifactOptions::default()
638 },
639 )
640 .map(fallow_engine::project_analysis::ProjectAnalysisArtifacts::into_output)
641 .map(EditorProjectAnalysisOutput::from_engine)
642 .map(|output| self.with_resolved_rule_severities(output))
643 }
644
645 pub fn apply_change_scope(
656 &self,
657 output: &mut EditorProjectAnalysisOutput,
658 scope: &ChangeScope,
659 ) {
660 fallow_engine::dead_code::apply_scope(
661 &mut output.dead_code.results,
662 &fallow_engine::dead_code::DeadCodeScope {
663 workspace_roots: None,
664 changes: Some(scope),
665 diff: None,
666 files: None,
667 },
668 self.inner.config(),
669 );
670 fallow_engine::duplicates::apply_scope(
671 &mut output.duplication,
672 &fallow_engine::duplicates::DuplicationScope {
673 changes: Some(scope),
674 diff: None,
675 workspace_roots: None,
676 },
677 &self.inner.config().root,
678 );
679 }
680
681 fn with_resolved_rule_severities(
688 &self,
689 mut output: EditorProjectAnalysisOutput,
690 ) -> EditorProjectAnalysisOutput {
691 fallow_engine::dead_code::apply_rule_severities(
692 &mut output.dead_code.results,
693 self.inner.config(),
694 );
695 output
696 }
697
698 const fn from_engine(inner: fallow_engine::session::AnalysisSession) -> Self {
699 Self { inner }
700 }
701}
702
703#[derive(Debug)]
705pub struct EditorProjectAnalysisOutput {
706 pub dead_code: EditorDeadCodeAnalysisOutput,
708 pub duplication: EditorDuplicationReport,
710}
711
712impl EditorProjectAnalysisOutput {
713 fn from_engine(output: fallow_engine::project_analysis::ProjectAnalysisOutput) -> Self {
714 Self {
715 dead_code: EditorDeadCodeAnalysisOutput::from_engine(output.dead_code),
716 duplication: output.duplication,
717 }
718 }
719}
720
721#[derive(Debug, Default)]
723pub struct EditorAnalysisOutput {
724 pub results: EditorAnalysisResults,
726 pub duplication: EditorDuplicationReport,
728}
729
730impl EditorAnalysisOutput {
731 #[must_use]
733 pub const fn new(results: EditorAnalysisResults, duplication: EditorDuplicationReport) -> Self {
734 Self {
735 results,
736 duplication,
737 }
738 }
739
740 pub fn merge_project_output(&mut self, output: EditorProjectAnalysisOutput) {
742 self.merge_results(output.dead_code.results);
743 self.merge_duplication(output.duplication);
744 }
745
746 pub fn merge_results(&mut self, source: EditorAnalysisResults) {
748 self.results.merge_into(source);
749 }
750
751 pub fn merge_duplication(&mut self, source: EditorDuplicationReport) {
754 self.duplication.clone_groups.extend(source.clone_groups);
755 self.duplication
756 .clone_families
757 .extend(source.clone_families);
758 self.duplication
759 .mirrored_directories
760 .extend(source.mirrored_directories);
761 self.duplication.stats.clone_groups += source.stats.clone_groups;
762 self.duplication.stats.clone_families += source.stats.clone_families;
763 self.duplication.stats.clone_instances += source.stats.clone_instances;
764 self.duplication.stats.total_files += source.stats.total_files;
765 self.duplication.stats.files_with_clones += source.stats.files_with_clones;
766 self.duplication.stats.total_lines += source.stats.total_lines;
767 self.duplication.stats.duplicated_lines += source.stats.duplicated_lines;
768 self.duplication.stats.total_tokens += source.stats.total_tokens;
769 self.duplication.stats.duplicated_tokens += source.stats.duplicated_tokens;
770 self.duplication.stats.clone_groups_below_min_occurrences +=
771 source.stats.clone_groups_below_min_occurrences;
772 self.duplication.stats.clone_groups_ignored += source.stats.clone_groups_ignored;
773 self.duplication.stats.near_candidates_skipped += source.stats.near_candidates_skipped;
774 self.duplication.stats.duplication_percentage = if self.duplication.stats.total_lines > 0 {
775 (self.duplication.stats.duplicated_lines as f64
776 / self.duplication.stats.total_lines as f64)
777 * 100.0
778 } else {
779 0.0
780 };
781 }
782
783 pub fn filter_by_changed_files(&mut self, changed_files: &FxHashSet<PathBuf>, root: &Path) {
792 fallow_engine::changed_files::filter_results_by_changed_files(
793 &mut self.results,
794 changed_files,
795 );
796 fallow_engine::changed_files::filter_duplication_by_changed_files(
797 &mut self.duplication,
798 changed_files,
799 root,
800 );
801 }
802}
803
804#[cfg(test)]
805pub(crate) mod tests {
806 use super::*;
807
808 use fallow_types::duplicates::{CloneFamily, CloneGroup, CloneInstance, DuplicationStats};
809
810 use super::editor_results::{
811 BoundaryViolation, BoundaryViolationFinding, CircularDependency, CircularDependencyFinding,
812 DevDependencyInProduction, DevDependencyInProductionFinding, ExportUsage, SecuritySeverity,
813 TestOnlyDependency, TestOnlyDependencyFinding, TypeOnlyDependency, UnlistedDependency,
814 UnlistedDependencyFinding, UnusedClassMemberFinding, UnusedDependency,
815 UnusedDependencyFinding, UnusedDevDependencyFinding, UnusedEnumMemberFinding, UnusedExport,
816 UnusedExportFinding, UnusedFile, UnusedFileFinding, UnusedMember,
817 UnusedOptionalDependencyFinding, UnusedStoreMemberFinding, UnusedTypeFinding,
818 };
819
820 #[test]
821 fn merges_duplication_stats_and_recomputes_percentage() {
822 let mut output = EditorAnalysisOutput {
823 duplication: EditorDuplicationReport {
824 clone_groups: vec![CloneGroup {
825 instances: vec![CloneInstance {
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]
959 fn changed_files_scope_runs_after_the_analysis_keeps_all_unused_files() {
960 let temp = tempfile::tempdir().expect("temp project");
961 let root = temp.path().canonicalize().expect("canonical root");
962 let src = root.join("src");
963 std::fs::create_dir_all(&src).expect("src dir");
964 std::fs::write(
965 root.join("package.json"),
966 r#"{"name":"editor-scope-order","main":"src/a.ts"}"#,
967 )
968 .expect("package.json");
969 std::fs::write(src.join("a.ts"), "export const a = 1;\n").expect("source a");
970 std::fs::write(src.join("orphan.ts"), "export const orphan = 1;\n").expect("orphan");
971
972 let session = EditorAnalysisSession::load(&root, None).expect("session loads");
973 let mut changed_files = FxHashSet::default();
974 changed_files.insert(src.join("a.ts"));
975 let mut output = session
976 .analyze_project_with_changed_files(
977 &fallow_config::DuplicatesConfig::default(),
978 false,
979 Some(&changed_files),
980 )
981 .expect("analysis runs");
982 let unused_files = |output: &EditorProjectAnalysisOutput| {
983 output
984 .dead_code
985 .results
986 .unused_files
987 .iter()
988 .map(|finding| finding.file.path.clone())
989 .collect::<Vec<_>>()
990 };
991 assert_eq!(
992 unused_files(&output),
993 vec![src.join("orphan.ts")],
994 "the analysis keeps the unused file outside the changed set"
995 );
996
997 session.apply_change_scope(&mut output, &ChangeScope::changed_files(&changed_files));
998 assert!(
999 unused_files(&output).is_empty(),
1000 "the scope removes the unused file outside the changed set: {:?}",
1001 unused_files(&output)
1002 );
1003 }
1004
1005 #[test]
1006 fn editor_session_scopes_duplication_to_changed_files() {
1007 let temp = tempfile::tempdir().expect("temp project");
1008 let root = temp.path();
1009 let src = root.join("src");
1010 std::fs::create_dir_all(&src).expect("src dir");
1011 std::fs::write(
1012 root.join("package.json"),
1013 r#"{"name":"editor-api-session","main":"src/a.ts"}"#,
1014 )
1015 .expect("package.json");
1016 let repeated =
1017 "export function repeated() {\n return ['alpha', 'beta', 'gamma'].join(',');\n}\n";
1018 std::fs::write(src.join("a.ts"), repeated).expect("source a");
1019 std::fs::write(src.join("b.ts"), repeated).expect("source b");
1020
1021 let session = EditorAnalysisSession::load(root, None).expect("session loads");
1022 let mut config = session.config().duplicates.clone();
1023 config.min_tokens = 1;
1024 config.min_lines = 1;
1025 let full = session
1026 .analyze_project_with(&config, false)
1027 .expect("analysis runs");
1028 assert!(!full.duplication.clone_groups.is_empty());
1029
1030 let mut changed_files = FxHashSet::default();
1031 changed_files.insert(src.join("unrelated.ts"));
1032 let scoped = session
1033 .analyze_project_with_changed_files(&config, false, Some(&changed_files))
1034 .expect("analysis runs");
1035 assert!(scoped.duplication.clone_groups.is_empty());
1036 }
1037
1038 #[test]
1041 fn inline_complexity_applies_health_threshold_overrides() {
1042 let temp = tempfile::tempdir().expect("temp project");
1043 let root = temp.path();
1044 std::fs::create_dir_all(root.join("src")).expect("src dir");
1045 std::fs::write(
1046 root.join("package.json"),
1047 r#"{"name":"editor-inline-overrides","main":"src/index.ts"}"#,
1048 )
1049 .expect("package.json");
1050 std::fs::write(
1051 root.join(".fallowrc.json"),
1052 r#"{"health":{"maxCyclomatic":2,"maxCognitive":2,"thresholdOverrides":[{"files":["src/legacy.ts"],"maxCyclomatic":50,"maxCognitive":50}]}}"#,
1053 )
1054 .expect("config");
1055 let branchy = |name: &str| {
1056 format!(
1057 "export function {name}(value: number): number {{\n if (value > 1) {{ return 1; }}\n \
1058 if (value > 2) {{ return 2; }}\n if (value > 3) {{ return 3; }}\n return 0;\n}}\n"
1059 )
1060 };
1061 std::fs::write(root.join("src/app.ts"), branchy("appBranchy")).expect("app");
1062 std::fs::write(root.join("src/legacy.ts"), branchy("legacyBranchy")).expect("legacy");
1063 std::fs::write(
1064 root.join("src/index.ts"),
1065 "export { appBranchy } from \"./app\";\nexport { legacyBranchy } from \"./legacy\";\n",
1066 )
1067 .expect("index");
1068
1069 let session = EditorAnalysisSession::load(root, None).expect("session loads");
1070 let output = session
1071 .analyze_project_with(&session.config().duplicates.clone(), true)
1072 .expect("analysis runs");
1073 let names = collect_inline_complexity(session.config(), &output.dead_code)
1074 .into_iter()
1075 .map(|finding| finding.name)
1076 .collect::<Vec<_>>();
1077
1078 assert_eq!(
1079 names,
1080 vec!["appBranchy".to_string()],
1081 "the override raises the ceilings for src/legacy.ts, as in `fallow health`"
1082 );
1083 }
1084
1085 fn make_inline_finding(path: PathBuf) -> EditorInlineComplexityFinding {
1086 EditorInlineComplexityFinding {
1087 path,
1088 name: "myFn".to_string(),
1089 line: 1,
1090 col: 0,
1091 cyclomatic: 5,
1092 cognitive: 4,
1093 exceeded: EditorInlineComplexityExceeded::Cyclomatic,
1094 }
1095 }
1096
1097 #[test]
1098 fn filter_inline_complexity_keeps_findings_in_changed_set() {
1099 let changed: FxHashSet<PathBuf> = [PathBuf::from("/src/a.ts"), PathBuf::from("/src/b.ts")]
1100 .into_iter()
1101 .collect();
1102 let mut findings = vec![
1103 make_inline_finding(PathBuf::from("/src/a.ts")),
1104 make_inline_finding(PathBuf::from("/src/c.ts")),
1105 ];
1106
1107 filter_inline_complexity_by_changed_files(&mut findings, &changed);
1108
1109 assert_eq!(findings.len(), 1);
1110 assert_eq!(
1111 findings[0].path.to_string_lossy().replace('\\', "/"),
1112 "/src/a.ts"
1113 );
1114 }
1115
1116 #[test]
1117 fn filter_inline_complexity_removes_all_when_changed_set_empty() {
1118 let changed: FxHashSet<PathBuf> = FxHashSet::default();
1119 let mut findings = vec![make_inline_finding(PathBuf::from("/src/a.ts"))];
1120
1121 filter_inline_complexity_by_changed_files(&mut findings, &changed);
1122
1123 assert!(
1124 findings.is_empty(),
1125 "empty changed-files set must drop all inline complexity findings"
1126 );
1127 }
1128
1129 #[test]
1130 fn filter_inline_complexity_keeps_all_when_all_in_changed_set() {
1131 let path_a = PathBuf::from("/src/a.ts");
1132 let path_b = PathBuf::from("/src/b.ts");
1133 let changed: FxHashSet<PathBuf> = [path_a.clone(), path_b.clone()].into_iter().collect();
1134 let mut findings = vec![make_inline_finding(path_a), make_inline_finding(path_b)];
1135
1136 filter_inline_complexity_by_changed_files(&mut findings, &changed);
1137
1138 assert_eq!(
1139 findings.len(),
1140 2,
1141 "all findings in the changed set must be retained"
1142 );
1143 }
1144
1145 #[test]
1146 fn editor_session_applies_per_path_rule_overrides() {
1147 let temp = tempfile::tempdir().expect("temp project");
1151 let root = temp.path();
1152 std::fs::create_dir_all(root.join("src/ui")).expect("ui dir");
1153 std::fs::create_dir_all(root.join("src/lib")).expect("lib dir");
1154 std::fs::write(
1155 root.join("package.json"),
1156 r#"{"name":"editor-override-rules","private":true,"main":"src/index.ts"}"#,
1157 )
1158 .expect("package.json");
1159 std::fs::write(
1160 root.join(".fallowrc.json"),
1161 r#"{
1162 "rules": { "unused-exports": "warn", "private-type-leaks": "warn" },
1163 "overrides": [
1164 {
1165 "files": ["src/ui/**"],
1166 "rules": { "unused-exports": "off", "private-type-leaks": "off" }
1167 }
1168 ]
1169}"#,
1170 )
1171 .expect("config");
1172 std::fs::write(
1173 root.join("src/index.ts"),
1174 "import { kitUsed } from './ui/kit';\nimport { libUsed } from './lib/util';\n\nexport const app = `${kitUsed}${libUsed}`;\n",
1175 )
1176 .expect("index");
1177 std::fs::write(
1178 root.join("src/ui/kit.ts"),
1179 "type Props = { label: string };\n\nexport const kitUsed = 'kit';\n\nexport const Unused = (props: Props) => props.label;\n",
1180 )
1181 .expect("kit");
1182 std::fs::write(
1183 root.join("src/lib/util.ts"),
1184 "type Internal = { id: string };\n\nexport const libUsed = 'lib';\n\nexport const alsoUnused = (value: Internal) => value.id;\n",
1185 )
1186 .expect("util");
1187
1188 let session = EditorAnalysisSession::load(root, None).expect("session loads");
1189 let output = session
1190 .analyze_project_with_changed_files(
1191 &fallow_config::DuplicatesConfig::default(),
1192 false,
1193 None,
1194 )
1195 .expect("analysis runs");
1196 let results = &output.dead_code.results;
1197
1198 let unused_export_paths = || {
1199 results
1200 .unused_exports
1201 .iter()
1202 .map(|finding| finding.export.path.clone())
1203 .collect::<Vec<_>>()
1204 };
1205 let leak_paths = || {
1206 results
1207 .private_type_leaks
1208 .iter()
1209 .map(|finding| finding.leak.path.clone())
1210 .collect::<Vec<_>>()
1211 };
1212
1213 assert!(
1214 !unused_export_paths()
1215 .iter()
1216 .any(|path| path.ends_with("kit.ts")),
1217 "the override turns unused-exports off for src/ui/**: {:?}",
1218 unused_export_paths()
1219 );
1220 assert!(
1221 !leak_paths().iter().any(|path| path.ends_with("kit.ts")),
1222 "the override turns private-type-leaks off for src/ui/**: {:?}",
1223 leak_paths()
1224 );
1225 assert!(
1226 unused_export_paths()
1227 .iter()
1228 .any(|path| path.ends_with("util.ts")),
1229 "paths outside the override keep their unused export: {:?}",
1230 unused_export_paths()
1231 );
1232 assert!(
1233 leak_paths().iter().any(|path| path.ends_with("util.ts")),
1234 "paths outside the override keep their private type leak: {:?}",
1235 leak_paths()
1236 );
1237 }
1238
1239 #[test]
1240 fn merge_results_covers_all_fields() {
1241 let mut output = EditorAnalysisOutput::default();
1242
1243 output.merge_results(merge_test_source_with_all_fields());
1244
1245 let target = &output.results;
1246
1247 assert_eq!(target.unused_files.len(), 1);
1248 assert_eq!(target.unused_exports.len(), 1);
1249 assert_eq!(target.unused_types.len(), 1);
1250 assert_eq!(target.private_type_leaks.len(), 1);
1251 assert_eq!(target.deprecated_exports_in_use.len(), 1);
1252 assert_eq!(target.unused_dependencies.len(), 1);
1253 assert_eq!(target.unused_dev_dependencies.len(), 1);
1254 assert_eq!(target.unused_optional_dependencies.len(), 1);
1255 assert_eq!(target.unused_enum_members.len(), 1);
1256 assert_eq!(target.unused_class_members.len(), 1);
1257 assert_eq!(target.unused_store_members.len(), 1);
1258 assert_eq!(target.unresolved_imports.len(), 1);
1259 assert_eq!(target.unlisted_dependencies.len(), 1);
1260 assert_eq!(target.duplicate_exports.len(), 1);
1261 assert_eq!(target.type_only_dependencies.len(), 1);
1262 assert_eq!(target.test_only_dependencies.len(), 1);
1263 assert_eq!(target.circular_dependencies.len(), 1);
1264 assert_eq!(target.re_export_cycles.len(), 1);
1265 assert_eq!(target.package_cycles.len(), 1);
1266 assert_eq!(target.boundary_violations.len(), 1);
1267 assert_eq!(target.boundary_call_violations.len(), 1);
1268 assert_eq!(target.policy_violations.len(), 1);
1269 assert_eq!(target.stale_suppressions.len(), 1);
1270 assert_eq!(target.unused_catalog_entries.len(), 1);
1271 assert_eq!(target.empty_catalog_groups.len(), 1);
1272 assert_eq!(target.unresolved_catalog_references.len(), 1);
1273 assert_eq!(target.unused_dependency_overrides.len(), 1);
1274 assert_eq!(target.misconfigured_dependency_overrides.len(), 1);
1275 assert_eq!(target.invalid_client_exports.len(), 1);
1276 assert_eq!(target.mixed_client_server_barrels.len(), 1);
1277 assert_eq!(target.misplaced_directives.len(), 1);
1278 assert_eq!(target.export_usages.len(), 1);
1279 assert_eq!(target.feature_flags.len(), 1);
1280 assert_eq!(target.security_findings.len(), 1);
1281 assert_eq!(target.security_unresolved_edge_files, 2);
1282 assert_eq!(target.security_unresolved_callee_diagnostics.len(), 1);
1283 assert_eq!(target.suppression_count, 1);
1284 assert!(target.entry_point_summary.is_some());
1285 assert_eq!(
1286 target
1287 .render_fan_in
1288 .as_ref()
1289 .and_then(|m| m.max_distinct_parents),
1290 Some(3)
1291 );
1292 assert_eq!(target.react_component_intel.len(), 1);
1293 assert_eq!(target.dev_dependencies_in_production.len(), 1);
1294 assert_eq!(target.boundary_coverage_violations.len(), 1);
1295 assert_eq!(target.route_collisions.len(), 1);
1296 assert_eq!(target.dynamic_segment_name_conflicts.len(), 1);
1297 assert_eq!(target.unprovided_injects.len(), 1);
1298 assert_eq!(target.unrendered_components.len(), 1);
1299 assert_eq!(target.unused_component_props.len(), 1);
1300 assert_eq!(target.unused_component_emits.len(), 1);
1301 assert_eq!(target.unused_component_inputs.len(), 1);
1302 assert_eq!(target.unused_component_outputs.len(), 1);
1303 assert_eq!(target.unused_svelte_events.len(), 1);
1304 assert_eq!(target.unused_server_actions.len(), 1);
1305 assert_eq!(target.unused_load_data_keys.len(), 1);
1306 assert!(target.unused_load_data_keys_global_abstain);
1307 assert_eq!(target.prop_drilling_chains.len(), 1);
1308 assert_eq!(target.thin_wrappers.len(), 1);
1309 assert_eq!(target.duplicate_prop_shapes.len(), 1);
1310 assert_eq!(target.active_suppressions.len(), 1);
1311 assert_eq!(target.semantic_framework_contracts.len(), 1);
1312 assert_eq!(target.security_unresolved_callee_sites, 3);
1313 assert_eq!(target.unused_component_props_exempted, 1);
1314 }
1315
1316 #[test]
1317 fn merge_duplication_recomputes_percentage() {
1318 let target = EditorDuplicationReport {
1319 clone_groups: vec![],
1320 clone_families: vec![],
1321 mirrored_directories: vec![],
1322 stats: DuplicationStats {
1323 total_files: 5,
1324 files_with_clones: 1,
1325 total_lines: 200,
1326 duplicated_lines: 20,
1327 total_tokens: 1000,
1328 duplicated_tokens: 100,
1329 clone_groups: 1,
1330 clone_families: 0,
1331 clone_instances: 2,
1332 duplication_percentage: 10.0, clone_groups_below_min_occurrences: 0,
1334 clone_groups_ignored: 0,
1335 near_candidates_skipped: 0,
1336 },
1337 };
1338 let source = EditorDuplicationReport {
1339 clone_groups: vec![],
1340 clone_families: vec![],
1341 mirrored_directories: vec![],
1342 stats: DuplicationStats {
1343 total_files: 3,
1344 files_with_clones: 1,
1345 total_lines: 300,
1346 duplicated_lines: 60,
1347 total_tokens: 1500,
1348 duplicated_tokens: 300,
1349 clone_groups: 2,
1350 clone_families: 0,
1351 clone_instances: 4,
1352 duplication_percentage: 20.0, clone_groups_below_min_occurrences: 0,
1354 clone_groups_ignored: 0,
1355 near_candidates_skipped: 0,
1356 },
1357 };
1358
1359 let mut output = EditorAnalysisOutput::new(EditorAnalysisResults::default(), target);
1360 output.merge_duplication(source);
1361
1362 let target = &output.duplication;
1363 assert_eq!(target.stats.total_files, 8);
1364 assert_eq!(target.stats.files_with_clones, 2);
1365 assert_eq!(target.stats.total_lines, 500);
1366 assert_eq!(target.stats.duplicated_lines, 80);
1367 assert_eq!(target.stats.total_tokens, 2500);
1368 assert_eq!(target.stats.duplicated_tokens, 400);
1369 assert_eq!(target.stats.clone_groups, 3);
1370 assert_eq!(target.stats.clone_instances, 6);
1371 assert!((target.stats.duplication_percentage - 16.0).abs() < f64::EPSILON);
1372 }
1373
1374 #[test]
1375 fn merge_duplication_zero_total_lines_yields_zero_percentage() {
1376 let mut output = EditorAnalysisOutput::default();
1377
1378 output.merge_duplication(EditorDuplicationReport::default());
1379
1380 let target = &output.duplication;
1381
1382 assert_eq!(target.stats.total_lines, 0);
1383 assert!((target.stats.duplication_percentage - 0.0).abs() < f64::EPSILON);
1384 }
1385
1386 fn merge_test_unused_export(
1387 path: &str,
1388 export_name: &str,
1389 is_type_only: bool,
1390 line: u32,
1391 ) -> UnusedExport {
1392 UnusedExport {
1393 path: path.into(),
1394 export_name: export_name.to_string(),
1395 is_type_only,
1396 line,
1397 col: 0,
1398 span_start: 0,
1399 is_re_export: false,
1400 deprecated: false,
1401 deprecated_reason: None,
1402 }
1403 }
1404
1405 fn merge_test_unused_dependency(
1406 package_name: &str,
1407 location: super::editor_results::DependencyLocation,
1408 line: u32,
1409 ) -> UnusedDependency {
1410 UnusedDependency {
1411 package_name: package_name.to_string(),
1412 location,
1413 path: "/pkg.json".into(),
1414 line,
1415 used_in_workspaces: Vec::new(),
1416 }
1417 }
1418
1419 fn merge_test_unused_member(
1420 parent_name: &str,
1421 member_name: &str,
1422 kind: super::editor_extract::MemberKind,
1423 line: u32,
1424 ) -> UnusedMember {
1425 UnusedMember {
1426 path: "/f.ts".into(),
1427 parent_name: parent_name.to_string(),
1428 member_name: member_name.to_string(),
1429 kind,
1430 line,
1431 col: 0,
1432 }
1433 }
1434
1435 #[expect(
1436 clippy::too_many_lines,
1437 reason = "intentionally names every EditorAnalysisResults field (no ..Default::default()) so a new field is a compile error here; see #444"
1438 )]
1439 pub fn merge_test_source_with_all_fields() -> EditorAnalysisResults {
1440 EditorAnalysisResults {
1441 unused_files: vec![UnusedFileFinding::with_actions(UnusedFile {
1442 path: "/f.ts".into(),
1443 })],
1444 unused_exports: vec![UnusedExportFinding::with_actions(merge_test_unused_export(
1445 "/f.ts", "e", false, 1,
1446 ))],
1447 unused_types: vec![UnusedTypeFinding::with_actions(merge_test_unused_export(
1448 "/f.ts", "T", true, 2,
1449 ))],
1450 unused_dependencies: vec![UnusedDependencyFinding::with_actions(
1451 merge_test_unused_dependency(
1452 "dep",
1453 super::editor_results::DependencyLocation::Dependencies,
1454 3,
1455 ),
1456 )],
1457 unused_dev_dependencies: vec![UnusedDevDependencyFinding::with_actions(
1458 merge_test_unused_dependency(
1459 "dev-dep",
1460 super::editor_results::DependencyLocation::DevDependencies,
1461 4,
1462 ),
1463 )],
1464 unused_optional_dependencies: vec![UnusedOptionalDependencyFinding::with_actions(
1465 merge_test_unused_dependency(
1466 "opt-dep",
1467 super::editor_results::DependencyLocation::OptionalDependencies,
1468 5,
1469 ),
1470 )],
1471 unused_enum_members: vec![UnusedEnumMemberFinding::with_actions(
1472 merge_test_unused_member(
1473 "E",
1474 "A",
1475 super::editor_extract::MemberKind::EnumMember,
1476 6,
1477 ),
1478 )],
1479 unused_class_members: vec![UnusedClassMemberFinding::with_actions(
1480 merge_test_unused_member(
1481 "C",
1482 "m",
1483 super::editor_extract::MemberKind::ClassMethod,
1484 7,
1485 ),
1486 )],
1487 unused_store_members: vec![UnusedStoreMemberFinding::with_actions(
1488 merge_test_unused_member(
1489 "S",
1490 "a",
1491 super::editor_extract::MemberKind::StoreMember,
1492 7,
1493 ),
1494 )],
1495 unresolved_imports: vec![
1496 super::editor_results::UnresolvedImportFinding::with_actions(
1497 super::editor_results::UnresolvedImport {
1498 path: "/f.ts".into(),
1499 specifier: "./gone".to_string(),
1500 line: 8,
1501 col: 0,
1502 specifier_col: 10,
1503 },
1504 ),
1505 ],
1506 unlisted_dependencies: vec![UnlistedDependencyFinding::with_actions(
1507 UnlistedDependency {
1508 package_name: "unlisted".to_string(),
1509 imported_from: vec![],
1510 },
1511 )],
1512 duplicate_exports: vec![super::editor_results::DuplicateExportFinding::with_actions(
1513 super::editor_results::DuplicateExport {
1514 export_name: "dup".to_string(),
1515 locations: vec![],
1516 },
1517 )],
1518 type_only_dependencies: vec![
1519 super::editor_results::TypeOnlyDependencyFinding::with_actions(
1520 TypeOnlyDependency {
1521 package_name: "type-only".to_string(),
1522 path: "/pkg.json".into(),
1523 line: 9,
1524 },
1525 ),
1526 ],
1527 circular_dependencies: vec![CircularDependencyFinding::with_actions(
1528 CircularDependency {
1529 files: vec!["/a.ts".into(), "/b.ts".into()],
1530 length: 2,
1531 line: 10,
1532 col: 0,
1533 edges: Vec::new(),
1534 is_cross_package: false,
1535 },
1536 )],
1537 test_only_dependencies: vec![TestOnlyDependencyFinding::with_actions(
1538 TestOnlyDependency {
1539 package_name: "test-only".to_string(),
1540 path: "/pkg.json".into(),
1541 line: 11,
1542 },
1543 )],
1544 dev_dependencies_in_production: vec![DevDependencyInProductionFinding::with_actions(
1545 DevDependencyInProduction {
1546 package_name: "dev-in-prod".to_string(),
1547 path: "/pkg.json".into(),
1548 line: 12,
1549 },
1550 )],
1551 boundary_violations: vec![BoundaryViolationFinding::with_actions(BoundaryViolation {
1552 from_path: "/a.ts".into(),
1553 to_path: "/b.ts".into(),
1554 from_zone: "ui".to_string(),
1555 to_zone: "data".to_string(),
1556 import_specifier: "../data/db".to_string(),
1557 line: 12,
1558 col: 0,
1559 via_path: None,
1560 })],
1561 boundary_coverage_violations: vec![
1562 super::editor_results::BoundaryCoverageViolationFinding::with_actions(
1563 super::editor_results::BoundaryCoverageViolation {
1564 path: "/unzoned.ts".into(),
1565 line: 13,
1566 col: 0,
1567 },
1568 ),
1569 ],
1570 boundary_call_violations: vec![
1571 super::editor_results::BoundaryCallViolationFinding::with_actions(
1572 super::editor_results::BoundaryCallViolation {
1573 path: "/zoned.ts".into(),
1574 line: 14,
1575 col: 0,
1576 zone: "domain".to_string(),
1577 callee: "console.log".to_string(),
1578 pattern: "console.*".to_string(),
1579 },
1580 ),
1581 ],
1582 policy_violations: vec![super::editor_results::PolicyViolationFinding::with_actions(
1583 super::editor_results::PolicyViolation {
1584 path: "/zoned.ts".into(),
1585 line: 15,
1586 col: 0,
1587 pack: "team-policy".to_string(),
1588 rule_id: "no-console".to_string(),
1589 kind: super::editor_results::PolicyRuleKind::BannedCall,
1590 matched: "console.log".to_string(),
1591 severity: super::editor_results::PolicyViolationSeverity::Warn,
1592 message: None,
1593 },
1594 )],
1595 export_usages: vec![ExportUsage {
1596 path: "/f.ts".into(),
1597 export_name: "used".to_string(),
1598 line: 15,
1599 col: 0,
1600 reference_count: 3,
1601 reference_locations: vec![],
1602 }],
1603 private_type_leaks: vec![super::editor_results::PrivateTypeLeakFinding::with_actions(
1604 super::editor_results::PrivateTypeLeak {
1605 path: "/f.ts".into(),
1606 export_name: "pub_fn".to_string(),
1607 type_name: "Secret".to_string(),
1608 line: 14,
1609 col: 0,
1610 span_start: 0,
1611 semantic: None,
1612 },
1613 )],
1614 deprecated_exports_in_use: vec![
1615 super::editor_results::DeprecatedExportInUseFinding::with_actions(
1616 super::editor_results::DeprecatedExportInUse {
1617 path: "/f.ts".into(),
1618 export_name: "old".to_string(),
1619 is_type_only: false,
1620 line: 16,
1621 col: 0,
1622 span_start: 0,
1623 deprecated_reason: None,
1624 consumer_count: 1,
1625 consumers: vec![super::editor_results::DeprecatedExportConsumer {
1626 path: "/g.ts".into(),
1627 line: 1,
1628 col: 0,
1629 kind: super::editor_results::DeprecatedConsumerKind::NamedImport,
1630 }],
1631 public_api: false,
1632 },
1633 ),
1634 ],
1635 re_export_cycles: vec![super::editor_results::ReExportCycleFinding::with_actions(
1636 super::editor_results::ReExportCycle {
1637 files: vec!["/barrel.ts".into()],
1638 kind: super::editor_results::ReExportCycleKind::SelfLoop,
1639 },
1640 )],
1641 package_cycles: vec![super::editor_results::PackageCycleFinding::with_actions(
1642 super::editor_results::PackageCycle {
1643 packages: vec!["a".into(), "b".into()],
1644 package_roots: Vec::new(),
1645 length: 2,
1646 edges: vec![super::editor_results::PackageCycleEdge {
1647 from_package: "a".into(),
1648 to_package: "b".into(),
1649 path: "/a/x.ts".into(),
1650 target_path: "/b/y.ts".into(),
1651 line: 1,
1652 col: 0,
1653 type_only: false,
1654 }],
1655 group_truncated: false,
1656 },
1657 )],
1658 stale_suppressions: vec![super::editor_results::StaleSuppression {
1659 finding_id: None,
1660 path: "/f.ts".into(),
1661 line: 15,
1662 col: 0,
1663 origin: super::editor_results::SuppressionOrigin::Comment {
1664 issue_kind: None,
1665 reason: None,
1666 is_file_level: false,
1667 kind_known: true,
1668 },
1669 missing_reason: false,
1670 actions: super::editor_results::StaleSuppression::actions_for(false),
1671 effective_severity: None,
1672 }],
1673 unused_catalog_entries: vec![
1674 super::editor_results::UnusedCatalogEntryFinding::with_actions(
1675 super::editor_results::UnusedCatalogEntry {
1676 entry_name: "react".to_string(),
1677 catalog_name: "default".to_string(),
1678 path: "/pnpm-workspace.yaml".into(),
1679 line: 16,
1680 hardcoded_consumers: vec![],
1681 },
1682 ),
1683 ],
1684 empty_catalog_groups: vec![
1685 super::editor_results::EmptyCatalogGroupFinding::with_actions(
1686 super::editor_results::EmptyCatalogGroup {
1687 catalog_name: "ui".to_string(),
1688 path: "/pnpm-workspace.yaml".into(),
1689 line: 17,
1690 },
1691 ),
1692 ],
1693 unresolved_catalog_references: vec![
1694 super::editor_results::UnresolvedCatalogReferenceFinding::with_actions(
1695 super::editor_results::UnresolvedCatalogReference {
1696 entry_name: "vue".to_string(),
1697 catalog_name: "default".to_string(),
1698 path: "/pkg.json".into(),
1699 line: 18,
1700 available_in_catalogs: vec![],
1701 },
1702 ),
1703 ],
1704 unused_dependency_overrides: vec![
1705 super::editor_results::UnusedDependencyOverrideFinding::with_actions(
1706 super::editor_results::UnusedDependencyOverride {
1707 raw_key: "react".to_string(),
1708 target_package: "react".to_string(),
1709 parent_package: None,
1710 version_constraint: None,
1711 version_range: "18".to_string(),
1712 source: super::editor_results::DependencyOverrideSource::PnpmWorkspaceYaml,
1713 path: "/pnpm-workspace.yaml".into(),
1714 line: 19,
1715 hint: None,
1716 },
1717 ),
1718 ],
1719 misconfigured_dependency_overrides: vec![
1720 super::editor_results::MisconfiguredDependencyOverrideFinding::with_actions(
1721 super::editor_results::MisconfiguredDependencyOverride {
1722 raw_key: "bad>".to_string(),
1723 target_package: None,
1724 raw_value: String::new(),
1725 reason:
1726 super::editor_results::DependencyOverrideMisconfigReason::EmptyValue,
1727 source: super::editor_results::DependencyOverrideSource::PnpmPackageJson,
1728 path: "/pkg.json".into(),
1729 line: 20,
1730 },
1731 ),
1732 ],
1733 invalid_client_exports: vec![
1734 super::editor_results::InvalidClientExportFinding::with_actions(
1735 super::editor_results::InvalidClientExport {
1736 path: "/app/page.tsx".into(),
1737 export_name: "metadata".to_string(),
1738 directive: "use client".to_string(),
1739 line: 22,
1740 col: 0,
1741 },
1742 ),
1743 ],
1744 mixed_client_server_barrels: vec![
1745 super::editor_results::MixedClientServerBarrelFinding::with_actions(
1746 super::editor_results::MixedClientServerBarrel {
1747 path: "/app/components/index.ts".into(),
1748 client_origin: "./Button".to_string(),
1749 server_origin: "./fetchUser".to_string(),
1750 line: 23,
1751 col: 0,
1752 },
1753 ),
1754 ],
1755 misplaced_directives: vec![
1756 super::editor_results::MisplacedDirectiveFinding::with_actions(
1757 super::editor_results::MisplacedDirective {
1758 path: "/app/widget.tsx".into(),
1759 directive: "use client".to_string(),
1760 line: 24,
1761 col: 0,
1762 },
1763 ),
1764 ],
1765 unprovided_injects: vec![
1766 super::editor_results::UnprovidedInjectFinding::with_actions(
1767 super::editor_results::UnprovidedInject {
1768 path: "/Comp.vue".into(),
1769 key_name: "ApiKey".to_string(),
1770 framework: "vue".to_string(),
1771 line: 25,
1772 col: 0,
1773 },
1774 ),
1775 ],
1776 unrendered_components: vec![
1777 super::editor_results::UnrenderedComponentFinding::with_actions(
1778 super::editor_results::UnrenderedComponent {
1779 path: "/Widget.vue".into(),
1780 component_name: "Widget".to_string(),
1781 framework: "vue".to_string(),
1782 reachable_via: None,
1783 line: 26,
1784 col: 0,
1785 },
1786 ),
1787 ],
1788 unused_component_props: vec![
1789 super::editor_results::UnusedComponentPropFinding::with_actions(
1790 super::editor_results::UnusedComponentProp {
1791 path: "/Widget.vue".into(),
1792 component_name: "Widget".to_string(),
1793 prop_name: "size".to_string(),
1794 line: 27,
1795 col: 0,
1796 },
1797 ),
1798 ],
1799 unused_component_emits: vec![
1800 super::editor_results::UnusedComponentEmitFinding::with_actions(
1801 super::editor_results::UnusedComponentEmit {
1802 path: "/Widget.vue".into(),
1803 component_name: "Widget".to_string(),
1804 emit_name: "change".to_string(),
1805 line: 28,
1806 col: 0,
1807 },
1808 ),
1809 ],
1810 unused_component_inputs: vec![
1811 super::editor_results::UnusedComponentInputFinding::with_actions(
1812 super::editor_results::UnusedComponentInput {
1813 path: "/widget.component.ts".into(),
1814 component_name: "WidgetComponent".to_string(),
1815 input_name: "size".to_string(),
1816 line: 29,
1817 col: 0,
1818 },
1819 ),
1820 ],
1821 unused_component_outputs: vec![
1822 super::editor_results::UnusedComponentOutputFinding::with_actions(
1823 super::editor_results::UnusedComponentOutput {
1824 path: "/widget.component.ts".into(),
1825 component_name: "WidgetComponent".to_string(),
1826 output_name: "change".to_string(),
1827 line: 30,
1828 col: 0,
1829 },
1830 ),
1831 ],
1832 unused_svelte_events: vec![
1833 super::editor_results::UnusedSvelteEventFinding::with_actions(
1834 super::editor_results::UnusedSvelteEvent {
1835 path: "/Child.svelte".into(),
1836 component_name: "Child".to_string(),
1837 event_name: "dead".to_string(),
1838 line: 31,
1839 col: 0,
1840 },
1841 ),
1842 ],
1843 unused_server_actions: vec![
1844 super::editor_results::UnusedServerActionFinding::with_actions(
1845 super::editor_results::UnusedServerAction {
1846 path: "/app/actions.ts".into(),
1847 action_name: "createUser".to_string(),
1848 line: 32,
1849 col: 0,
1850 },
1851 ),
1852 ],
1853 unused_load_data_keys: vec![
1854 super::editor_results::UnusedLoadDataKeyFinding::with_actions(
1855 super::editor_results::UnusedLoadDataKey {
1856 path: "/src/routes/blog/+page.server.ts".into(),
1857 key_name: "posts".to_string(),
1858 line: 33,
1859 col: 0,
1860 route_dir: None,
1861 },
1862 ),
1863 ],
1864 unused_load_data_keys_global_abstain: true,
1865 prop_drilling_chains: vec![
1866 super::editor_results::PropDrillingChainFinding::with_actions(
1867 super::editor_results::PropDrillingChain {
1868 prop: "user".to_string(),
1869 depth: 1,
1870 hops: vec![super::editor_results::PropDrillHop {
1871 file: "/Hop.tsx".into(),
1872 line: 34,
1873 component: "Hop".to_string(),
1874 }],
1875 },
1876 ),
1877 ],
1878 thin_wrappers: vec![super::editor_results::ThinWrapperFinding::with_actions(
1879 super::editor_results::ThinWrapper {
1880 file: "/Wrapper.tsx".into(),
1881 line: 35,
1882 component: "Wrapper".to_string(),
1883 child_component: "Child".to_string(),
1884 },
1885 )],
1886 duplicate_prop_shapes: vec![
1887 super::editor_results::DuplicatePropShapeFinding::with_actions(
1888 super::editor_results::DuplicatePropShape {
1889 file: "/Card.tsx".into(),
1890 line: 36,
1891 component: "Card".to_string(),
1892 shape: vec!["subtitle".to_string(), "title".to_string()],
1893 group_size: 2,
1894 sharing_components: vec![],
1895 },
1896 ),
1897 ],
1898 route_collisions: vec![super::editor_results::RouteCollisionFinding::with_actions(
1899 super::editor_results::RouteCollision {
1900 path: "/app/(a)/about/page.tsx".into(),
1901 url: "/about".to_string(),
1902 conflicting_paths: vec!["/app/(b)/about/page.tsx".into()],
1903 line: 1,
1904 col: 0,
1905 },
1906 )],
1907 dynamic_segment_name_conflicts: vec![
1908 super::editor_results::DynamicSegmentNameConflictFinding::with_actions(
1909 super::editor_results::DynamicSegmentNameConflict {
1910 path: "/app/shop/[id]/page.tsx".into(),
1911 position: "/shop".to_string(),
1912 conflicting_segments: vec!["[id]".to_string(), "[slug]".to_string()],
1913 conflicting_paths: vec!["/app/shop/[slug]/edit/page.tsx".into()],
1914 line: 1,
1915 col: 0,
1916 },
1917 ),
1918 ],
1919 suppression_count: 1,
1920 unused_component_props_exempted: 1,
1921 active_suppressions: vec![super::editor_results::ActiveSuppression {
1922 path: "/f.ts".into(),
1923 kind: Some("unused-export".to_string()),
1924 is_file_level: false,
1925 reason: None,
1926 comment_line: 37,
1927 }],
1928 feature_flags: vec![super::editor_results::FeatureFlag {
1929 path: "/f.ts".into(),
1930 flag_name: "ENABLE_X".to_string(),
1931 kind: super::editor_results::FlagKind::EnvironmentVariable,
1932 confidence: super::editor_results::FlagConfidence::High,
1933 line: 21,
1934 col: 0,
1935 guard_span_start: None,
1936 guard_span_end: None,
1937 sdk_name: None,
1938 guard_line_start: None,
1939 guard_line_end: None,
1940 guarded_dead_exports: vec![],
1941 }],
1942 entry_point_summary: Some(super::editor_results::EntryPointSummary {
1943 total: 0,
1944 by_source: vec![],
1945 }),
1946 security_findings: vec![super::editor_results::SecurityFinding {
1947 finding_id: String::new(),
1948 candidate: super::editor_results::SecurityCandidate::default(),
1949 taint_flow: None,
1950 attack_surface: None,
1951 kind: super::editor_results::SecurityFindingKind::ClientServerLeak,
1952 category: None,
1953 cwe: None,
1954 path: "/client.tsx".into(),
1955 line: 1,
1956 col: 0,
1957 evidence: "transitively reaches DATABASE_URL".to_string(),
1958 source_backed: false,
1959 source_read: None,
1960 severity: SecuritySeverity::Low,
1961 trace: vec![],
1962 actions: vec![],
1963 dead_code: None,
1964 reachability: None,
1965 runtime: None,
1966 }],
1967 security_unresolved_edge_files: 2,
1968 security_unresolved_callee_sites: 3,
1969 security_unresolved_callee_diagnostics: vec![
1970 super::editor_results::SecurityUnresolvedCalleeDiagnostic {
1971 path: "/client.tsx".into(),
1972 line: 2,
1973 col: 0,
1974 reason: super::editor_extract::SkippedSecurityCalleeReason::DynamicDispatch,
1975 expression_kind:
1976 super::editor_extract::SkippedSecurityCalleeExpressionKind::Other,
1977 },
1978 ],
1979 render_fan_in: Some(super::editor_results::RenderFanInMetric {
1980 per_component: vec![super::editor_results::RenderFanInComponent {
1981 file: "/Button.tsx".into(),
1982 component: "Button".to_string(),
1983 render_sites: 6,
1984 distinct_parents: 3,
1985 }],
1986 p95_distinct_parents: Some(3),
1987 high_pct: Some(0.0),
1988 max_distinct_parents: Some(3),
1989 }),
1990 react_component_intel: vec![super::editor_results::ReactComponentIntel {
1991 path: "/Button.tsx".into(),
1992 component_name: "Button".to_string(),
1993 anchor_line: 1,
1994 anchor_col: 0,
1995 render_sites: 6,
1996 distinct_parents: 3,
1997 prop_count: 1,
1998 hooks: super::editor_results::ReactHookSummary::default(),
1999 props: Vec::new(),
2000 }],
2001 semantic_framework_contracts: vec![fallow_types::semantic::SemanticFrameworkContract {
2002 framework: "lit".to_string(),
2003 package: "lit".to_string(),
2004 heritage_symbol: "LitElement".to_string(),
2005 heritage_names: vec!["LitElement".to_string()],
2006 relation: fallow_types::semantic::SemanticFrameworkRelation::Extends,
2007 members: vec!["render".to_string()],
2008 }],
2009 }
2010 }
2011}