1818
1919use Phauthentic \BcCheck \Detector \DetectorRegistry ;
2020use Phauthentic \BcCheck \Diff \RenameDetector ;
21+ use Phauthentic \BcCheck \Diff \RenameMap ;
2122use Phauthentic \BcCheck \Git \GitRepositoryInterface ;
23+ use Phauthentic \BcCheck \Parser \AnalysisResult ;
2224use Phauthentic \BcCheck \Parser \CodebaseAnalyzerInterface ;
2325use Phauthentic \BcCheck \ValueObject \BcBreak ;
2426use Phauthentic \BcCheck \ValueObject \BcBreakType ;
27+ use Phauthentic \BcCheck \ValueObject \ClassInfo ;
2528
2629final readonly class BcChecker implements BcCheckerInterface
2730{
@@ -33,50 +36,108 @@ public function __construct(
3336 ) {
3437 }
3538
36- public function check (string $ fromCommit , string $ toCommit ): array
39+ /**
40+ * Validates that both commit references are valid.
41+ *
42+ * @throws InvalidCommitException When either commit is invalid
43+ */
44+ private function validateCommits (string $ fromCommit , string $ toCommit ): void
3745 {
38- // Validate commits
3946 if (!$ this ->git ->isValidCommit ($ fromCommit )) {
4047 throw InvalidCommitException::invalidFromCommit ($ fromCommit );
4148 }
4249
4350 if (!$ this ->git ->isValidCommit ($ toCommit )) {
4451 throw InvalidCommitException::invalidToCommit ($ toCommit );
4552 }
53+ }
4654
47- // Detect renames from diff
55+ /**
56+ * Detects method and property renames between commits.
57+ *
58+ * @return array<string, RenameMap> Map of file paths to rename maps
59+ */
60+ private function detectRenames (string $ fromCommit , string $ toCommit ): array
61+ {
4862 $ diff = $ this ->git ->getDiff ($ fromCommit , $ toCommit );
49- $ renamesByFile = $ this ->renameDetector ->detect ($ diff );
5063
51- // Analyze both versions with file mapping
52- $ beforeResult = $ this ->analyzer ->analyzeAtCommitWithFileMap ($ fromCommit , 'source ' );
53- $ afterResult = $ this ->analyzer ->analyzeAtCommitWithFileMap ($ toCommit , 'target ' );
64+ return $ this ->renameDetector ->detect ($ diff );
65+ }
5466
67+ /**
68+ * Analyzes both source and target codebases at their respective commits.
69+ *
70+ * @return array{0: AnalysisResult, 1: AnalysisResult} Array containing [sourceAnalysis, targetAnalysis]
71+ */
72+ private function analyzeCodebases (string $ fromCommit , string $ toCommit ): array
73+ {
74+ $ sourceAnalysis = $ this ->analyzer ->analyzeAtCommitWithFileMap ($ fromCommit , 'source ' );
75+ $ targetAnalysis = $ this ->analyzer ->analyzeAtCommitWithFileMap ($ toCommit , 'target ' );
76+
77+ return [$ sourceAnalysis , $ targetAnalysis ];
78+ }
79+
80+ /**
81+ * Detects classes that have been removed between source and target analyses.
82+ *
83+ * @return list<BcBreak>
84+ */
85+ private function detectRemovedClasses (AnalysisResult $ sourceAnalysis , AnalysisResult $ targetAnalysis ): array
86+ {
5587 $ breaks = [];
5688
57- // Check for removed classes
58- foreach ($ beforeResult ->getClasses () as $ className => $ beforeClass ) {
59- if (!$ afterResult ->hasClass ($ className )) {
89+ foreach ($ sourceAnalysis ->getClasses () as $ className => $ sourceClass ) {
90+ if (!$ targetAnalysis ->hasClass ($ className )) {
6091 $ breaks [] = new BcBreak (
61- message: sprintf ('%s %s was removed ' , ucfirst ($ beforeClass ->type ->value ), $ className ),
92+ message: sprintf ('%s %s was removed ' , ucfirst ($ sourceClass ->type ->value ), $ className ),
6293 className: $ className ,
6394 type: BcBreakType::ClassRemoved,
6495 );
65-
66- continue ;
6796 }
97+ }
6898
69- // Get rename map for this class's file
70- $ filePath = $ beforeResult ->getFileForClass ($ className );
71- $ renameMap = $ filePath !== null ? ($ renamesByFile [$ filePath ] ?? null ) : null ;
99+ return $ breaks ;
100+ }
72101
73- // Compare classes - we know afterClass exists because hasClass was true
74- /** @var \Phauthentic\BcCheck\ValueObject\ClassInfo $afterClass */
75- $ afterClass = $ afterResult ->getClass ($ className );
76- $ detected = $ this ->registry ->detectAll ($ beforeClass , $ afterClass , $ renameMap );
102+ /**
103+ * Compares two versions of a class and detects backward compatibility breaks.
104+ *
105+ * @param array<string, RenameMap> $renamesByFile Map of file paths to rename maps
106+ * @return list<BcBreak>
107+ */
108+ private function compareClasses (ClassInfo $ sourceClass , ClassInfo $ targetClass , AnalysisResult $ sourceAnalysis , array $ renamesByFile ): array
109+ {
110+ $ renameMap = $ this ->getRenameMapForClass ($ sourceClass ->name , $ sourceAnalysis , $ renamesByFile );
111+
112+ return $ this ->registry ->detectAll ($ sourceClass , $ targetClass , $ renameMap );
113+ }
114+
115+ /**
116+ * Gets the rename map for a specific class based on its file path.
117+ *
118+ * @param array<string, RenameMap> $renamesByFile Map of file paths to rename maps
119+ */
120+ private function getRenameMapForClass (string $ className , AnalysisResult $ sourceAnalysis , array $ renamesByFile ): ?RenameMap
121+ {
122+ $ filePath = $ sourceAnalysis ->getFileForClass ($ className );
123+
124+ return $ filePath !== null ? ($ renamesByFile [$ filePath ] ?? null ) : null ;
125+ }
126+
127+ public function check (string $ fromCommit , string $ toCommit ): array
128+ {
129+ $ this ->validateCommits ($ fromCommit , $ toCommit );
130+ $ renamesByFile = $ this ->detectRenames ($ fromCommit , $ toCommit );
131+ [$ sourceAnalysis , $ targetAnalysis ] = $ this ->analyzeCodebases ($ fromCommit , $ toCommit );
132+
133+ $ breaks = [];
134+ $ breaks = array_merge ($ breaks , $ this ->detectRemovedClasses ($ sourceAnalysis , $ targetAnalysis ));
77135
78- foreach ($ detected as $ break ) {
79- $ breaks [] = $ break ;
136+ foreach ($ sourceAnalysis ->getClasses () as $ className => $ sourceClass ) {
137+ if ($ targetAnalysis ->hasClass ($ className )) {
138+ $ targetClass = $ targetAnalysis ->getClass ($ className );
139+ assert ($ targetClass !== null ); // Guaranteed by hasClass() check above
140+ $ breaks = array_merge ($ breaks , $ this ->compareClasses ($ sourceClass , $ targetClass , $ sourceAnalysis , $ renamesByFile ));
80141 }
81142 }
82143
0 commit comments