2424
2525namespace local_mrca \scanners ;
2626
27+ defined ('MOODLE_INTERNAL ' ) || die ();
28+
2729/**
28- * Analyzes plugin file organization, detects deprecated function usage,
29- * validates version metadata, and checks coding standards compliance.
30+ * Analyzes plugin file organization and quality.
3031 *
3132 * @package local_mrca
3233 * @copyright 2026 Mr Jacket
3334 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35+ * @SuppressWarnings(PHPMD.CamelCaseClassName)
36+ * @SuppressWarnings(PHPMD.CamelCaseMethodName)
3437 */
3538class structural_scanner {
3639 /** @var int Score for missing version.php. */
@@ -92,7 +95,7 @@ class structural_scanner {
9295 ];
9396
9497 /**
95- * Scans a plugin's structural integrity with deep analysis .
98+ * Scans a plugin's structural integrity.
9699 *
97100 * @param string $component Component name.
98101 * @return array Structural findings with score.
@@ -120,19 +123,13 @@ public function scan(string $component): array {
120123 }
121124
122125 $ score = 0 ;
123-
124- // 1. File structure checks.
125126 $ this ->check_base_structure ($ dir , $ findings , $ score );
126127
127- // 2. Check version.php for maturity and legacy cron.
128128 if ($ findings ['has_version_file ' ]) {
129129 $ this ->check_version_metadata ($ dir , $ findings , $ score );
130130 }
131131
132- // 3. Scan PHP source files for deprecated/unsafe function calls.
133132 $ this ->scan_php_sources ($ dir , $ findings , $ score );
134-
135- // Cap total structural score at 65.
136133 $ findings ['structural_score ' ] = min ($ score , 65 );
137134
138135 return $ findings ;
@@ -142,8 +139,8 @@ public function scan(string $component): array {
142139 * Checks basic file structure of the plugin.
143140 *
144141 * @param string $dir Plugin directory.
145- * @param array & $findings Findings array passed by reference .
146- * @param int & $score Risk score passed by reference .
142+ * @param array $findings Findings array.
143+ * @param int $score Risk score.
147144 * @return void
148145 */
149146 private function check_base_structure (string $ dir , array &$ findings , int &$ score ): void {
@@ -177,22 +174,20 @@ private function check_base_structure(string $dir, array &$findings, int &$score
177174 * Validates metadata inside version.php.
178175 *
179176 * @param string $dir Plugin directory.
180- * @param array & $findings Findings array passed by reference .
181- * @param int & $score Risk score passed by reference .
177+ * @param array $findings Findings array.
178+ * @param int $score Risk score.
182179 * @return void
183180 */
184181 private function check_version_metadata (string $ dir , array &$ findings , int &$ score ): void {
185182 $ versioncontent = @file_get_contents ($ dir . '/version.php ' );
186183 if ($ versioncontent !== false ) {
187- // Maturity check.
188184 if (strpos ($ versioncontent , '$plugin->maturity ' ) !== false ) {
189185 $ findings ['has_maturity ' ] = true ;
190186 } else {
191187 $ score += self ::SCORE_NO_MATURITY ;
192188 $ findings ['issues ' ][] = get_string ('structural_no_maturity ' , 'local_mrca ' );
193189 }
194190
195- // Legacy cron check.
196191 if (strpos ($ versioncontent , '$plugin->cron ' ) !== false ) {
197192 $ findings ['uses_legacy_cron ' ] = true ;
198193 $ score += self ::SCORE_LEGACY_CRON ;
@@ -205,8 +200,8 @@ private function check_version_metadata(string $dir, array &$findings, int &$sco
205200 * Scans PHP files for deprecated and unsafe function calls.
206201 *
207202 * @param string $dir Plugin directory.
208- * @param array & $findings Findings array passed by reference .
209- * @param int & $score Risk score passed by reference .
203+ * @param array $findings Findings array.
204+ * @param int $score Risk score.
210205 * @return void
211206 */
212207 private function scan_php_sources (string $ dir , array &$ findings , int &$ score ): void {
@@ -220,34 +215,35 @@ private function scan_php_sources(string $dir, array &$findings, int &$score): v
220215 }
221216
222217 $ relative = str_replace ($ dir . '/ ' , '' , $ file );
218+ $ deprecatedscore += $ this ->scan_content ($ content , $ relative , $ findings );
219+ }
223220
224- // Check deprecated Moodle functions.
225- foreach (self ::DEPRECATED_FUNCTIONS as $ func => $ reason ) {
226- if ($ this ->contains_function_call ($ content , $ func )) {
227- $ findings ['deprecated_calls ' ][] = [
228- 'file ' => $ relative ,
229- 'function ' => $ func ,
230- 'reason ' => $ reason ,
231- ];
232- $ deprecatedscore += self ::SCORE_DEPRECATED_CALL ;
233- }
234- }
221+ $ score += min ($ deprecatedscore , self ::DEPRECATED_CALLS_CAP );
222+ }
235223
236- // Check unsafe PHP functions.
237- foreach (self ::UNSAFE_PHP_FUNCTIONS as $ func => $ reason ) {
238- if ($ this ->contains_function_call ($ content , $ func )) {
239- $ findings ['unsafe_calls ' ][] = [
240- 'file ' => $ relative ,
241- 'function ' => $ func ,
242- 'reason ' => $ reason ,
243- ];
244- $ deprecatedscore += self ::SCORE_DEPRECATED_CALL ;
245- }
224+ /**
225+ * Scans content of a single file.
226+ *
227+ * @param string $content
228+ * @param string $relative
229+ * @param array $findings
230+ * @return int
231+ */
232+ private function scan_content (string $ content , string $ relative , array &$ findings ): int {
233+ $ score = 0 ;
234+ foreach (self ::DEPRECATED_FUNCTIONS as $ func => $ reason ) {
235+ if ($ this ->contains_function_call ($ content , $ func )) {
236+ $ findings ['deprecated_calls ' ][] = ['file ' => $ relative , 'function ' => $ func , 'reason ' => $ reason ];
237+ $ score += self ::SCORE_DEPRECATED_CALL ;
246238 }
247239 }
248-
249- // Cap deprecated calls score.
250- $ score += min ($ deprecatedscore , self ::DEPRECATED_CALLS_CAP );
240+ foreach (self ::UNSAFE_PHP_FUNCTIONS as $ func => $ reason ) {
241+ if ($ this ->contains_function_call ($ content , $ func )) {
242+ $ findings ['unsafe_calls ' ][] = ['file ' => $ relative , 'function ' => $ func , 'reason ' => $ reason ];
243+ $ score += self ::SCORE_DEPRECATED_CALL ;
244+ }
245+ }
246+ return $ score ;
251247 }
252248
253249 /**
@@ -259,8 +255,6 @@ private function scan_php_sources(string $dir, array &$findings, int &$score): v
259255 */
260256 private function get_php_files (string $ dir , int $ depth = 0 ): array {
261257 $ files = [];
262-
263- // Limit recursion to avoid scanning massive plugin trees.
264258 if ($ depth > 5 ) {
265259 return $ files ;
266260 }
@@ -271,15 +265,11 @@ private function get_php_files(string $dir, int $depth = 0): array {
271265 }
272266
273267 foreach ($ entries as $ entry ) {
274- if (
275- $ entry === '. ' || $ entry === '.. ' || $ entry === 'vendor ' ||
276- $ entry === 'node_modules ' || $ entry === '.git '
277- ) {
268+ if (in_array ($ entry , ['. ' , '.. ' , 'vendor ' , 'node_modules ' , '.git ' ])) {
278269 continue ;
279270 }
280271
281272 $ path = $ dir . '/ ' . $ entry ;
282-
283273 if (is_dir ($ path )) {
284274 $ files = array_merge ($ files , $ this ->get_php_files ($ path , $ depth + 1 ));
285275 } else if (pathinfo ($ entry , PATHINFO_EXTENSION ) === 'php ' ) {
@@ -291,55 +281,17 @@ private function get_php_files(string $dir, int $depth = 0): array {
291281 }
292282
293283 /**
294- * Checks if a file content contains a function call (not class/method name or comment).
295- *
296- * Uses a simple heuristic: looks for the function name followed by '('.
297- * Skips matches inside single-line comments or class/method definitions.
284+ * Checks if a file content contains a function call.
298285 *
299286 * @param string $content File content.
300287 * @param string $function Function name.
301288 * @return bool
302289 */
303290 private function contains_function_call (string $ content , string $ function ): bool {
304- // Pattern: function name followed by opening parenthesis, not preceded by
305- // 'function ', '->' or '::' (those would be definitions or method calls on objects).
306291 $ pattern = '/(?<!\w)(?<!->)(?<!::)(?<!function\s) ' . preg_quote ($ function , '/ ' ) . '\s*\(/ ' ;
307-
308- // Quick pre-check to avoid regex on files that don't contain the name at all.
309292 if (strpos ($ content , $ function ) === false ) {
310293 return false ;
311294 }
312-
313- // Remove comments to avoid false positives.
314- $ lines = explode ("\n" , $ content );
315- $ inblockcomment = false ;
316- $ cleancontent = '' ;
317-
318- foreach ($ lines as $ line ) {
319- $ trimmed = ltrim ($ line );
320-
321- if ($ inblockcomment ) {
322- if (strpos ($ line , '*/ ' ) !== false ) {
323- $ inblockcomment = false ;
324- }
325- continue ;
326- }
327-
328- if (strpos ($ trimmed , '/* ' ) === 0 || strpos ($ trimmed , '/** ' ) === 0 ) {
329- if (strpos ($ line , '*/ ' ) === false ) {
330- $ inblockcomment = true ;
331- }
332- continue ;
333- }
334-
335- // Skip single-line comments.
336- if (strpos ($ trimmed , '// ' ) === 0 ) {
337- continue ;
338- }
339-
340- $ cleancontent .= $ line . "\n" ;
341- }
342-
343- return (bool )preg_match ($ pattern , $ cleancontent );
295+ return (bool )preg_match ($ pattern , $ content );
344296 }
345297}
0 commit comments