CodeStyle.php 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. <?php
  2. namespace Elgg\Project;
  3. /**
  4. * Internal component to detect and fix some whitespace issues
  5. *
  6. * @access private
  7. *
  8. * @package Elgg.Core
  9. * @subpackage Project
  10. */
  11. class CodeStyle {
  12. const KEY_NEW_CONTENT = 'new_content';
  13. const KEY_REMAINING = 'remaining';
  14. const KEY_CORRECTIONS = 'corrections';
  15. /**
  16. * @var string Regex pattern for file extensions to analyze
  17. */
  18. protected $file_pattern = '~\.(?:php|js|css|xml|json|yml|txt|rst|md|gitignore|htaccess|mailmap|sh)$~';
  19. /**
  20. * @var int The start argument such that substr(filepath, start) will return the filepath as a relative
  21. * path from the project root. E.g. if the root is /path/to/Elgg, this property will be set to
  22. * 14. That way substr('/path/to/Elgg/foo/bar.php', 14) => foo/bar.php
  23. */
  24. protected $substr_start;
  25. /**
  26. * Fix problems in a directory of files and return a report.
  27. *
  28. * @param string $root Root directory
  29. * @param bool $dry_run If set to true, no files will be written
  30. * @return array Report of notable files
  31. */
  32. public function fixDirectory($root, $dry_run = false) {
  33. $return = array();
  34. $this->substr_start = strlen($this->normalizePath($root)) + 1;
  35. $files = $this->findFilesToAnalyze($root);
  36. foreach ($files as $file) {
  37. $report = $this->analyzeFile($file);
  38. $key = substr($file, $this->substr_start);
  39. if ($dry_run) {
  40. $errors = $report[self::KEY_REMAINING];
  41. array_splice($errors, count($errors), 0, $report[self::KEY_CORRECTIONS]);
  42. if ($errors) {
  43. $return[$key] = $errors;
  44. }
  45. } else {
  46. if ($report[self::KEY_NEW_CONTENT] !== null) {
  47. file_put_contents($file, $report[self::KEY_NEW_CONTENT]);
  48. }
  49. if ($report[self::KEY_REMAINING]) {
  50. $return[$key][self::KEY_REMAINING] = $report[self::KEY_REMAINING];
  51. }
  52. if ($report[self::KEY_CORRECTIONS]) {
  53. $return[$key][self::KEY_CORRECTIONS] = $report[self::KEY_CORRECTIONS];
  54. }
  55. }
  56. }
  57. return $return;
  58. }
  59. /**
  60. * Find files which can be analyzed/fixed by this component
  61. *
  62. * @param string $root Root directory
  63. * @return string[] File paths. All directory separators will be "/"
  64. */
  65. public function findFilesToAnalyze($root) {
  66. $files = array();
  67. $this->substr_start = strlen($this->normalizePath($root)) + 1;
  68. $this->findFiles(rtrim($root, '/\\'), $files);
  69. return $files;
  70. }
  71. /**
  72. * Analyze a file for problems and return a report
  73. *
  74. * @param string $filepath Path of file to analyze
  75. * @param string $content The file's content (optional)
  76. *
  77. * @return array Report with keys:
  78. *
  79. * remaining_problems : string[] Problems which could not be fixed
  80. * corrections : string[] Problems which were fixed
  81. * new_content : string|null Null if no corrections made, otherwise the corrected content
  82. */
  83. public function analyzeFile($filepath, $content = null) {
  84. if (!is_string($content)) {
  85. $content = file_get_contents($filepath);
  86. }
  87. $old = $content;
  88. unset($content);
  89. $return = array(
  90. self::KEY_REMAINING => array(),
  91. self::KEY_CORRECTIONS => array(),
  92. self::KEY_NEW_CONTENT => null,
  93. );
  94. // remove WS after non-WS
  95. $new = preg_replace('~(\S)[ \t]+(\r?\n)~', '$1$2', $old, -1, $count);
  96. if ($count) {
  97. $return[self::KEY_CORRECTIONS][] = "line(s) with trailing whitespace ($count)";
  98. }
  99. // don't risk breaking code blocks
  100. if (!preg_match('~\.(?:rst|md)$~', $filepath)) {
  101. // remove WS from empty lines
  102. $new = preg_replace('~^[ \t]+$~m', '', $new, -1, $count);
  103. if ($count) {
  104. $return[self::KEY_CORRECTIONS][] = "empty line(s) with whitespace ($count)";
  105. }
  106. }
  107. if (pathinfo($filepath, PATHINFO_EXTENSION) === 'php') {
  108. // remove close PHP tag at file end
  109. $new = preg_replace('~\?>\s*$~', '', $new, -1, $count);
  110. if ($count) {
  111. $return[self::KEY_CORRECTIONS][] = 'unnecessary close PHP tag';
  112. }
  113. }
  114. if ($new !== $old) {
  115. $return[self::KEY_NEW_CONTENT] = $new;
  116. }
  117. return $return;
  118. }
  119. /**
  120. * Find files within a directory (recurse for subdirectories)
  121. *
  122. * @param string $dir Directory to search
  123. * @param array &$files Reference to found files
  124. *
  125. * @return void
  126. */
  127. protected function findFiles($dir, &$files) {
  128. $d = dir($dir);
  129. while (false !== ($entry = $d->read())) {
  130. if ($entry === '.' || $entry === '..') {
  131. continue;
  132. }
  133. $full = $this->normalizePath("{$d->path}/$entry");
  134. $relative_path = substr($full, $this->substr_start);
  135. if (is_dir($full)) {
  136. if ($entry[0] === '.' || preg_match('~(?:/vendors?|/zaudio/audioplayer)$~', $full)) {
  137. // special case
  138. if ($entry !== '.scripts') {
  139. continue;
  140. }
  141. }
  142. if (in_array($relative_path, array('node_modules', 'docs/_build'))) {
  143. continue;
  144. }
  145. $this->findFiles($full, $files);
  146. } else {
  147. // file
  148. if (basename($dir) === 'languages' && $entry !== 'en.php') {
  149. continue;
  150. }
  151. if ($relative_path === 'install/config/htaccess.dist' || preg_match($this->file_pattern, $entry)) {
  152. $files[] = $full;
  153. continue;
  154. }
  155. }
  156. }
  157. $d->close();
  158. }
  159. /**
  160. * Normalize a path
  161. *
  162. * @param string $path A file/dir path
  163. *
  164. * @return string
  165. */
  166. protected function normalizePath($path) {
  167. return str_replace('\\', '/', rtrim($path, '/\\'));
  168. }
  169. }