fcis 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.plans/003-code-cleanup-consolidation.md +242 -0
- package/.plans/004-directory-depth-rollup.md +408 -0
- package/.plans/005-code-refinements.md +210 -0
- package/.plans/006-minor-refinements.md +149 -0
- package/.plans/007-compositional-function-scoring.md +514 -0
- package/README.md +38 -3
- package/TECHNICAL.md +125 -2
- package/dist/cli.js +595 -327
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +15 -2
- package/dist/index.js +409 -240
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/cli-utils.ts +201 -0
- package/src/cli.ts +99 -117
- package/src/detection/markers.ts +0 -222
- package/src/extraction/extract-functions.ts +106 -2
- package/src/extraction/extractor.ts +35 -74
- package/src/reporting/report-console.ts +188 -102
- package/src/reporting/report-json.ts +26 -3
- package/src/scoring/scorer.ts +425 -160
- package/src/types.ts +9 -2
- package/tests/classifier.test.ts +0 -1
- package/tests/cli.test.ts +356 -0
- package/tests/detect-markers.test.ts +1 -3
- package/tests/extractor.test.ts +95 -1
- package/tests/integration.test.ts +344 -0
- package/tests/report-console.test.ts +92 -0
- package/tests/scorer.test.ts +886 -0
package/tests/scorer.test.ts
CHANGED
|
@@ -13,8 +13,12 @@ import {
|
|
|
13
13
|
scoreDirectory,
|
|
14
14
|
scoreProject,
|
|
15
15
|
groupFilesByDirectory,
|
|
16
|
+
groupFunctionsByParent,
|
|
16
17
|
calculateDelta,
|
|
17
18
|
getDiagnosticInsights,
|
|
19
|
+
getPathAtDepth,
|
|
20
|
+
rollupDirectoriesByDepth,
|
|
21
|
+
aggregateMetrics,
|
|
18
22
|
} from '../src/scoring/scorer.js'
|
|
19
23
|
import type {
|
|
20
24
|
ClassifiedFunction,
|
|
@@ -48,6 +52,8 @@ function createClassifiedFunction(
|
|
|
48
52
|
classification: 'pure',
|
|
49
53
|
qualityScore: null,
|
|
50
54
|
status: 'ok',
|
|
55
|
+
enclosingFunctionStartLine: null,
|
|
56
|
+
isInlineCallback: false,
|
|
51
57
|
...overrides,
|
|
52
58
|
}
|
|
53
59
|
}
|
|
@@ -766,3 +772,883 @@ describe('getDiagnosticInsights', () => {
|
|
|
766
772
|
expect(Array.isArray(insights)).toBe(true)
|
|
767
773
|
})
|
|
768
774
|
})
|
|
775
|
+
|
|
776
|
+
describe('getPathAtDepth', () => {
|
|
777
|
+
it('should return first segment for depth 0', () => {
|
|
778
|
+
expect(getPathAtDepth('src/services/auth/utils', 0)).toBe('src')
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
it('should return first N+1 segments for depth N', () => {
|
|
782
|
+
expect(getPathAtDepth('src/services/auth/utils', 1)).toBe('src/services')
|
|
783
|
+
expect(getPathAtDepth('src/services/auth/utils', 2)).toBe(
|
|
784
|
+
'src/services/auth',
|
|
785
|
+
)
|
|
786
|
+
expect(getPathAtDepth('src/services/auth/utils', 3)).toBe(
|
|
787
|
+
'src/services/auth/utils',
|
|
788
|
+
)
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
it('should return full path when depth exceeds segments', () => {
|
|
792
|
+
expect(getPathAtDepth('src/utils', 5)).toBe('src/utils')
|
|
793
|
+
expect(getPathAtDepth('src', 10)).toBe('src')
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
it('should handle single segment paths', () => {
|
|
797
|
+
expect(getPathAtDepth('src', 0)).toBe('src')
|
|
798
|
+
expect(getPathAtDepth('src', 1)).toBe('src')
|
|
799
|
+
})
|
|
800
|
+
|
|
801
|
+
it('should handle empty path', () => {
|
|
802
|
+
expect(getPathAtDepth('', 0)).toBe('.')
|
|
803
|
+
})
|
|
804
|
+
|
|
805
|
+
it('should normalize backslashes to forward slashes', () => {
|
|
806
|
+
expect(getPathAtDepth('src\\services\\auth', 1)).toBe('src/services')
|
|
807
|
+
})
|
|
808
|
+
})
|
|
809
|
+
|
|
810
|
+
describe('rollupDirectoriesByDepth', () => {
|
|
811
|
+
/**
|
|
812
|
+
* Helper to create a directory score for rollup testing
|
|
813
|
+
*/
|
|
814
|
+
function createDirScoreForRollup(
|
|
815
|
+
dirPath: string,
|
|
816
|
+
overrides: Partial<DirectoryScore> = {},
|
|
817
|
+
): DirectoryScore {
|
|
818
|
+
return {
|
|
819
|
+
dirPath,
|
|
820
|
+
purity: 50,
|
|
821
|
+
impurityQuality: 60,
|
|
822
|
+
health: 75,
|
|
823
|
+
pureCount: 5,
|
|
824
|
+
impureCount: 5,
|
|
825
|
+
excludedCount: 0,
|
|
826
|
+
statusBreakdown: { ok: 7, review: 2, refactor: 1 },
|
|
827
|
+
pureLineCount: 50,
|
|
828
|
+
impureLineCount: 100,
|
|
829
|
+
fileScores: [],
|
|
830
|
+
...overrides,
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
it('should return empty array for empty input', () => {
|
|
835
|
+
const rolled = rollupDirectoriesByDepth([], 1)
|
|
836
|
+
expect(rolled).toHaveLength(0)
|
|
837
|
+
})
|
|
838
|
+
|
|
839
|
+
it('should aggregate directories at depth 0', () => {
|
|
840
|
+
const dirs = [
|
|
841
|
+
createDirScoreForRollup('src/services/auth', {
|
|
842
|
+
pureCount: 5,
|
|
843
|
+
impureCount: 5,
|
|
844
|
+
}),
|
|
845
|
+
createDirScoreForRollup('src/services/users', {
|
|
846
|
+
pureCount: 3,
|
|
847
|
+
impureCount: 7,
|
|
848
|
+
}),
|
|
849
|
+
createDirScoreForRollup('src/utils/format', {
|
|
850
|
+
pureCount: 8,
|
|
851
|
+
impureCount: 2,
|
|
852
|
+
}),
|
|
853
|
+
]
|
|
854
|
+
|
|
855
|
+
const rolled = rollupDirectoriesByDepth(dirs, 0)
|
|
856
|
+
|
|
857
|
+
expect(rolled).toHaveLength(1)
|
|
858
|
+
expect(rolled[0]?.dirPath).toBe('src')
|
|
859
|
+
expect(rolled[0]?.pureCount).toBe(16)
|
|
860
|
+
expect(rolled[0]?.impureCount).toBe(14)
|
|
861
|
+
})
|
|
862
|
+
|
|
863
|
+
it('should aggregate directories at depth 1', () => {
|
|
864
|
+
const dirs = [
|
|
865
|
+
createDirScoreForRollup('src/services/auth', {
|
|
866
|
+
pureCount: 5,
|
|
867
|
+
impureCount: 5,
|
|
868
|
+
}),
|
|
869
|
+
createDirScoreForRollup('src/services/users', {
|
|
870
|
+
pureCount: 3,
|
|
871
|
+
impureCount: 7,
|
|
872
|
+
}),
|
|
873
|
+
createDirScoreForRollup('src/utils/format', {
|
|
874
|
+
pureCount: 8,
|
|
875
|
+
impureCount: 2,
|
|
876
|
+
}),
|
|
877
|
+
]
|
|
878
|
+
|
|
879
|
+
const rolled = rollupDirectoriesByDepth(dirs, 1)
|
|
880
|
+
|
|
881
|
+
expect(rolled).toHaveLength(2)
|
|
882
|
+
expect(rolled.find(d => d.dirPath === 'src/services')?.pureCount).toBe(8)
|
|
883
|
+
expect(rolled.find(d => d.dirPath === 'src/services')?.impureCount).toBe(12)
|
|
884
|
+
expect(rolled.find(d => d.dirPath === 'src/utils')?.pureCount).toBe(8)
|
|
885
|
+
expect(rolled.find(d => d.dirPath === 'src/utils')?.impureCount).toBe(2)
|
|
886
|
+
})
|
|
887
|
+
|
|
888
|
+
it('should calculate weighted health correctly', () => {
|
|
889
|
+
const dirs = [
|
|
890
|
+
createDirScoreForRollup('src/a', {
|
|
891
|
+
pureCount: 10,
|
|
892
|
+
impureCount: 0,
|
|
893
|
+
statusBreakdown: { ok: 10, review: 0, refactor: 0 },
|
|
894
|
+
}),
|
|
895
|
+
createDirScoreForRollup('src/b', {
|
|
896
|
+
pureCount: 0,
|
|
897
|
+
impureCount: 10,
|
|
898
|
+
statusBreakdown: { ok: 0, review: 0, refactor: 10 },
|
|
899
|
+
}),
|
|
900
|
+
]
|
|
901
|
+
|
|
902
|
+
const rolled = rollupDirectoriesByDepth(dirs, 0)
|
|
903
|
+
|
|
904
|
+
// Weighted by function count: (10 ok out of 20 total) = 50%
|
|
905
|
+
expect(rolled[0]?.health).toBe(50)
|
|
906
|
+
})
|
|
907
|
+
|
|
908
|
+
it('should calculate weighted purity correctly', () => {
|
|
909
|
+
const dirs = [
|
|
910
|
+
createDirScoreForRollup('src/a', {
|
|
911
|
+
pureCount: 8,
|
|
912
|
+
impureCount: 2,
|
|
913
|
+
statusBreakdown: { ok: 10, review: 0, refactor: 0 },
|
|
914
|
+
}),
|
|
915
|
+
createDirScoreForRollup('src/b', {
|
|
916
|
+
pureCount: 2,
|
|
917
|
+
impureCount: 8,
|
|
918
|
+
statusBreakdown: { ok: 10, review: 0, refactor: 0 },
|
|
919
|
+
}),
|
|
920
|
+
]
|
|
921
|
+
|
|
922
|
+
const rolled = rollupDirectoriesByDepth(dirs, 0)
|
|
923
|
+
|
|
924
|
+
// Total: 10 pure, 10 impure = 50% purity
|
|
925
|
+
expect(rolled[0]?.purity).toBe(50)
|
|
926
|
+
})
|
|
927
|
+
|
|
928
|
+
it('should calculate weighted impurity quality correctly', () => {
|
|
929
|
+
const dirs = [
|
|
930
|
+
createDirScoreForRollup('src/a', {
|
|
931
|
+
pureCount: 0,
|
|
932
|
+
impureCount: 2,
|
|
933
|
+
impurityQuality: 80,
|
|
934
|
+
statusBreakdown: { ok: 2, review: 0, refactor: 0 },
|
|
935
|
+
}),
|
|
936
|
+
createDirScoreForRollup('src/b', {
|
|
937
|
+
pureCount: 0,
|
|
938
|
+
impureCount: 8,
|
|
939
|
+
impurityQuality: 40,
|
|
940
|
+
statusBreakdown: { ok: 8, review: 0, refactor: 0 },
|
|
941
|
+
}),
|
|
942
|
+
]
|
|
943
|
+
|
|
944
|
+
const rolled = rollupDirectoriesByDepth(dirs, 0)
|
|
945
|
+
|
|
946
|
+
// Weighted: (80*2 + 40*8) / 10 = 48
|
|
947
|
+
expect(rolled[0]?.impurityQuality).toBe(48)
|
|
948
|
+
})
|
|
949
|
+
|
|
950
|
+
it('should handle depth greater than actual nesting', () => {
|
|
951
|
+
const dirs = [
|
|
952
|
+
createDirScoreForRollup('src', { pureCount: 5, impureCount: 5 }),
|
|
953
|
+
]
|
|
954
|
+
|
|
955
|
+
const rolled = rollupDirectoriesByDepth(dirs, 5)
|
|
956
|
+
|
|
957
|
+
expect(rolled).toHaveLength(1)
|
|
958
|
+
expect(rolled[0]?.dirPath).toBe('src')
|
|
959
|
+
})
|
|
960
|
+
|
|
961
|
+
it('should sort results alphabetically by path', () => {
|
|
962
|
+
const dirs = [
|
|
963
|
+
createDirScoreForRollup('z/deep', {
|
|
964
|
+
pureCount: 1,
|
|
965
|
+
impureCount: 1,
|
|
966
|
+
}),
|
|
967
|
+
createDirScoreForRollup('a/deep', {
|
|
968
|
+
pureCount: 1,
|
|
969
|
+
impureCount: 1,
|
|
970
|
+
}),
|
|
971
|
+
createDirScoreForRollup('m/deep', {
|
|
972
|
+
pureCount: 1,
|
|
973
|
+
impureCount: 1,
|
|
974
|
+
}),
|
|
975
|
+
]
|
|
976
|
+
|
|
977
|
+
const rolled = rollupDirectoriesByDepth(dirs, 0)
|
|
978
|
+
|
|
979
|
+
expect(rolled.map(d => d.dirPath)).toEqual(['a', 'm', 'z'])
|
|
980
|
+
})
|
|
981
|
+
|
|
982
|
+
it('should have empty fileScores for rolled-up directories', () => {
|
|
983
|
+
const dirs = [
|
|
984
|
+
createDirScoreForRollup('src/a', {
|
|
985
|
+
pureCount: 5,
|
|
986
|
+
impureCount: 5,
|
|
987
|
+
}),
|
|
988
|
+
createDirScoreForRollup('src/b', {
|
|
989
|
+
pureCount: 5,
|
|
990
|
+
impureCount: 5,
|
|
991
|
+
}),
|
|
992
|
+
]
|
|
993
|
+
|
|
994
|
+
const rolled = rollupDirectoriesByDepth(dirs, 0)
|
|
995
|
+
|
|
996
|
+
expect(rolled[0]?.fileScores).toEqual([])
|
|
997
|
+
})
|
|
998
|
+
|
|
999
|
+
it('should aggregate excludedCount from all directories', () => {
|
|
1000
|
+
const dirs = [
|
|
1001
|
+
createDirScoreForRollup('src/a', {
|
|
1002
|
+
pureCount: 5,
|
|
1003
|
+
impureCount: 5,
|
|
1004
|
+
excludedCount: 3,
|
|
1005
|
+
}),
|
|
1006
|
+
createDirScoreForRollup('src/b', {
|
|
1007
|
+
pureCount: 5,
|
|
1008
|
+
impureCount: 5,
|
|
1009
|
+
excludedCount: 7,
|
|
1010
|
+
}),
|
|
1011
|
+
]
|
|
1012
|
+
|
|
1013
|
+
const rolled = rollupDirectoriesByDepth(dirs, 0)
|
|
1014
|
+
|
|
1015
|
+
expect(rolled[0]?.excludedCount).toBe(10)
|
|
1016
|
+
})
|
|
1017
|
+
|
|
1018
|
+
it('should handle directories with no functions', () => {
|
|
1019
|
+
const dirs = [
|
|
1020
|
+
createDirScoreForRollup('src/types', {
|
|
1021
|
+
pureCount: 0,
|
|
1022
|
+
impureCount: 0,
|
|
1023
|
+
statusBreakdown: { ok: 0, review: 0, refactor: 0 },
|
|
1024
|
+
}),
|
|
1025
|
+
]
|
|
1026
|
+
|
|
1027
|
+
const rolled = rollupDirectoriesByDepth(dirs, 0)
|
|
1028
|
+
|
|
1029
|
+
expect(rolled).toHaveLength(1)
|
|
1030
|
+
expect(rolled[0]?.purity).toBe(100)
|
|
1031
|
+
expect(rolled[0]?.health).toBe(100)
|
|
1032
|
+
expect(rolled[0]?.impurityQuality).toBeNull()
|
|
1033
|
+
})
|
|
1034
|
+
})
|
|
1035
|
+
|
|
1036
|
+
describe('aggregateMetrics', () => {
|
|
1037
|
+
it('should aggregate metrics from multiple items', () => {
|
|
1038
|
+
const items = [
|
|
1039
|
+
{
|
|
1040
|
+
pureCount: 5,
|
|
1041
|
+
impureCount: 5,
|
|
1042
|
+
impurityQuality: 80,
|
|
1043
|
+
statusBreakdown: { ok: 8, review: 1, refactor: 1 },
|
|
1044
|
+
pureLineCount: 50,
|
|
1045
|
+
impureLineCount: 100,
|
|
1046
|
+
excludedCount: 2,
|
|
1047
|
+
},
|
|
1048
|
+
{
|
|
1049
|
+
pureCount: 3,
|
|
1050
|
+
impureCount: 7,
|
|
1051
|
+
impurityQuality: 40,
|
|
1052
|
+
statusBreakdown: { ok: 5, review: 3, refactor: 2 },
|
|
1053
|
+
pureLineCount: 30,
|
|
1054
|
+
impureLineCount: 140,
|
|
1055
|
+
excludedCount: 1,
|
|
1056
|
+
},
|
|
1057
|
+
]
|
|
1058
|
+
|
|
1059
|
+
const aggregated = aggregateMetrics(items)
|
|
1060
|
+
|
|
1061
|
+
expect(aggregated.pureCount).toBe(8)
|
|
1062
|
+
expect(aggregated.impureCount).toBe(12)
|
|
1063
|
+
expect(aggregated.purity).toBe(40) // 8 / 20 = 40%
|
|
1064
|
+
expect(aggregated.statusBreakdown.ok).toBe(13)
|
|
1065
|
+
expect(aggregated.statusBreakdown.review).toBe(4)
|
|
1066
|
+
expect(aggregated.statusBreakdown.refactor).toBe(3)
|
|
1067
|
+
expect(aggregated.health).toBe(65) // 13 / 20 = 65%
|
|
1068
|
+
expect(aggregated.pureLineCount).toBe(80)
|
|
1069
|
+
expect(aggregated.impureLineCount).toBe(240)
|
|
1070
|
+
expect(aggregated.excludedCount).toBe(3)
|
|
1071
|
+
})
|
|
1072
|
+
|
|
1073
|
+
it('should return defaults for empty array', () => {
|
|
1074
|
+
const aggregated = aggregateMetrics([])
|
|
1075
|
+
|
|
1076
|
+
expect(aggregated.purity).toBe(100)
|
|
1077
|
+
expect(aggregated.health).toBe(100)
|
|
1078
|
+
expect(aggregated.impurityQuality).toBeNull()
|
|
1079
|
+
expect(aggregated.pureCount).toBe(0)
|
|
1080
|
+
expect(aggregated.impureCount).toBe(0)
|
|
1081
|
+
})
|
|
1082
|
+
|
|
1083
|
+
it('should calculate weighted impurity quality', () => {
|
|
1084
|
+
const items = [
|
|
1085
|
+
{
|
|
1086
|
+
pureCount: 0,
|
|
1087
|
+
impureCount: 2,
|
|
1088
|
+
impurityQuality: 100,
|
|
1089
|
+
statusBreakdown: { ok: 2, review: 0, refactor: 0 },
|
|
1090
|
+
pureLineCount: 0,
|
|
1091
|
+
impureLineCount: 20,
|
|
1092
|
+
excludedCount: 0,
|
|
1093
|
+
},
|
|
1094
|
+
{
|
|
1095
|
+
pureCount: 0,
|
|
1096
|
+
impureCount: 8,
|
|
1097
|
+
impurityQuality: 50,
|
|
1098
|
+
statusBreakdown: { ok: 8, review: 0, refactor: 0 },
|
|
1099
|
+
pureLineCount: 0,
|
|
1100
|
+
impureLineCount: 80,
|
|
1101
|
+
excludedCount: 0,
|
|
1102
|
+
},
|
|
1103
|
+
]
|
|
1104
|
+
|
|
1105
|
+
const aggregated = aggregateMetrics(items)
|
|
1106
|
+
|
|
1107
|
+
// Weighted: (100*2 + 50*8) / 10 = 60
|
|
1108
|
+
expect(aggregated.impurityQuality).toBe(60)
|
|
1109
|
+
})
|
|
1110
|
+
})
|
|
1111
|
+
|
|
1112
|
+
describe('groupFunctionsByParent', () => {
|
|
1113
|
+
it('should group inline callbacks under their parent function', () => {
|
|
1114
|
+
const parent = createClassifiedFunction({
|
|
1115
|
+
name: 'parentFunction',
|
|
1116
|
+
startLine: 1,
|
|
1117
|
+
endLine: 50,
|
|
1118
|
+
bodyLineCount: 50,
|
|
1119
|
+
isInlineCallback: false,
|
|
1120
|
+
enclosingFunctionStartLine: null,
|
|
1121
|
+
})
|
|
1122
|
+
|
|
1123
|
+
const callback1 = createClassifiedFunction({
|
|
1124
|
+
name: 'forEach',
|
|
1125
|
+
startLine: 10,
|
|
1126
|
+
endLine: 20,
|
|
1127
|
+
bodyLineCount: 11,
|
|
1128
|
+
parentContext: 'forEach',
|
|
1129
|
+
isInlineCallback: true,
|
|
1130
|
+
enclosingFunctionStartLine: 1, // links to parent
|
|
1131
|
+
})
|
|
1132
|
+
|
|
1133
|
+
const callback2 = createClassifiedFunction({
|
|
1134
|
+
name: 'map',
|
|
1135
|
+
startLine: 25,
|
|
1136
|
+
endLine: 30,
|
|
1137
|
+
bodyLineCount: 6,
|
|
1138
|
+
parentContext: 'map',
|
|
1139
|
+
isInlineCallback: true,
|
|
1140
|
+
enclosingFunctionStartLine: 1, // links to parent
|
|
1141
|
+
})
|
|
1142
|
+
|
|
1143
|
+
const grouped = groupFunctionsByParent([parent, callback1, callback2])
|
|
1144
|
+
|
|
1145
|
+
expect(grouped).toHaveLength(1) // only 1 top-level function
|
|
1146
|
+
expect(grouped[0]?.fn.name).toBe('parentFunction')
|
|
1147
|
+
expect(grouped[0]?.children).toHaveLength(2)
|
|
1148
|
+
})
|
|
1149
|
+
|
|
1150
|
+
it('should treat module-scope inline callbacks as top-level', () => {
|
|
1151
|
+
const moduleScopeCallback = createClassifiedFunction({
|
|
1152
|
+
name: 'map',
|
|
1153
|
+
startLine: 1,
|
|
1154
|
+
endLine: 10,
|
|
1155
|
+
bodyLineCount: 10,
|
|
1156
|
+
parentContext: 'map',
|
|
1157
|
+
isInlineCallback: true,
|
|
1158
|
+
enclosingFunctionStartLine: null, // module scope
|
|
1159
|
+
})
|
|
1160
|
+
|
|
1161
|
+
const grouped = groupFunctionsByParent([moduleScopeCallback])
|
|
1162
|
+
|
|
1163
|
+
expect(grouped).toHaveLength(1)
|
|
1164
|
+
expect(grouped[0]?.fn.name).toBe('map')
|
|
1165
|
+
expect(grouped[0]?.children).toHaveLength(0)
|
|
1166
|
+
})
|
|
1167
|
+
|
|
1168
|
+
it('should treat non-inline-callback functions as top-level', () => {
|
|
1169
|
+
const regularFn = createClassifiedFunction({
|
|
1170
|
+
name: 'regularFunction',
|
|
1171
|
+
isInlineCallback: false,
|
|
1172
|
+
enclosingFunctionStartLine: null,
|
|
1173
|
+
})
|
|
1174
|
+
|
|
1175
|
+
const trpcHandler = createClassifiedFunction({
|
|
1176
|
+
name: 'query',
|
|
1177
|
+
parentContext: 'query',
|
|
1178
|
+
isInlineCallback: false, // tRPC handlers are not inline callbacks
|
|
1179
|
+
enclosingFunctionStartLine: null,
|
|
1180
|
+
})
|
|
1181
|
+
|
|
1182
|
+
const grouped = groupFunctionsByParent([regularFn, trpcHandler])
|
|
1183
|
+
|
|
1184
|
+
expect(grouped).toHaveLength(2)
|
|
1185
|
+
})
|
|
1186
|
+
})
|
|
1187
|
+
|
|
1188
|
+
describe('compositional scoring', () => {
|
|
1189
|
+
it('should count only top-level functions', () => {
|
|
1190
|
+
const parent = createClassifiedFunction({
|
|
1191
|
+
name: 'processItems',
|
|
1192
|
+
startLine: 1,
|
|
1193
|
+
endLine: 50,
|
|
1194
|
+
bodyLineCount: 50,
|
|
1195
|
+
classification: 'pure',
|
|
1196
|
+
isInlineCallback: false,
|
|
1197
|
+
enclosingFunctionStartLine: null,
|
|
1198
|
+
})
|
|
1199
|
+
|
|
1200
|
+
const callback1 = createClassifiedFunction({
|
|
1201
|
+
name: 'map',
|
|
1202
|
+
startLine: 10,
|
|
1203
|
+
endLine: 20,
|
|
1204
|
+
bodyLineCount: 11,
|
|
1205
|
+
classification: 'pure',
|
|
1206
|
+
parentContext: 'map',
|
|
1207
|
+
isInlineCallback: true,
|
|
1208
|
+
enclosingFunctionStartLine: 1,
|
|
1209
|
+
})
|
|
1210
|
+
|
|
1211
|
+
const callback2 = createClassifiedFunction({
|
|
1212
|
+
name: 'filter',
|
|
1213
|
+
startLine: 25,
|
|
1214
|
+
endLine: 30,
|
|
1215
|
+
bodyLineCount: 6,
|
|
1216
|
+
classification: 'pure',
|
|
1217
|
+
parentContext: 'filter',
|
|
1218
|
+
isInlineCallback: true,
|
|
1219
|
+
enclosingFunctionStartLine: 1,
|
|
1220
|
+
})
|
|
1221
|
+
|
|
1222
|
+
const score = scoreFile('/test.ts', [parent, callback1, callback2])
|
|
1223
|
+
|
|
1224
|
+
// Should count only 1 top-level function, not 3
|
|
1225
|
+
expect(score.pureCount + score.impureCount).toBe(1)
|
|
1226
|
+
expect(score.pureCount).toBe(1)
|
|
1227
|
+
expect(score.impureCount).toBe(0)
|
|
1228
|
+
})
|
|
1229
|
+
|
|
1230
|
+
it('should not double-count line counts', () => {
|
|
1231
|
+
const parent = createClassifiedFunction({
|
|
1232
|
+
name: 'processItems',
|
|
1233
|
+
startLine: 1,
|
|
1234
|
+
endLine: 50,
|
|
1235
|
+
bodyLineCount: 50, // includes callback lines
|
|
1236
|
+
classification: 'pure',
|
|
1237
|
+
isInlineCallback: false,
|
|
1238
|
+
enclosingFunctionStartLine: null,
|
|
1239
|
+
})
|
|
1240
|
+
|
|
1241
|
+
const callback = createClassifiedFunction({
|
|
1242
|
+
name: 'forEach',
|
|
1243
|
+
startLine: 10,
|
|
1244
|
+
endLine: 20,
|
|
1245
|
+
bodyLineCount: 11,
|
|
1246
|
+
classification: 'pure',
|
|
1247
|
+
parentContext: 'forEach',
|
|
1248
|
+
isInlineCallback: true,
|
|
1249
|
+
enclosingFunctionStartLine: 1,
|
|
1250
|
+
})
|
|
1251
|
+
|
|
1252
|
+
const score = scoreFile('/test.ts', [parent, callback])
|
|
1253
|
+
|
|
1254
|
+
// Line count should be 50 (parent's lines), not 61 (50 + 11)
|
|
1255
|
+
expect(score.pureLineCount).toBe(50)
|
|
1256
|
+
expect(score.impureLineCount).toBe(0)
|
|
1257
|
+
})
|
|
1258
|
+
|
|
1259
|
+
it('should treat module-scope inline callbacks independently', () => {
|
|
1260
|
+
const moduleScopeCallback = createClassifiedFunction({
|
|
1261
|
+
name: 'map',
|
|
1262
|
+
startLine: 1,
|
|
1263
|
+
endLine: 15,
|
|
1264
|
+
bodyLineCount: 15,
|
|
1265
|
+
statementCount: 5,
|
|
1266
|
+
classification: 'impure',
|
|
1267
|
+
qualityScore: 50,
|
|
1268
|
+
status: 'review',
|
|
1269
|
+
markers: [{ type: 'console-log', detail: 'console.log' }],
|
|
1270
|
+
parentContext: 'map',
|
|
1271
|
+
isInlineCallback: true,
|
|
1272
|
+
enclosingFunctionStartLine: null, // module scope - no enclosing function
|
|
1273
|
+
})
|
|
1274
|
+
|
|
1275
|
+
const score = scoreFile('/test.ts', [moduleScopeCallback])
|
|
1276
|
+
|
|
1277
|
+
// Should count as its own function since it has no parent
|
|
1278
|
+
expect(score.pureCount + score.impureCount).toBe(1)
|
|
1279
|
+
expect(score.impureCount).toBe(1)
|
|
1280
|
+
})
|
|
1281
|
+
|
|
1282
|
+
it('should make parent effectively impure if callback is impure', () => {
|
|
1283
|
+
const parent = createClassifiedFunction({
|
|
1284
|
+
name: 'processItems',
|
|
1285
|
+
startLine: 1,
|
|
1286
|
+
endLine: 50,
|
|
1287
|
+
bodyLineCount: 50,
|
|
1288
|
+
classification: 'pure', // parent itself is pure
|
|
1289
|
+
qualityScore: null,
|
|
1290
|
+
status: 'ok',
|
|
1291
|
+
isInlineCallback: false,
|
|
1292
|
+
enclosingFunctionStartLine: null,
|
|
1293
|
+
})
|
|
1294
|
+
|
|
1295
|
+
const impureCallback = createClassifiedFunction({
|
|
1296
|
+
name: 'forEach',
|
|
1297
|
+
startLine: 10,
|
|
1298
|
+
endLine: 20,
|
|
1299
|
+
bodyLineCount: 11,
|
|
1300
|
+
classification: 'impure', // callback is impure
|
|
1301
|
+
qualityScore: 40,
|
|
1302
|
+
status: 'review',
|
|
1303
|
+
markers: [{ type: 'console-log', detail: 'console.log' }],
|
|
1304
|
+
parentContext: 'forEach',
|
|
1305
|
+
isInlineCallback: true,
|
|
1306
|
+
enclosingFunctionStartLine: 1,
|
|
1307
|
+
})
|
|
1308
|
+
|
|
1309
|
+
const score = scoreFile('/test.ts', [parent, impureCallback])
|
|
1310
|
+
|
|
1311
|
+
// Parent should be counted as effectively impure
|
|
1312
|
+
expect(score.pureCount).toBe(0)
|
|
1313
|
+
expect(score.impureCount).toBe(1)
|
|
1314
|
+
expect(score.impureLineCount).toBe(50)
|
|
1315
|
+
expect(score.pureLineCount).toBe(0)
|
|
1316
|
+
})
|
|
1317
|
+
|
|
1318
|
+
it('should use worst status among parent and children', () => {
|
|
1319
|
+
const parent = createClassifiedFunction({
|
|
1320
|
+
name: 'processItems',
|
|
1321
|
+
startLine: 1,
|
|
1322
|
+
endLine: 50,
|
|
1323
|
+
bodyLineCount: 50,
|
|
1324
|
+
classification: 'impure',
|
|
1325
|
+
qualityScore: 75,
|
|
1326
|
+
status: 'ok', // parent is ok
|
|
1327
|
+
markers: [{ type: 'database-call', detail: 'db.find' }],
|
|
1328
|
+
isInlineCallback: false,
|
|
1329
|
+
enclosingFunctionStartLine: null,
|
|
1330
|
+
})
|
|
1331
|
+
|
|
1332
|
+
const badCallback = createClassifiedFunction({
|
|
1333
|
+
name: 'forEach',
|
|
1334
|
+
startLine: 10,
|
|
1335
|
+
endLine: 20,
|
|
1336
|
+
bodyLineCount: 11,
|
|
1337
|
+
classification: 'impure',
|
|
1338
|
+
qualityScore: 20,
|
|
1339
|
+
status: 'refactor', // callback needs refactoring
|
|
1340
|
+
markers: [{ type: 'console-log', detail: 'console.log' }],
|
|
1341
|
+
parentContext: 'forEach',
|
|
1342
|
+
isInlineCallback: true,
|
|
1343
|
+
enclosingFunctionStartLine: 1,
|
|
1344
|
+
})
|
|
1345
|
+
|
|
1346
|
+
const score = scoreFile('/test.ts', [parent, badCallback])
|
|
1347
|
+
|
|
1348
|
+
// Status breakdown should reflect the worst status
|
|
1349
|
+
expect(score.statusBreakdown.refactor).toBe(1)
|
|
1350
|
+
expect(score.statusBreakdown.ok).toBe(0)
|
|
1351
|
+
expect(score.health).toBe(0) // 0% health because the only function needs refactoring
|
|
1352
|
+
})
|
|
1353
|
+
|
|
1354
|
+
it('should calculate health based on effective status', () => {
|
|
1355
|
+
const goodParent = createClassifiedFunction({
|
|
1356
|
+
name: 'goodFunction',
|
|
1357
|
+
startLine: 1,
|
|
1358
|
+
endLine: 30,
|
|
1359
|
+
bodyLineCount: 30,
|
|
1360
|
+
classification: 'pure',
|
|
1361
|
+
status: 'ok',
|
|
1362
|
+
isInlineCallback: false,
|
|
1363
|
+
enclosingFunctionStartLine: null,
|
|
1364
|
+
})
|
|
1365
|
+
|
|
1366
|
+
const parentWithBadCallback = createClassifiedFunction({
|
|
1367
|
+
name: 'parentWithIssue',
|
|
1368
|
+
startLine: 50,
|
|
1369
|
+
endLine: 100,
|
|
1370
|
+
bodyLineCount: 51,
|
|
1371
|
+
classification: 'impure',
|
|
1372
|
+
qualityScore: 75,
|
|
1373
|
+
status: 'ok',
|
|
1374
|
+
markers: [{ type: 'database-call', detail: 'db.find' }],
|
|
1375
|
+
isInlineCallback: false,
|
|
1376
|
+
enclosingFunctionStartLine: null,
|
|
1377
|
+
})
|
|
1378
|
+
|
|
1379
|
+
const badCallback = createClassifiedFunction({
|
|
1380
|
+
name: 'forEach',
|
|
1381
|
+
startLine: 60,
|
|
1382
|
+
endLine: 70,
|
|
1383
|
+
bodyLineCount: 11,
|
|
1384
|
+
classification: 'impure',
|
|
1385
|
+
qualityScore: 30,
|
|
1386
|
+
status: 'refactor',
|
|
1387
|
+
markers: [{ type: 'console-log', detail: 'console.log' }],
|
|
1388
|
+
parentContext: 'forEach',
|
|
1389
|
+
isInlineCallback: true,
|
|
1390
|
+
enclosingFunctionStartLine: 50,
|
|
1391
|
+
})
|
|
1392
|
+
|
|
1393
|
+
const score = scoreFile('/test.ts', [
|
|
1394
|
+
goodParent,
|
|
1395
|
+
parentWithBadCallback,
|
|
1396
|
+
badCallback,
|
|
1397
|
+
])
|
|
1398
|
+
|
|
1399
|
+
// 2 top-level functions: 1 ok, 1 refactor
|
|
1400
|
+
expect(score.pureCount + score.impureCount).toBe(2)
|
|
1401
|
+
expect(score.statusBreakdown.ok).toBe(1)
|
|
1402
|
+
expect(score.statusBreakdown.refactor).toBe(1)
|
|
1403
|
+
expect(score.health).toBe(50) // 1/2 = 50%
|
|
1404
|
+
})
|
|
1405
|
+
|
|
1406
|
+
it('should aggregate markers from children into refactoring candidates', () => {
|
|
1407
|
+
const parent = createClassifiedFunction({
|
|
1408
|
+
name: 'processItems',
|
|
1409
|
+
startLine: 1,
|
|
1410
|
+
endLine: 50,
|
|
1411
|
+
bodyLineCount: 50,
|
|
1412
|
+
classification: 'impure',
|
|
1413
|
+
qualityScore: 30,
|
|
1414
|
+
status: 'refactor',
|
|
1415
|
+
markers: [{ type: 'database-call', detail: 'db.find' }],
|
|
1416
|
+
isInlineCallback: false,
|
|
1417
|
+
enclosingFunctionStartLine: null,
|
|
1418
|
+
})
|
|
1419
|
+
|
|
1420
|
+
const callback1 = createClassifiedFunction({
|
|
1421
|
+
name: 'forEach',
|
|
1422
|
+
startLine: 10,
|
|
1423
|
+
endLine: 20,
|
|
1424
|
+
bodyLineCount: 11,
|
|
1425
|
+
classification: 'impure',
|
|
1426
|
+
qualityScore: 20,
|
|
1427
|
+
status: 'refactor',
|
|
1428
|
+
markers: [{ type: 'console-log', detail: 'console.log' }],
|
|
1429
|
+
parentContext: 'forEach',
|
|
1430
|
+
isInlineCallback: true,
|
|
1431
|
+
enclosingFunctionStartLine: 1,
|
|
1432
|
+
})
|
|
1433
|
+
|
|
1434
|
+
const callback2 = createClassifiedFunction({
|
|
1435
|
+
name: 'map',
|
|
1436
|
+
startLine: 25,
|
|
1437
|
+
endLine: 35,
|
|
1438
|
+
bodyLineCount: 11,
|
|
1439
|
+
classification: 'impure',
|
|
1440
|
+
qualityScore: 25,
|
|
1441
|
+
status: 'refactor',
|
|
1442
|
+
markers: [
|
|
1443
|
+
{ type: 'logging', detail: 'logger.info' },
|
|
1444
|
+
{ type: 'console-log', detail: 'console.log' }, // duplicate type
|
|
1445
|
+
],
|
|
1446
|
+
parentContext: 'map',
|
|
1447
|
+
isInlineCallback: true,
|
|
1448
|
+
enclosingFunctionStartLine: 1,
|
|
1449
|
+
})
|
|
1450
|
+
|
|
1451
|
+
const score = scoreFile('/test.ts', [parent, callback1, callback2])
|
|
1452
|
+
|
|
1453
|
+
// Should have 1 refactoring candidate (the parent)
|
|
1454
|
+
expect(score.refactoringCandidates).toHaveLength(1)
|
|
1455
|
+
|
|
1456
|
+
const candidate = score.refactoringCandidates[0]
|
|
1457
|
+
expect(candidate?.name).toBe('processItems')
|
|
1458
|
+
|
|
1459
|
+
// Markers should be aggregated and deduped
|
|
1460
|
+
// Parent: database-call
|
|
1461
|
+
// Callback1: console-log
|
|
1462
|
+
// Callback2: logging, console-log (dupe)
|
|
1463
|
+
expect(candidate?.markers).toContain('database-call')
|
|
1464
|
+
expect(candidate?.markers).toContain('console-log')
|
|
1465
|
+
expect(candidate?.markers).toContain('logging')
|
|
1466
|
+
// Should be deduped (console-log appears only once)
|
|
1467
|
+
expect(candidate?.markers.filter(m => m === 'console-log')).toHaveLength(1)
|
|
1468
|
+
})
|
|
1469
|
+
|
|
1470
|
+
it('should blend parent and impure child quality by line count', () => {
|
|
1471
|
+
// Parent: 50 total lines, 40 own lines (after subtracting child), quality 60
|
|
1472
|
+
// Child: 10 lines, quality 40
|
|
1473
|
+
// Composed: (60*40 + 40*10) / 50 = (2400 + 400) / 50 = 56
|
|
1474
|
+
const parent = createClassifiedFunction({
|
|
1475
|
+
name: 'processItems',
|
|
1476
|
+
startLine: 1,
|
|
1477
|
+
endLine: 50,
|
|
1478
|
+
bodyLineCount: 50,
|
|
1479
|
+
classification: 'impure',
|
|
1480
|
+
qualityScore: 60,
|
|
1481
|
+
status: 'review',
|
|
1482
|
+
markers: [{ type: 'database-call', detail: 'db.find' }],
|
|
1483
|
+
isInlineCallback: false,
|
|
1484
|
+
enclosingFunctionStartLine: null,
|
|
1485
|
+
})
|
|
1486
|
+
|
|
1487
|
+
const impureCallback = createClassifiedFunction({
|
|
1488
|
+
name: 'forEach',
|
|
1489
|
+
startLine: 10,
|
|
1490
|
+
endLine: 19,
|
|
1491
|
+
bodyLineCount: 10,
|
|
1492
|
+
classification: 'impure',
|
|
1493
|
+
qualityScore: 40,
|
|
1494
|
+
status: 'review',
|
|
1495
|
+
markers: [{ type: 'console-log', detail: 'console.log' }],
|
|
1496
|
+
parentContext: 'forEach',
|
|
1497
|
+
isInlineCallback: true,
|
|
1498
|
+
enclosingFunctionStartLine: 1,
|
|
1499
|
+
})
|
|
1500
|
+
|
|
1501
|
+
const score = scoreFile('/test.ts', [parent, impureCallback])
|
|
1502
|
+
|
|
1503
|
+
// impurityQuality should be the composed quality: 56
|
|
1504
|
+
expect(score.impurityQuality).toBeCloseTo(56, 0)
|
|
1505
|
+
})
|
|
1506
|
+
|
|
1507
|
+
it('should not affect quality if all children are pure', () => {
|
|
1508
|
+
const parent = createClassifiedFunction({
|
|
1509
|
+
name: 'processItems',
|
|
1510
|
+
startLine: 1,
|
|
1511
|
+
endLine: 50,
|
|
1512
|
+
bodyLineCount: 50,
|
|
1513
|
+
classification: 'impure',
|
|
1514
|
+
qualityScore: 60,
|
|
1515
|
+
status: 'review',
|
|
1516
|
+
markers: [{ type: 'database-call', detail: 'db.find' }],
|
|
1517
|
+
isInlineCallback: false,
|
|
1518
|
+
enclosingFunctionStartLine: null,
|
|
1519
|
+
})
|
|
1520
|
+
|
|
1521
|
+
const pureCallback = createClassifiedFunction({
|
|
1522
|
+
name: 'map',
|
|
1523
|
+
startLine: 10,
|
|
1524
|
+
endLine: 19,
|
|
1525
|
+
bodyLineCount: 10,
|
|
1526
|
+
classification: 'pure',
|
|
1527
|
+
qualityScore: null,
|
|
1528
|
+
status: 'ok',
|
|
1529
|
+
markers: [],
|
|
1530
|
+
parentContext: 'map',
|
|
1531
|
+
isInlineCallback: true,
|
|
1532
|
+
enclosingFunctionStartLine: 1,
|
|
1533
|
+
})
|
|
1534
|
+
|
|
1535
|
+
const score = scoreFile('/test.ts', [parent, pureCallback])
|
|
1536
|
+
|
|
1537
|
+
// Quality should be unchanged (60) since all children are pure
|
|
1538
|
+
expect(score.impurityQuality).toBe(60)
|
|
1539
|
+
})
|
|
1540
|
+
|
|
1541
|
+
it('should weight heavily toward child when child dominates line count', () => {
|
|
1542
|
+
// Parent: 50 total lines, 10 own lines, quality 60
|
|
1543
|
+
// Child: 40 lines, quality 20
|
|
1544
|
+
// Composed: (60*10 + 20*40) / 50 = (600 + 800) / 50 = 28
|
|
1545
|
+
const parent = createClassifiedFunction({
|
|
1546
|
+
name: 'processItems',
|
|
1547
|
+
startLine: 1,
|
|
1548
|
+
endLine: 50,
|
|
1549
|
+
bodyLineCount: 50,
|
|
1550
|
+
classification: 'impure',
|
|
1551
|
+
qualityScore: 60,
|
|
1552
|
+
status: 'review',
|
|
1553
|
+
markers: [{ type: 'database-call', detail: 'db.find' }],
|
|
1554
|
+
isInlineCallback: false,
|
|
1555
|
+
enclosingFunctionStartLine: null,
|
|
1556
|
+
})
|
|
1557
|
+
|
|
1558
|
+
const largeImpureCallback = createClassifiedFunction({
|
|
1559
|
+
name: 'forEach',
|
|
1560
|
+
startLine: 5,
|
|
1561
|
+
endLine: 44,
|
|
1562
|
+
bodyLineCount: 40,
|
|
1563
|
+
classification: 'impure',
|
|
1564
|
+
qualityScore: 20,
|
|
1565
|
+
status: 'refactor',
|
|
1566
|
+
markers: [{ type: 'console-log', detail: 'console.log' }],
|
|
1567
|
+
parentContext: 'forEach',
|
|
1568
|
+
isInlineCallback: true,
|
|
1569
|
+
enclosingFunctionStartLine: 1,
|
|
1570
|
+
})
|
|
1571
|
+
|
|
1572
|
+
const score = scoreFile('/test.ts', [parent, largeImpureCallback])
|
|
1573
|
+
|
|
1574
|
+
// impurityQuality should be heavily influenced by the child: 28
|
|
1575
|
+
expect(score.impurityQuality).toBeCloseTo(28, 0)
|
|
1576
|
+
})
|
|
1577
|
+
|
|
1578
|
+
it('should handle pure parent with impure children', () => {
|
|
1579
|
+
// Parent is pure (no direct markers), but has impure callback
|
|
1580
|
+
// Result should still calculate quality for the impure child
|
|
1581
|
+
const parent = createClassifiedFunction({
|
|
1582
|
+
name: 'processItems',
|
|
1583
|
+
startLine: 1,
|
|
1584
|
+
endLine: 50,
|
|
1585
|
+
bodyLineCount: 50,
|
|
1586
|
+
classification: 'pure', // parent itself is pure
|
|
1587
|
+
qualityScore: null,
|
|
1588
|
+
status: 'ok',
|
|
1589
|
+
markers: [],
|
|
1590
|
+
isInlineCallback: false,
|
|
1591
|
+
enclosingFunctionStartLine: null,
|
|
1592
|
+
})
|
|
1593
|
+
|
|
1594
|
+
const impureCallback = createClassifiedFunction({
|
|
1595
|
+
name: 'forEach',
|
|
1596
|
+
startLine: 10,
|
|
1597
|
+
endLine: 29,
|
|
1598
|
+
bodyLineCount: 20,
|
|
1599
|
+
classification: 'impure',
|
|
1600
|
+
qualityScore: 40,
|
|
1601
|
+
status: 'review',
|
|
1602
|
+
markers: [{ type: 'console-log', detail: 'console.log' }],
|
|
1603
|
+
parentContext: 'forEach',
|
|
1604
|
+
isInlineCallback: true,
|
|
1605
|
+
enclosingFunctionStartLine: 1,
|
|
1606
|
+
})
|
|
1607
|
+
|
|
1608
|
+
const score = scoreFile('/test.ts', [parent, impureCallback])
|
|
1609
|
+
|
|
1610
|
+
// Parent is effectively impure, quality blended:
|
|
1611
|
+
// parentOwnLines = 50 - 20 = 30, parentQuality = 50 (default for pure)
|
|
1612
|
+
// childLines = 20, childQuality = 40
|
|
1613
|
+
// composed = (50*30 + 40*20) / 50 = (1500 + 800) / 50 = 46
|
|
1614
|
+
expect(score.impureCount).toBe(1)
|
|
1615
|
+
expect(score.pureCount).toBe(0)
|
|
1616
|
+
expect(score.impurityQuality).toBeCloseTo(46, 0)
|
|
1617
|
+
})
|
|
1618
|
+
|
|
1619
|
+
it('should not include callbacks in refactoring candidates list', () => {
|
|
1620
|
+
const parent = createClassifiedFunction({
|
|
1621
|
+
name: 'processItems',
|
|
1622
|
+
startLine: 1,
|
|
1623
|
+
endLine: 50,
|
|
1624
|
+
bodyLineCount: 50,
|
|
1625
|
+
classification: 'impure',
|
|
1626
|
+
qualityScore: 75,
|
|
1627
|
+
status: 'ok', // parent is ok
|
|
1628
|
+
markers: [{ type: 'database-call', detail: 'db.find' }],
|
|
1629
|
+
isInlineCallback: false,
|
|
1630
|
+
enclosingFunctionStartLine: null,
|
|
1631
|
+
})
|
|
1632
|
+
|
|
1633
|
+
const badCallback = createClassifiedFunction({
|
|
1634
|
+
name: 'forEach',
|
|
1635
|
+
startLine: 10,
|
|
1636
|
+
endLine: 20,
|
|
1637
|
+
bodyLineCount: 11,
|
|
1638
|
+
classification: 'impure',
|
|
1639
|
+
qualityScore: 20,
|
|
1640
|
+
status: 'refactor', // callback needs refactoring
|
|
1641
|
+
markers: [{ type: 'console-log', detail: 'console.log' }],
|
|
1642
|
+
parentContext: 'forEach',
|
|
1643
|
+
isInlineCallback: true,
|
|
1644
|
+
enclosingFunctionStartLine: 1,
|
|
1645
|
+
})
|
|
1646
|
+
|
|
1647
|
+
const score = scoreFile('/test.ts', [parent, badCallback])
|
|
1648
|
+
|
|
1649
|
+
// The callback itself should NOT appear as a separate refactoring candidate
|
|
1650
|
+
// Only the parent (with effective status 'refactor') should be a candidate
|
|
1651
|
+
expect(score.refactoringCandidates).toHaveLength(1)
|
|
1652
|
+
expect(score.refactoringCandidates[0]?.name).toBe('processItems')
|
|
1653
|
+
})
|
|
1654
|
+
})
|