]> git.mxchange.org Git - friendica.git/blob - src/Console/DocBloxErrorChecker.php
Posts per author/server on the community pages (#13764)
[friendica.git] / src / Console / DocBloxErrorChecker.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2023, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Console;
23
24 use Friendica\App;
25
26 /**
27  * When I installed docblox, I had the experience that it does not generate any output at all.
28  * This script may be used to find that kind of problems with the documentation build process.
29  * If docblox generates output, use another approach for debugging.
30  *
31  * Basically, docblox takes a list of files to build documentation from. This script assumes there is a file or set of files
32  * breaking the build when it is included in that list. It tries to calculate the smallest list containing these files.
33  * Unfortunately, the original problem is NP-complete, so what the script does is a best guess only.
34  *
35  * So it starts with a list of all files in the project.
36  * If that list can't be build, it cuts it in two parts and tries both parts independently. If only one of them breaks,
37  * it takes that one and tries the same independently. If both break, it assumes this is the smallest set. This assumption
38  * is not necessarily true. Maybe the smallest set consists of two files and both of them were in different parts when
39  * the list was divided, but by now it is my best guess. To make this assumption better, the list is shuffled after every step.
40  *
41  * After that, the script tries to remove a file from the list. It tests if the list breaks and if so, it
42  * assumes that the file it removed belongs to the set of erroneous files.
43  * This is done for all files, so, in the end removing one file leads to a working doc build.
44  */
45 class DocBloxErrorChecker extends \Asika\SimpleConsole\Console
46 {
47
48         protected $helpOptions = ['h', 'help', '?'];
49
50         /** @var App */
51         private $app;
52
53         public function __construct(App $app, array $argv = null)
54         {
55                 parent::__construct($argv);
56
57                 $this->app = $app;
58         }
59
60         protected function getHelp()
61         {
62                 $help = <<<HELP
63 console docbloxerrorchecker - Checks the file tree for docblox errors
64 Usage
65         bin/console docbloxerrorchecker [-h|--help|-?] [-v]
66
67 Options
68     -h|--help|-? Show help information
69     -v           Show more debug information.
70 HELP;
71                 return $help;
72         }
73
74         protected function doExecute(): int
75         {
76                 if ($this->getOption('v')) {
77                         $this->out('Class: ' . __CLASS__);
78                         $this->out('Arguments: ' . var_export($this->args, true));
79                         $this->out('Options: ' . var_export($this->options, true));
80                 }
81
82                 if (count($this->args) > 0) {
83                         throw new \Asika\SimpleConsole\CommandArgsException('Too many arguments');
84                 }
85
86                 if (!$this->commandExists('docblox')) {
87                         throw new \RuntimeException('DocBlox isn\'t available.');
88                 }
89
90                 $dir = $this->app->getBasePath();
91
92                 //stack for dirs to search
93                 $dirstack = [];
94                 //list of source files
95                 $filelist = [];
96
97                 //loop over all files in $dir
98                 while ($dh = opendir($dir)) {
99                         while ($file = readdir($dh)) {
100                                 if (is_dir($dir . "/" . $file)) {
101                                         //add to directory stack
102                                         if (strpos($file, '.') !== 0) {
103                                                 array_push($dirstack, $dir . "/" . $file);
104                                                 $this->out('dir ' . $dir . '/' . $file);
105                                         }
106                                 } else {
107                                         //test if it is a source file and add to filelist
108                                         if (substr($file, strlen($file) - 4) == ".php") {
109                                                 array_push($filelist, $dir . "/" . $file);
110                                                 $this->out($dir . '/' . $file);
111                                         }
112                                 }
113                         }
114                         //look at the next dir
115                         $dir = array_pop($dirstack);
116                 }
117
118                 //check the entire set
119                 if ($this->runs($filelist)) {
120                         throw new \RuntimeException("I can not detect a problem.");
121                 }
122
123                 //check half of the set and discard if that half is okay
124                 $res = $filelist;
125                 $i = count($res);
126                 do {
127                         $this->out($i . '/' . count($filelist) . ' elements remaining.');
128                         $res = $this->reduce($res, count($res) / 2);
129                         shuffle($res);
130                         $i = count($res);
131                 } while (count($res) < $i);
132
133                 //check one file after another
134                 $needed = [];
135
136                 while (count($res) != 0) {
137                         $file = array_pop($res);
138
139                         if ($this->runs(array_merge($res, $needed))) {
140                                 $this->out('needs: ' . $file . ' and file count ' . count($needed));
141                                 array_push($needed, $file);
142                         }
143                 }
144
145                 $this->out('Smallest Set is: ' . $this->namesList($needed) . ' with ' . count($needed) . ' files. ');
146
147                 return 0;
148         }
149
150         private function commandExists($command)
151         {
152                 $prefix = strpos(strtolower(PHP_OS),'win') > -1 ? 'where' : 'which';
153                 exec("{$prefix} {$command}", $output, $returnVal);
154                 return $returnVal === 0;
155         }
156
157         /**
158          * This function generates a comma separated list of file names.
159          *
160          * @param array $fileset Set of file names
161          *
162          * @return string comma-separated list of the file names
163          */
164         private function namesList($fileset)
165         {
166                 return implode(',', $fileset);
167         }
168
169         /**
170          * This functions runs phpdoc on the provided list of files
171          *
172          * @param array $fileset Set of filenames
173          *
174          * @return bool true, if that set can be built
175          */
176         private function runs($fileset)
177         {
178                 $fsParam = $this->namesList($fileset);
179                 $this->exec('docblox -t phpdoc_out -f ' . $fsParam);
180                 if (file_exists("phpdoc_out/index.html")) {
181                         $this->out('Subset ' . $fsParam . ' is okay.');
182                         $this->exec('rm -r phpdoc_out');
183                         return true;
184                 } else {
185                         $this->out('Subset ' . $fsParam . ' failed.');
186                         return false;
187                 }
188         }
189
190         /**
191          * This functions cuts down a fileset by removing files until it finally works.
192          * it was meant to be recursive, but php's maximum stack size is to small. So it just simulates recursion.
193          *
194          * In that version, it does not necessarily generate the smallest set, because it may not alter the elements order enough.
195          *
196          * @param array $fileset set of filenames
197          * @param int $ps number of files in subsets
198          *
199          * @return array a part of $fileset, that crashes
200          */
201         private function reduce($fileset, $ps)
202         {
203                 //split array...
204                 $parts = array_chunk($fileset, $ps);
205                 //filter working subsets...
206                 $parts = array_filter($parts, [$this, 'runs']);
207                 //melt remaining parts together
208                 if (is_array($parts)) {
209                         return array_reduce($parts, "array_merge", []);
210                 }
211                 return [];
212         }
213
214 }