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