PluginProbe
Parse.ly / 3.23.0
Parse.ly v3.23.0
3.24.1 3.24.0 3.23.7 3.23.6 3.23.5 3.23.4 3.23.3 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.17.0 3.18.0 3.18.1 3.19.0 3.19.1 3.19.2 3.19.3 3.2.0 3.2.1 3.20.0 3.20.1 3.20.2 3.20.3 All 105 releases
wp-parsely / lint.php

lint.php in Parse.ly 3.23.0, at lint.php

187 lines 5.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Small linter that can detect and fix some code style violations
4 *
5 * Usage:
6 * php lint.php // Show errors.
7 * php lint.php --fix // Auto-fix errors where supported.
8 *
9 * @since 3.18.0
10 */
11
12 // Configuration.
13 $config = array(
14 new Lint_Rule(
15 'Two or more consecutive empty lines found',
16 '/(\r?\n){3,}/',
17 '/\.(php|js|css|html|scss|yml|md|ts|tsx|sh|json)$/',
18 "\n\n"
19 ),
20 new Lint_Rule(
21 'One or more empty lines between closing brackets found',
22 '/(\})\n{2,}(\})/',
23 '/\.(php|js|css|html|scss|yml|md|ts|tsx|sh|json)$/',
24 "$1\n$2"
25 ),
26 );
27
28 // Run the rules and display any errors.
29 foreach ( $config as $rule ) {
30 $errors = $rule->run( in_array( '--fix', $argv ) );
31
32 if ( '' !== $errors ) {
33 $exit_code = 1;
34 echo $errors;
35 }
36 }
37
38 exit( $exit_code ?? 0 );
39
40 /**
41 * A class representing a linting rule.
42 *
43 * @since 3.18.0
44 */
45 class Lint_Rule {
46 /**
47 * @var string Error message to display when this linting rule is violated.
48 */
49 private $error_message;
50
51 /**
52 * @var string Regex pattern used for rule violation detection.
53 */
54 private $error_pattern;
55
56 /**
57 * @var string Regex pattern used to specify the files to search in.
58 */
59 private $include_files;
60
61 /**
62 * @var string|null Optional regex pattern used to auto-fix the violation.
63 */
64 private $fix_pattern;
65
66 /**
67 * @var array<string> Directories to exclude from search.
68 */
69 private $exclude_dirs;
70
71 /**
72 * @var array<string> Array of errors found when running the rule.
73 */
74 private $errors = array();
75
76 /**
77 * Constructor.
78 *
79 * @since 3.18.0
80 *
81 * @param string $error_message The error message to display.
82 * @param string $error_pattern The regex matching the rule violation.
83 * @param string $include_files The regex matching the files to search in.
84 * @param string|null $fix_pattern The regex to fix any violations (optional).
85 * @param array<string> $exclude_dirs The directories to exclude from search.
86 */
87 public function __construct(
88 string $error_message,
89 string $error_pattern,
90 string $include_files,
91 ?string $fix_pattern = null,
92 array $exclude_dirs = array( 'artifacts', 'build', 'vendor', 'node_modules' )
93 ) {
94 $this->error_message = $error_message;
95 $this->error_pattern = $error_pattern;
96 $this->include_files = $include_files;
97 $this->fix_pattern = $fix_pattern;
98 $this->exclude_dirs = $exclude_dirs;
99 }
100
101 /**
102 * Runs the rule to detect or auto-fix any violations.
103 *
104 * @since 3.18.0
105 *
106 * @param bool $auto_fix_mode Whether violations should be auto-fixed.
107 * @return string The violations found, or an empty string.
108 */
109 public function run( $auto_fix_mode ): string {
110 $base_dir = __DIR__ . '/';
111 $iterator = $this->create_iterator( $this->exclude_dirs );
112
113 // Iterate over the directories and files.
114 foreach ( $iterator as $file ) {
115 // Check if the item is a file that should be checked.
116 if ( $file->isFile() && preg_match( $this->include_files, $file->getFilename() ) ) {
117 $file_content = file_get_contents( $file->getPathname() );
118
119 // Check for rule violations.
120 if ( preg_match_all( $this->error_pattern, $file_content, $matches, PREG_OFFSET_CAPTURE ) ) {
121 $relative_path = str_replace( $base_dir, '', $file->getPathname() );
122
123 // Auto-fix or log the issue.
124 if ( $auto_fix_mode && null !== $this->fix_pattern ) {
125 $fixed_content = preg_replace( $this->error_pattern, $this->fix_pattern, $file_content );
126
127 if ( null === $fixed_content ) {
128 $this->errors[] = "Error while trying to lint {$relative_path}.";
129 }
130
131 if ( false === file_put_contents( $file->getPathname(), $fixed_content ) ) {
132 $this->errors[] = "Failed to save fix for {$relative_path}.";
133 }
134 } else {
135 foreach ( $matches[0] as $match ) {
136 $line_number = substr_count( substr( $file_content, 0, $match[1] ), "\n" ) + 1;
137 $this->errors[] = "{$relative_path} \033[90m(line {$line_number})\033[0m";
138 }
139 }
140 }
141 }
142 }
143
144 if ( empty( $this->errors ) ) {
145 return '';
146 }
147
148 $output = "\033[31mERROR: {$this->error_message}.\033[0m\n";
149 if ( null !== $this->fix_pattern ) {
150 $output .= "\033[33mAuto-fixable with the --fix parameter.\033[0m\n";
151 }
152 $output .= "\033[36mYou can use the {$this->error_pattern} regex to find the offending code.\033[0m\n\n";
153
154 foreach ( $this->errors as $error ) {
155 $output .= " - {$error}\n";
156 }
157
158 return $output . "\n\n";
159 }
160
161 /**
162 * Creates an iterator that iterates across the project's folder structure,
163 * skipping excluded directories.
164 *
165 * @since 3.18.0
166 *
167 * @return RecursiveIteratorIterator
168 */
169 private function create_iterator(): RecursiveIteratorIterator {
170 return new RecursiveIteratorIterator(
171 new RecursiveCallbackFilterIterator(
172 new RecursiveDirectoryIterator( __DIR__, RecursiveDirectoryIterator::SKIP_DOTS ),
173 function ( $current, $key, $iterator ) {
174 if ( $iterator->hasChildren() && in_array( $current->getFilename(), $this->exclude_dirs ) ) {
175 return false; // Skip excluded directories.
176 }
177
178 return true;
179 }
180 ),
181 RecursiveIteratorIterator::SELF_FIRST
182 );
183 }
184 }
185
186 // phpcs:ignoreFile
187