Passed
Push — master ( c0a3a7...3b84a4 )
by Jeroen
58:51
created

engine/classes/Elgg/Project/CodeStyle.php (1 issue)

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

148
		$d = /** @scrutinizer ignore-call */ dir($dir);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
149
150
		while (false !== ($entry = $d->read())) {
151
			if ($entry === '.' || $entry === '..') {
152
				continue;
153
			}
154
155
			$full = $this->normalizePath("{$d->path}/$entry");
156
			$relative_path = substr($full, $this->substr_start);
157
158
			if (is_dir($full)) {
159
				if ($entry[0] === '.' || preg_match('~(?:/vendors?)$~', $full)) {
160
					// special case
161
					if ($entry !== '.scripts') {
162
						continue;
163
					}
164
				}
165
166
				if (in_array($relative_path, ['node_modules', 'docs/_build'])) {
167
					continue;
168
				}
169
170
				$this->findFiles($full, $files);
171
			} else {
172
				// file
173
174
				if (basename($dir) === 'languages' && $entry !== 'en.php') {
175
					continue;
176
				}
177
178
				if ($relative_path === 'install/config/htaccess.dist' || preg_match($this->file_pattern, $entry)) {
179
					$files[] = $full;
180
					continue;
181
				}
182
			}
183
		}
184
		$d->close();
185
	}
186
187
	/**
188
	 * Normalize a path
189
	 *
190
	 * @param string $path A file/dir path
191
	 *
192
	 * @return string
193
	 */
194
	protected function normalizePath($path) {
195
		return str_replace('\\', '/', rtrim($path, '/\\'));
196
	}
197
}
198