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