22
33namespace ShipMonk \CoverageGuard \Command ;
44
5+ use ShipMonk \CoverageGuard \Ast \FileTraverser ;
56use ShipMonk \CoverageGuard \Cli \Arguments \CoverageFileCliArgument ;
67use ShipMonk \CoverageGuard \Cli \Options \ConfigCliOption ;
78use ShipMonk \CoverageGuard \Cli \Options \PatchCliOption ;
9+ use ShipMonk \CoverageGuard \Coverage \FileCoverage ;
810use ShipMonk \CoverageGuard \CoverageProvider ;
911use ShipMonk \CoverageGuard \Exception \ErrorException ;
12+ use ShipMonk \CoverageGuard \Excluder \ExcluderVisitor ;
13+ use ShipMonk \CoverageGuard \Excluder \ExclusionContext ;
14+ use ShipMonk \CoverageGuard \Excluder \ExecutableLineExcluder ;
1015use ShipMonk \CoverageGuard \Printer ;
1116use ShipMonk \CoverageGuard \Utils \ConfigResolver ;
17+ use ShipMonk \CoverageGuard \Utils \FileUtils ;
1218use ShipMonk \CoverageGuard \Utils \PatchParser ;
19+ use function array_combine ;
20+ use function count ;
1321use function number_format ;
22+ use function range ;
1423
1524final class PatchCoverageCommand implements Command
1625{
@@ -20,6 +29,7 @@ public function __construct(
2029 private readonly PatchParser $ patchParser ,
2130 private readonly ConfigResolver $ configResolver ,
2231 private readonly CoverageProvider $ coverageProvider ,
32+ private readonly FileTraverser $ fileTraverser ,
2333 )
2434 {
2535 }
@@ -42,6 +52,7 @@ public function __invoke(
4252
4353 $ coveragePerFile = $ this ->coverageProvider ->getCoverage ($ config , $ coverageFile );
4454 $ changesPerFile = $ this ->patchParser ->getPatchChangedLines ($ patchPath , $ config );
55+ $ excluders = $ config ->getExecutableLineExcluders ();
4556
4657 // Calculate coverage for changed lines
4758 $ totalChangedLines = 0 ;
@@ -52,23 +63,76 @@ public function __invoke(
5263 continue ; // File not in coverage report
5364 }
5465
55- $ fileCoverage = $ coveragePerFile [$ file ];
56- $ executableLinesMap = [];
66+ $ changedExecutableLines = $ this ->getChangedExecutableLines ($ coveragePerFile [$ file ], $ changedLines );
5767
58- foreach ( $ fileCoverage -> executableLines as $ line ) {
59- $ executableLinesMap [ $ line -> lineNumber ] = $ line -> hits > 0 ;
60- }
68+ $ excluderVisitor = $ excluders !== [] && $ changedExecutableLines !== []
69+ ? $ this -> createExcluderVisitor ( $ excluders , $ file )
70+ : null ;
6171
62- foreach ($ changedLines as $ lineNumber ) {
63- if (isset ($ executableLinesMap [$ lineNumber ])) {
64- $ totalChangedLines ++;
65- if ($ executableLinesMap [$ lineNumber ]) {
66- $ totalCoveredLines ++;
67- }
72+ foreach ($ changedExecutableLines as $ lineNumber => $ isCovered ) {
73+ if ($ excluderVisitor ?->isLineExcluded($ lineNumber ) === true ) {
74+ continue ;
75+ }
76+ $ totalChangedLines ++;
77+ if ($ isCovered ) {
78+ $ totalCoveredLines ++;
6879 }
6980 }
7081 }
7182
83+ $ this ->printStatistics ($ totalChangedLines , $ totalCoveredLines );
84+
85+ return 0 ;
86+ }
87+
88+ /**
89+ * @param list<int> $changedLines
90+ * @return array<int, bool> line number => is covered
91+ */
92+ private function getChangedExecutableLines (
93+ FileCoverage $ fileCoverage ,
94+ array $ changedLines ,
95+ ): array
96+ {
97+ $ executableLinesMap = [];
98+ foreach ($ fileCoverage ->executableLines as $ line ) {
99+ $ executableLinesMap [$ line ->lineNumber ] = $ line ->hits > 0 ;
100+ }
101+
102+ $ changedExecutableLines = [];
103+ foreach ($ changedLines as $ lineNumber ) {
104+ if (isset ($ executableLinesMap [$ lineNumber ])) {
105+ $ changedExecutableLines [$ lineNumber ] = $ executableLinesMap [$ lineNumber ];
106+ }
107+ }
108+
109+ return $ changedExecutableLines ;
110+ }
111+
112+ /**
113+ * @param non-empty-list<ExecutableLineExcluder> $excluders
114+ *
115+ * @throws ErrorException
116+ */
117+ private function createExcluderVisitor (
118+ array $ excluders ,
119+ string $ file ,
120+ ): ExcluderVisitor
121+ {
122+ $ fileLines = FileUtils::readFileLines ($ file );
123+ $ linesContents = array_combine (range (1 , count ($ fileLines )), $ fileLines );
124+
125+ $ excluderVisitor = new ExcluderVisitor ($ excluders , new ExclusionContext ($ file , $ linesContents ));
126+ $ this ->fileTraverser ->traverse ($ file , $ fileLines , $ excluderVisitor );
127+
128+ return $ excluderVisitor ;
129+ }
130+
131+ private function printStatistics (
132+ int $ totalChangedLines ,
133+ int $ totalCoveredLines ,
134+ ): void
135+ {
72136 if ($ totalChangedLines === 0 ) {
73137 $ percentage = 0 ;
74138 } else {
@@ -84,8 +148,6 @@ public function __invoke(
84148 $ this ->stdoutPrinter ->printLine (' Uncovered lines: <orange> ' . ($ totalChangedLines - $ totalCoveredLines ) . '</orange> ' );
85149 $ this ->stdoutPrinter ->printLine (" Coverage: {$ percentageFormatted }% " );
86150 $ this ->stdoutPrinter ->printLine ('' );
87-
88- return 0 ;
89151 }
90152
91153 public function getName (): string
0 commit comments