foo/bar.php */ protected $substr_start; /** * Fix problems in a directory of files and return a report. * * @param string $root Root directory * @param bool $dry_run If set to true, no files will be written * @return array Report of notable files */ public function fixDirectory($root, $dry_run = false) { $return = array(); $this->substr_start = strlen($this->normalizePath($root)) + 1; $files = $this->findFilesToAnalyze($root); foreach ($files as $file) { $report = $this->analyzeFile($file); $key = substr($file, $this->substr_start); if ($dry_run) { $errors = $report[self::KEY_REMAINING]; array_splice($errors, count($errors), 0, $report[self::KEY_CORRECTIONS]); if ($errors) { $return[$key] = $errors; } } else { if ($report[self::KEY_NEW_CONTENT] !== null) { file_put_contents($file, $report[self::KEY_NEW_CONTENT]); } if ($report[self::KEY_REMAINING]) { $return[$key][self::KEY_REMAINING] = $report[self::KEY_REMAINING]; } if ($report[self::KEY_CORRECTIONS]) { $return[$key][self::KEY_CORRECTIONS] = $report[self::KEY_CORRECTIONS]; } } } return $return; } /** * Find files which can be analyzed/fixed by this component * * @param string $root Root directory * @return string[] File paths. All directory separators will be "/" */ public function findFilesToAnalyze($root) { $files = array(); $this->substr_start = strlen($this->normalizePath($root)) + 1; $this->findFiles(rtrim($root, '/\\'), $files); return $files; } /** * Analyze a file for problems and return a report * * @param string $filepath Path of file to analyze * @param string $content The file's content (optional) * * @return array Report with keys: * * remaining_problems : string[] Problems which could not be fixed * corrections : string[] Problems which were fixed * new_content : string|null Null if no corrections made, otherwise the corrected content */ public function analyzeFile($filepath, $content = null) { if (!is_string($content)) { $content = file_get_contents($filepath); } $old = $content; unset($content); $return = array( self::KEY_REMAINING => array(), self::KEY_CORRECTIONS => array(), self::KEY_NEW_CONTENT => null, ); // remove WS after non-WS $new = preg_replace('~(\S)[ \t]+(\r?\n)~', '$1$2', $old, -1, $count); if ($count) { $return[self::KEY_CORRECTIONS][] = "line(s) with trailing whitespace ($count)"; } // don't risk breaking code blocks if (!preg_match('~\.(?:rst|md)$~', $filepath)) { // remove WS from empty lines $new = preg_replace('~^[ \t]+$~m', '', $new, -1, $count); if ($count) { $return[self::KEY_CORRECTIONS][] = "empty line(s) with whitespace ($count)"; } } if (pathinfo($filepath, PATHINFO_EXTENSION) === 'php') { // remove close PHP tag at file end $new = preg_replace('~\?>\s*$~', '', $new, -1, $count); if ($count) { $return[self::KEY_CORRECTIONS][] = 'unnecessary close PHP tag'; } } if ($new !== $old) { $return[self::KEY_NEW_CONTENT] = $new; } return $return; } /** * Find files within a directory (recurse for subdirectories) * * @param string $dir Directory to search * @param array &$files Reference to found files * * @return void */ protected function findFiles($dir, &$files) { $d = dir($dir); while (false !== ($entry = $d->read())) { if ($entry === '.' || $entry === '..') { continue; } $full = $this->normalizePath("{$d->path}/$entry"); $relative_path = substr($full, $this->substr_start); if (is_dir($full)) { if ($entry[0] === '.' || preg_match('~(?:/vendors?|/zaudio/audioplayer)$~', $full)) { // special case if ($entry !== '.scripts') { continue; } } if (in_array($relative_path, array('node_modules', 'docs/_build'))) { continue; } $this->findFiles($full, $files); } else { // file if (basename($dir) === 'languages' && $entry !== 'en.php') { continue; } if ($relative_path === 'install/config/htaccess.dist' || preg_match($this->file_pattern, $entry)) { $files[] = $full; continue; } } } $d->close(); } /** * Normalize a path * * @param string $path A file/dir path * * @return string */ protected function normalizePath($path) { return str_replace('\\', '/', rtrim($path, '/\\')); } }