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;
10pub type EditorCloneGroup = fallow_types::duplicates::CloneGroup;
11pub type EditorCloneInstance = fallow_types::duplicates::CloneInstance;
12pub type EditorDuplicationReport = fallow_types::duplicates::DuplicationReport;
13pub type EditorDuplicationStats = fallow_types::duplicates::DuplicationStats;
14pub type EditorMirroredDirectory = fallow_types::duplicates::MirroredDirectory;
15pub type EditorRefactoringKind = fallow_types::duplicates::RefactoringKind;
16pub type EditorRefactoringSuggestion = fallow_types::duplicates::RefactoringSuggestion;
17
18#[derive(Debug, Clone)]
20pub struct EditorCloneFingerprintSet {
21 inner: fallow_engine::CloneFingerprintSet,
22}
23
24impl EditorCloneFingerprintSet {
25 #[must_use]
27 pub fn from_groups(groups: &[EditorCloneGroup]) -> Self {
28 Self {
29 inner: fallow_engine::CloneFingerprintSet::from_groups(groups),
30 }
31 }
32
33 #[must_use]
35 pub fn fingerprint_for_group(&self, group: &EditorCloneGroup) -> String {
36 self.inner.fingerprint_for_group(group)
37 }
38
39 #[must_use]
41 pub fn fingerprint_for_parts(
42 &self,
43 instances: &[EditorCloneInstance],
44 token_count: usize,
45 line_count: usize,
46 ) -> String {
47 self.inner
48 .fingerprint_for_parts(instances, token_count, line_count)
49 }
50
51 #[must_use]
53 pub fn find_group<'a>(
54 &self,
55 groups: &'a [EditorCloneGroup],
56 fingerprint: &str,
57 ) -> Option<&'a EditorCloneGroup> {
58 self.inner.find_group(groups, fingerprint)
59 }
60}
61
62pub mod editor_duplicates {
63 pub use crate::editor::{
64 EditorCloneFamily as CloneFamily, EditorCloneFingerprintSet as CloneFingerprintSet,
65 EditorCloneGroup as CloneGroup, EditorCloneInstance as CloneInstance,
66 EditorDuplicationReport as DuplicationReport, EditorDuplicationStats as DuplicationStats,
67 EditorMirroredDirectory as MirroredDirectory, EditorRefactoringKind as RefactoringKind,
68 EditorRefactoringSuggestion as RefactoringSuggestion,
69 };
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub enum ChangedFilesError {
75 InvalidRef(String),
77 GitMissing(String),
79 NotARepository,
81 GitFailed(String),
83}
84
85impl ChangedFilesError {
86 #[must_use]
88 pub fn describe(&self) -> String {
89 match self {
90 Self::InvalidRef(err) => format!("invalid git ref: {err}"),
91 Self::GitMissing(err) => format!("failed to run git: {err}"),
92 Self::NotARepository => "not a git repository".to_owned(),
93 Self::GitFailed(stderr) => {
94 let lower = stderr.to_ascii_lowercase();
95 if lower.contains("not a valid object name")
96 || lower.contains("unknown revision")
97 || lower.contains("ambiguous argument")
98 {
99 format!(
100 "{stderr} (shallow clone? try `git fetch --unshallow`, or set `fetch-depth: 0` on actions/checkout / `GIT_DEPTH: 0` in GitLab CI)"
101 )
102 } else {
103 stderr.clone()
104 }
105 }
106 }
107 }
108}
109
110impl From<fallow_engine::ChangedFilesError> for ChangedFilesError {
111 fn from(error: fallow_engine::ChangedFilesError) -> Self {
112 match error {
113 fallow_engine::ChangedFilesError::InvalidRef(err) => Self::InvalidRef(err),
114 fallow_engine::ChangedFilesError::GitMissing(err) => Self::GitMissing(err),
115 fallow_engine::ChangedFilesError::NotARepository => Self::NotARepository,
116 fallow_engine::ChangedFilesError::GitFailed(stderr) => Self::GitFailed(stderr),
117 }
118 }
119}
120
121pub fn resolve_git_toplevel(cwd: &Path) -> Result<PathBuf, ChangedFilesError> {
128 fallow_engine::resolve_git_toplevel(cwd).map_err(ChangedFilesError::from)
129}
130
131pub fn try_get_changed_files_with_toplevel(
138 cwd: &Path,
139 toplevel: &Path,
140 git_ref: &str,
141) -> Result<FxHashSet<PathBuf>, ChangedFilesError> {
142 fallow_engine::try_get_changed_files_with_toplevel(cwd, toplevel, git_ref)
143 .map_err(ChangedFilesError::from)
144}
145
146pub mod editor_extract {
147 pub use fallow_types::extract::{
148 AngularComponentSelector, AngularInputMember, AngularOutputMember,
149 AngularTemplateMemberAccessFact, AngularThisSpreadFact, CalleeUse, ClassHeritageInfo,
150 ComplexityContribution, ComplexityContributionKind, ComplexityMetric, ComponentEmit,
151 ComponentFunction, ComponentFunctionKind, ComponentProp, CssAnalytics, CssDeclarationBlock,
152 CssRuleMetric, DiFramework, DiKeySite, DiRole, DispatchedEvent,
153 DynamicCustomElementRenderFact, DynamicImportInfo, DynamicImportPattern, ExportInfo,
154 ExportName, FactoryCallMemberAccessFact, FactoryFnMemberAccessFact, FactoryReturnExport,
155 FlagUse, FlagUseKind, FluentChainMemberAccessFact, FluentChainNewMemberAccessFact,
156 ForwardAttr, FunctionComplexity, HookUse, HookUseKind, ImportInfo, ImportedName,
157 InstanceExportBindingFact, LoadReturnKey, LocalTypeDeclaration, MemberAccess, MemberInfo,
158 MemberKind, MisplacedDirectiveSite, ModuleInfo, NamespaceObjectAlias, PUBLIC_ENV_EXACT,
159 PUBLIC_ENV_METADATA_TOKENS, PUBLIC_ENV_PREFIXES, ParseResult, PlaywrightFixtureAliasFact,
160 PlaywrightFixtureDefinitionFact, PlaywrightFixtureTypeFact, PlaywrightFixtureUseFact,
161 PublicSignatureTypeReference, ReExportInfo, RegisteredCustomElement, RenderEdge,
162 RequireCallInfo, SECRET_ENV_TOKENS, SanitizedSinkArg, SanitizerScope, SecurityControlKind,
163 SecurityControlSite, SecurityUrlShape, SemanticFact, SemanticFactView, SinkArgKind,
164 SinkLiteralValue, SinkObjectProperty, SinkShape, SinkSite,
165 SkippedSecurityCalleeExpressionKind, SkippedSecurityCalleeReason,
166 SkippedSecurityCalleeSite, TaintedBinding, VisibilityTag,
167 };
168}
169
170pub mod editor_results {
171 pub use fallow_types::output_dead_code::{
172 BoundaryCallViolationFinding, BoundaryCoverageViolationFinding, BoundaryViolationFinding,
173 CircularDependencyFinding, DuplicateExportFinding, DuplicatePropShapeFinding,
174 DynamicSegmentNameConflictFinding, EmptyCatalogGroupFinding, InvalidClientExportFinding,
175 MisconfiguredDependencyOverrideFinding, MisplacedDirectiveFinding,
176 MixedClientServerBarrelFinding, PolicyViolationFinding, PrivateTypeLeakFinding,
177 PropDrillingChainFinding, ReExportCycleFinding, RouteCollisionFinding,
178 TestOnlyDependencyFinding, ThinWrapperFinding, TypeOnlyDependencyFinding,
179 UnlistedDependencyFinding, UnprovidedInjectFinding, UnrenderedComponentFinding,
180 UnresolvedCatalogReferenceFinding, UnresolvedImportFinding, UnusedCatalogEntryFinding,
181 UnusedClassMemberFinding, UnusedComponentEmitFinding, UnusedComponentInputFinding,
182 UnusedComponentOutputFinding, UnusedComponentPropFinding, UnusedDependencyFinding,
183 UnusedDependencyOverrideFinding, UnusedDevDependencyFinding, UnusedEnumMemberFinding,
184 UnusedExportFinding, UnusedFileFinding, UnusedLoadDataKeyFinding,
185 UnusedOptionalDependencyFinding, UnusedServerActionFinding, UnusedStoreMemberFinding,
186 UnusedSvelteEventFinding, UnusedTypeFinding,
187 };
188 pub use fallow_types::results::{
189 ActiveSuppression, AnalysisResults, BoundaryCallViolation, BoundaryCoverageViolation,
190 BoundaryViolation, CircularDependency, CircularDependencyEdge, DependencyLocation,
191 DependencyOverrideMisconfigReason, DependencyOverrideSource, DuplicateExport,
192 DuplicateLocation, DuplicatePropShape, DuplicatePropShapeMember,
193 DynamicSegmentNameConflict, EmptyCatalogGroup, EntryPointSummary, ExportUsage, FeatureFlag,
194 FlagConfidence, FlagKind, ImportSite, InvalidClientExport, MisconfiguredDependencyOverride,
195 MisplacedDirective, MixedClientServerBarrel, PolicyRuleKind, PolicyViolation,
196 PolicyViolationSeverity, PrivateTypeLeak, PropDrillHop, PropDrillingChain, ReExportCycle,
197 ReExportCycleKind, ReactComponentIntel, ReactHookSummary, ReactPropDrill, ReactPropIntel,
198 ReferenceLocation, RenderFanInComponent, RenderFanInMetric, RouteCollision,
199 SecurityAttackSurfaceEntry, SecurityCandidate, SecurityCandidateBoundary,
200 SecurityCandidateSink, SecurityDeadCodeContext, SecurityDeadCodeKind,
201 SecurityDefensiveBoundary, SecurityDefensiveControl, SecurityFinding, SecurityFindingKind,
202 SecurityNetworkContext, SecurityReachability, SecurityRuntimeContext, SecurityRuntimeState,
203 SecuritySeverity, SecurityTaintFlow, SecurityUnresolvedCalleeDiagnostic,
204 SecurityZoneCrossing, StaleSuppression, SuppressionOrigin, TaintConfidence, TaintEndpoint,
205 TaintPath, TestOnlyDependency, ThinWrapper, TraceHop, TraceHopRole, TypeOnlyDependency,
206 UnlistedDependency, UnprovidedInject, UnrenderedComponent, UnresolvedCatalogReference,
207 UnresolvedImport, UnusedCatalogEntry, UnusedComponentEmit, UnusedComponentInput,
208 UnusedComponentOutput, UnusedComponentProp, UnusedDependency, UnusedDependencyOverride,
209 UnusedExport, UnusedFile, UnusedLoadDataKey, UnusedMember, UnusedServerAction,
210 UnusedSvelteEvent,
211 };
212}
213
214pub mod editor_security {
215 #[must_use]
217 pub fn security_catalogue_title(kind: &str) -> Option<&'static str> {
218 fallow_engine::security_catalogue_title(kind)
219 }
220}
221
222pub mod editor_suppress {
223 pub use fallow_types::suppress::{IssueKind, is_suppressed};
224}
225
226pub type EditorAnalysisResults = fallow_types::results::AnalysisResults;
227
228#[derive(Debug)]
233pub struct EditorDeadCodeAnalysisOutput {
234 pub results: EditorAnalysisResults,
235 pub modules: Option<Vec<ModuleInfo>>,
236 pub files: Option<Vec<DiscoveredFile>>,
237}
238
239impl EditorDeadCodeAnalysisOutput {
240 fn from_engine(output: fallow_engine::DeadCodeAnalysisOutput) -> Self {
241 Self {
242 results: output.results,
243 modules: output.modules,
244 files: output.files,
245 }
246 }
247}
248
249#[derive(Debug, Clone, PartialEq, Eq)]
255pub struct EditorInlineComplexityFinding {
256 pub path: PathBuf,
257 pub name: String,
258 pub line: u32,
259 pub col: u32,
260 pub cyclomatic: u16,
261 pub cognitive: u16,
262 pub exceeded: EditorInlineComplexityExceeded,
263}
264
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub enum EditorInlineComplexityExceeded {
268 Cyclomatic,
269 Cognitive,
270 CyclomaticAndCognitive,
271}
272
273#[must_use]
275pub fn collect_inline_complexity(
276 config: &fallow_config::ResolvedConfig,
277 output: &EditorDeadCodeAnalysisOutput,
278) -> Vec<EditorInlineComplexityFinding> {
279 let Some(modules) = output.modules.as_ref() else {
280 return Vec::new();
281 };
282 let Some(files) = output.files.as_ref() else {
283 return Vec::new();
284 };
285
286 let file_paths: rustc_hash::FxHashMap<_, _> =
287 files.iter().map(|file| (file.id, &file.path)).collect();
288 let ignore_set = build_health_ignore_set(&config.health.ignore);
289 let mut findings = Vec::new();
290
291 for module in modules {
292 let Some(path) = file_paths.get(&module.file_id) else {
293 continue;
294 };
295 let relative = path.strip_prefix(&config.root).unwrap_or(path);
296 if ignore_set
297 .as_ref()
298 .is_some_and(|set| set.is_match(relative))
299 {
300 continue;
301 }
302
303 for function in &module.complexity {
304 if fallow_types::suppress::is_suppressed(
305 &module.suppressions,
306 function.line,
307 fallow_types::suppress::IssueKind::Complexity,
308 ) {
309 continue;
310 }
311
312 let exceeds_cyclomatic = function.cyclomatic > config.health.max_cyclomatic;
313 let exceeds_cognitive = function.cognitive > config.health.max_cognitive;
314 let exceeded = match (exceeds_cyclomatic, exceeds_cognitive) {
315 (true, true) => EditorInlineComplexityExceeded::CyclomaticAndCognitive,
316 (true, false) => EditorInlineComplexityExceeded::Cyclomatic,
317 (false, true) => EditorInlineComplexityExceeded::Cognitive,
318 (false, false) => continue,
319 };
320
321 findings.push(EditorInlineComplexityFinding {
322 path: (*path).clone(),
323 name: function.name.clone(),
324 line: function.line,
325 col: function.col,
326 cyclomatic: function.cyclomatic,
327 cognitive: function.cognitive,
328 exceeded,
329 });
330 }
331 }
332
333 findings
334}
335
336#[allow(
338 clippy::implicit_hasher,
339 reason = "editor analysis changed-file sets use the workspace FxHashSet convention"
340)]
341pub fn filter_inline_complexity_by_changed_files(
342 findings: &mut Vec<EditorInlineComplexityFinding>,
343 changed_files: &FxHashSet<PathBuf>,
344) {
345 findings.retain(|finding| changed_files.contains(&finding.path));
346}
347
348fn build_health_ignore_set(patterns: &[String]) -> Option<globset::GlobSet> {
349 if patterns.is_empty() {
350 return None;
351 }
352
353 let mut builder = globset::GlobSetBuilder::new();
354 for pattern in patterns {
355 let Ok(glob) = globset::Glob::new(pattern) else {
356 continue;
357 };
358 builder.add(glob);
359 }
360 builder.build().ok()
361}
362
363#[derive(Debug)]
365pub struct EditorAnalysisSession {
366 inner: fallow_engine::AnalysisSession,
367}
368
369impl EditorAnalysisSession {
370 pub fn load(root: &Path, config_path: Option<&Path>) -> fallow_engine::EngineResult<Self> {
376 fallow_engine::AnalysisSession::load(root, config_path).map(Self::from_engine)
377 }
378
379 pub fn load_with_config(
385 root: &Path,
386 config_path: Option<&Path>,
387 configure: impl FnOnce(&mut fallow_config::ResolvedConfig),
388 ) -> fallow_engine::EngineResult<Self> {
389 fallow_engine::AnalysisSession::load_with_config(root, config_path, configure)
390 .map(Self::from_engine)
391 }
392
393 #[must_use]
395 pub fn load_default(root: &Path) -> Self {
396 Self::from_engine(fallow_engine::AnalysisSession::load_default(root))
397 }
398
399 #[must_use]
401 pub fn config(&self) -> &fallow_config::ResolvedConfig {
402 self.inner.config()
403 }
404
405 #[must_use]
407 pub fn config_path(&self) -> Option<&Path> {
408 self.inner.config_path()
409 }
410
411 pub fn analyze_project_with(
417 &self,
418 duplicates_config: &fallow_config::DuplicatesConfig,
419 retain_complexity_artifacts: bool,
420 ) -> fallow_engine::EngineResult<EditorProjectAnalysisOutput> {
421 self.inner
422 .analyze_project_with(duplicates_config, retain_complexity_artifacts)
423 .map(EditorProjectAnalysisOutput::from_engine)
424 }
425
426 const fn from_engine(inner: fallow_engine::AnalysisSession) -> Self {
427 Self { inner }
428 }
429}
430
431#[derive(Debug)]
433pub struct EditorProjectAnalysisOutput {
434 pub dead_code: EditorDeadCodeAnalysisOutput,
435 pub duplication: EditorDuplicationReport,
436}
437
438impl EditorProjectAnalysisOutput {
439 fn from_engine(output: fallow_engine::ProjectAnalysisOutput) -> Self {
440 Self {
441 dead_code: EditorDeadCodeAnalysisOutput::from_engine(output.dead_code),
442 duplication: output.duplication,
443 }
444 }
445}
446
447#[derive(Debug, Default)]
449pub struct EditorAnalysisOutput {
450 pub results: EditorAnalysisResults,
451 pub duplication: EditorDuplicationReport,
452}
453
454impl EditorAnalysisOutput {
455 #[must_use]
456 pub const fn new(results: EditorAnalysisResults, duplication: EditorDuplicationReport) -> Self {
457 Self {
458 results,
459 duplication,
460 }
461 }
462
463 #[must_use]
464 pub fn from_project_output(output: EditorProjectAnalysisOutput) -> Self {
465 Self::new(output.dead_code.results, output.duplication)
466 }
467
468 pub fn merge_project_output(&mut self, output: EditorProjectAnalysisOutput) {
469 self.merge_results(output.dead_code.results);
470 self.merge_duplication(output.duplication);
471 }
472
473 pub fn merge_results(&mut self, source: EditorAnalysisResults) {
474 self.results.merge_into(source);
475 }
476
477 pub fn merge_duplication(&mut self, source: EditorDuplicationReport) {
478 self.duplication.clone_groups.extend(source.clone_groups);
479 self.duplication
480 .clone_families
481 .extend(source.clone_families);
482 self.duplication
483 .mirrored_directories
484 .extend(source.mirrored_directories);
485 self.duplication.stats.clone_groups += source.stats.clone_groups;
486 self.duplication.stats.clone_instances += source.stats.clone_instances;
487 self.duplication.stats.total_files += source.stats.total_files;
488 self.duplication.stats.files_with_clones += source.stats.files_with_clones;
489 self.duplication.stats.total_lines += source.stats.total_lines;
490 self.duplication.stats.duplicated_lines += source.stats.duplicated_lines;
491 self.duplication.stats.total_tokens += source.stats.total_tokens;
492 self.duplication.stats.duplicated_tokens += source.stats.duplicated_tokens;
493 self.duplication.stats.clone_groups_below_min_occurrences +=
494 source.stats.clone_groups_below_min_occurrences;
495 self.duplication.stats.duplication_percentage = if self.duplication.stats.total_lines > 0 {
496 (self.duplication.stats.duplicated_lines as f64
497 / self.duplication.stats.total_lines as f64)
498 * 100.0
499 } else {
500 0.0
501 };
502 }
503
504 pub fn filter_by_changed_files(&mut self, changed_files: &FxHashSet<PathBuf>, root: &Path) {
505 fallow_engine::filter_results_by_changed_files(&mut self.results, changed_files);
506 fallow_engine::filter_duplication_by_changed_files(
507 &mut self.duplication,
508 changed_files,
509 root,
510 );
511 }
512
513 pub fn filter_by_changed_since(
514 &mut self,
515 root: &Path,
516 toplevel: &Path,
517 git_ref: &str,
518 ) -> Result<usize, ChangedFilesError> {
519 let changed = try_get_changed_files_with_toplevel(root, toplevel, git_ref)?;
520 let changed_count = changed.len();
521 self.filter_by_changed_files(&changed, root);
522 Ok(changed_count)
523 }
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529
530 use fallow_types::duplicates::{CloneGroup, CloneInstance, DuplicationStats};
531
532 #[test]
533 fn merges_duplication_stats_and_recomputes_percentage() {
534 let mut output = EditorAnalysisOutput {
535 duplication: EditorDuplicationReport {
536 clone_groups: vec![CloneGroup {
537 instances: vec![CloneInstance {
538 file: PathBuf::from("src/a.ts"),
539 start_line: 1,
540 end_line: 4,
541 start_col: 0,
542 end_col: 10,
543 fragment: "const a = 1;".to_string(),
544 }],
545 token_count: 8,
546 line_count: 4,
547 }],
548 clone_families: Vec::new(),
549 mirrored_directories: Vec::new(),
550 stats: DuplicationStats {
551 clone_groups: 1,
552 clone_instances: 1,
553 total_files: 1,
554 files_with_clones: 1,
555 total_lines: 20,
556 duplicated_lines: 4,
557 total_tokens: 80,
558 duplicated_tokens: 8,
559 duplication_percentage: 20.0,
560 clone_groups_below_min_occurrences: 1,
561 },
562 },
563 ..Default::default()
564 };
565
566 output.merge_duplication(EditorDuplicationReport {
567 clone_groups: Vec::new(),
568 clone_families: Vec::new(),
569 mirrored_directories: Vec::new(),
570 stats: DuplicationStats {
571 clone_groups: 0,
572 clone_instances: 0,
573 total_files: 1,
574 files_with_clones: 0,
575 total_lines: 30,
576 duplicated_lines: 6,
577 total_tokens: 120,
578 duplicated_tokens: 12,
579 duplication_percentage: 20.0,
580 clone_groups_below_min_occurrences: 2,
581 },
582 });
583
584 assert_eq!(output.duplication.stats.total_lines, 50);
585 assert_eq!(output.duplication.stats.duplicated_lines, 10);
586 assert_eq!(
587 output.duplication.stats.clone_groups_below_min_occurrences,
588 3
589 );
590 assert!((output.duplication.stats.duplication_percentage - 20.0).abs() < f64::EPSILON);
591 }
592
593 #[test]
594 fn editor_session_returns_api_owned_project_output() {
595 let temp = tempfile::tempdir().expect("temp project");
596 let root = temp.path();
597 std::fs::create_dir_all(root.join("src")).expect("src dir");
598 std::fs::write(
599 root.join("package.json"),
600 r#"{"name":"editor-api-session","main":"src/index.ts"}"#,
601 )
602 .expect("package.json");
603 std::fs::write(
604 root.join("src/index.ts"),
605 "export const used = 1;\nconsole.log(used);\n",
606 )
607 .expect("source");
608
609 let session = EditorAnalysisSession::load(root, None).expect("session loads");
610 let output = session
611 .analyze_project_with(&fallow_config::DuplicatesConfig::default(), true)
612 .expect("analysis runs");
613
614 assert!(output.dead_code.modules.is_some());
615 assert!(
616 output
617 .dead_code
618 .files
619 .as_ref()
620 .is_some_and(|files| !files.is_empty())
621 );
622 }
623
624 #[test]
625 fn build_health_ignore_set_returns_none_for_empty_patterns() {
626 assert!(
627 build_health_ignore_set(&[]).is_none(),
628 "empty ignore pattern list should avoid building a matcher"
629 );
630 }
631
632 #[test]
633 fn build_health_ignore_set_matches_glob_patterns() {
634 let set =
635 build_health_ignore_set(&["**/*.test.ts".to_string(), "src/generated/**".to_string()])
636 .expect("valid patterns build a glob set");
637
638 assert!(set.is_match(Path::new("src/foo.test.ts")));
639 assert!(set.is_match(Path::new("src/generated/client.ts")));
640 assert!(!set.is_match(Path::new("src/app.ts")));
641 }
642
643 #[test]
644 fn build_health_ignore_set_skips_invalid_patterns() {
645 let result = build_health_ignore_set(&["[invalid-glob".to_string()]);
646
647 match result {
648 None => {}
649 Some(set) => assert!(
650 !set.is_match(Path::new("any/path.ts")),
651 "set built from only invalid patterns must not match anything"
652 ),
653 }
654 }
655
656 fn make_inline_finding(path: PathBuf) -> EditorInlineComplexityFinding {
657 EditorInlineComplexityFinding {
658 path,
659 name: "myFn".to_string(),
660 line: 1,
661 col: 0,
662 cyclomatic: 5,
663 cognitive: 4,
664 exceeded: EditorInlineComplexityExceeded::Cyclomatic,
665 }
666 }
667
668 #[test]
669 fn filter_inline_complexity_keeps_findings_in_changed_set() {
670 let changed: FxHashSet<PathBuf> = [PathBuf::from("/src/a.ts"), PathBuf::from("/src/b.ts")]
671 .into_iter()
672 .collect();
673 let mut findings = vec![
674 make_inline_finding(PathBuf::from("/src/a.ts")),
675 make_inline_finding(PathBuf::from("/src/c.ts")),
676 ];
677
678 filter_inline_complexity_by_changed_files(&mut findings, &changed);
679
680 assert_eq!(findings.len(), 1);
681 assert_eq!(
682 findings[0].path.to_string_lossy().replace('\\', "/"),
683 "/src/a.ts"
684 );
685 }
686
687 #[test]
688 fn filter_inline_complexity_removes_all_when_changed_set_empty() {
689 let changed: FxHashSet<PathBuf> = FxHashSet::default();
690 let mut findings = vec![make_inline_finding(PathBuf::from("/src/a.ts"))];
691
692 filter_inline_complexity_by_changed_files(&mut findings, &changed);
693
694 assert!(
695 findings.is_empty(),
696 "empty changed-files set must drop all inline complexity findings"
697 );
698 }
699
700 #[test]
701 fn filter_inline_complexity_keeps_all_when_all_in_changed_set() {
702 let path_a = PathBuf::from("/src/a.ts");
703 let path_b = PathBuf::from("/src/b.ts");
704 let changed: FxHashSet<PathBuf> = [path_a.clone(), path_b.clone()].into_iter().collect();
705 let mut findings = vec![make_inline_finding(path_a), make_inline_finding(path_b)];
706
707 filter_inline_complexity_by_changed_files(&mut findings, &changed);
708
709 assert_eq!(
710 findings.len(),
711 2,
712 "all findings in the changed set must be retained"
713 );
714 }
715}