• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

equalizedigital / accessibility-checker / 18233292004

03 Oct 2025 08:40PM UTC coverage: 57.654% (-1.7%) from 59.385%
18233292004

push

github

web-flow
Merge pull request #1250 from equalizedigital/steve/no-issue/actions-filters-docs

tools: regenerate hooks docs, extract PHPDoc, add CI to auto-create when docs change

0 of 210 new or added lines in 1 file covered. (0.0%)

4154 of 7205 relevant lines covered (57.65%)

3.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/tools/generate-hooks-docs.php
1
<?php
2
/**
3
 * Generate Hooks Documentation.
4
 *
5
 * Scans the repository for add_action/add_filter calls and generates
6
 * `docs/hooks.md` containing a simple table of discovered hooks.
7
 *
8
 * Only hooks that are plugin-specific (prefixed with `edac_` or `edacp_`)
9
 * are included.
10
 *
11
 * @package AccessibilityChecker
12
 */
13

NEW
14
$root = dirname( __DIR__ );
×
15

16
// Configuration: allow overriding via environment variables for CI or local runs.
NEW
17
$github_owner  = getenv( 'GH_OWNER' ) ? getenv( 'GH_OWNER' ) : 'equalizedigital';
×
NEW
18
$github_repo   = getenv( 'GH_REPO' ) ? getenv( 'GH_REPO' ) : 'accessibility-checker';
×
NEW
19
$github_branch = getenv( 'GH_BRANCH' ) ? getenv( 'GH_BRANCH' ) : 'develop';
×
20

NEW
21
$rii = new RecursiveIteratorIterator(
×
NEW
22
        new RecursiveDirectoryIterator( $root )
×
NEW
23
);
×
24

25
/**
26
 * Find the nearest PHPDoc block before $pos within $contents and return summary and @since.
27
 *
28
 * @param string $contents Full file contents.
29
 * @param int    $pos      Byte offset where the hook call begins.
30
 * @param int    $max_lines Maximum number of lines allowed between docblock end and pos.
31
 * @return array ['summary' => string, 'since' => string]
32
 */
33
function edac_find_nearest_docblock( $contents, $pos, $max_lines = 10 ) {
NEW
34
        $leading = substr( $contents, 0, $pos );
×
35

36
        // Find all docblocks before the position.
NEW
37
        if ( preg_match_all( '/\/\*\*(?:[^*]|\*(?!\/))*\*\//s', $leading, $matches, PREG_OFFSET_CAPTURE ) ) {
×
NEW
38
                $last     = end( $matches[0] );
×
NEW
39
                $doc_text = $last[0];
×
NEW
40
                $doc_pos  = $last[1];
×
NEW
41
                $doc_end  = $doc_pos + strlen( $doc_text );
×
42

43
                // Lines between doc end and the hook position.
NEW
44
                $between  = substr( $contents, $doc_end, max( 0, $pos - $doc_end ) );
×
NEW
45
                $line_gap = substr_count( $between, "\n" );
×
NEW
46
                if ( $line_gap <= $max_lines ) {
×
47
                        // Clean comment markers and collect lines.
NEW
48
                        $lines = preg_split( '/\r?\n/', $doc_text );
×
NEW
49
                        $clean = [];
×
NEW
50
                        foreach ( $lines as $ln ) {
×
NEW
51
                                $ln = preg_replace( '/^\s*\/\*\*\s?/', '', $ln );
×
NEW
52
                                $ln = preg_replace( '/^\s*\*\s?/', '', $ln );
×
NEW
53
                                $ln = preg_replace( '/\s*\*\/$/', '', $ln );
×
NEW
54
                                $ln = trim( $ln );
×
NEW
55
                                if ( '' !== $ln ) {
×
NEW
56
                                        $clean[] = $ln;
×
57
                                }
58
                        }
59

NEW
60
                        $summary = '';
×
NEW
61
                        $since   = '';
×
NEW
62
                        if ( ! empty( $clean ) ) {
×
63
                                // Find first line that is not an @tag (e.g., not starting with @).
NEW
64
                                foreach ( $clean as $c ) {
×
NEW
65
                                        if ( 0 === strpos( $c, '@' ) ) {
×
NEW
66
                                                continue;
×
67
                                        }
NEW
68
                                        $summary = $c;
×
NEW
69
                                        break;
×
70
                                }
71

72
                                // Find @since tag if present.
NEW
73
                                foreach ( $clean as $c ) {
×
NEW
74
                                        if ( 0 === strpos( $c, '@since' ) ) {
×
NEW
75
                                                $parts = preg_split( '/\s+/', $c, 2 );
×
NEW
76
                                                $since = isset( $parts[1] ) ? $parts[1] : '';
×
NEW
77
                                                break;
×
78
                                        }
79
                                }
80
                        }
81

NEW
82
                        return [
×
NEW
83
                                'summary' => $summary,
×
NEW
84
                                'since'   => $since,
×
NEW
85
                        ];
×
86
                }
87
        }
88

89
        // Fallback: try to find the enclosing function's docblock (if any).
NEW
90
        if ( preg_match_all( '/function\s+[A-Za-z0-9_]+\s*\([^\)]*\)\s*\{?/s', $leading, $fn_matches, PREG_OFFSET_CAPTURE ) ) {
×
NEW
91
                $last_fn = end( $fn_matches[0] );
×
NEW
92
                $fn_pos  = $last_fn[1];
×
93

94
                // Find the last docblock before the function.
NEW
95
                if ( preg_match_all( '/\/\*\*(?:[^*]|\*(?!\/))*\*\//s', substr( $contents, 0, $fn_pos ), $m2, PREG_OFFSET_CAPTURE ) ) {
×
NEW
96
                        $last2    = end( $m2[0] );
×
NEW
97
                        $doc_text = $last2[0];
×
98

99
                        // Clean and extract as above.
NEW
100
                        $lines = preg_split( '/\r?\n/', $doc_text );
×
NEW
101
                        $clean = [];
×
NEW
102
                        foreach ( $lines as $ln ) {
×
NEW
103
                                $ln = preg_replace( '/^\s*\/\*\*\s?/', '', $ln );
×
NEW
104
                                $ln = preg_replace( '/^\s*\*\s?/', '', $ln );
×
NEW
105
                                $ln = preg_replace( '/\s*\*\/$/', '', $ln );
×
NEW
106
                                $ln = trim( $ln );
×
NEW
107
                                if ( '' !== $ln ) {
×
NEW
108
                                        $clean[] = $ln;
×
109
                                }
110
                        }
111

NEW
112
                        $summary = '';
×
NEW
113
                        $since   = '';
×
NEW
114
                        if ( ! empty( $clean ) ) {
×
NEW
115
                                foreach ( $clean as $c ) {
×
NEW
116
                                        if ( 0 === strpos( $c, '@' ) ) {
×
NEW
117
                                                continue;
×
118
                                        }
NEW
119
                                        $summary = $c;
×
NEW
120
                                        break;
×
121
                                }
NEW
122
                                foreach ( $clean as $c ) {
×
NEW
123
                                        if ( 0 === strpos( $c, '@since' ) ) {
×
NEW
124
                                                $parts = preg_split( '/\s+/', $c, 2 );
×
NEW
125
                                                $since = isset( $parts[1] ) ? $parts[1] : '';
×
NEW
126
                                                break;
×
127
                                        }
128
                                }
129
                        }
130

NEW
131
                        return [
×
NEW
132
                                'summary' => $summary,
×
NEW
133
                                'since'   => $since,
×
NEW
134
                        ];
×
135
                }
136
        }
137

NEW
138
        return [
×
NEW
139
                'summary' => '',
×
NEW
140
                'since'   => '',
×
NEW
141
        ];
×
142
}
143

144
// Collect candidates per hook so we can pick the single defining location.
NEW
145
$candidates = [];
×
146

NEW
147
foreach ( $rii as $file ) {
×
NEW
148
        if ( $file->isDir() ) {
×
NEW
149
                continue;
×
150
        }
151

NEW
152
        $filepath = $file->getPathname();
×
153

154
        // Skip vendor directory and non-PHP files.
NEW
155
        if ( false !== strpos( $filepath, DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR ) ) {
×
NEW
156
                continue;
×
157
        }
158

NEW
159
        if ( ! preg_match( '/\.php$/', $filepath ) ) {
×
NEW
160
                continue;
×
161
        }
162

NEW
163
        $contents = file_get_contents( $filepath ); // phpcs:ignore WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown
×
164

165
        // 1) Find hook definitions: do_action (action) and apply_filters (filter).
NEW
166
        if ( preg_match_all( '/\b(do_action|apply_filters)\s*\(\s*([\'\"])([^\2]+?)\2/s', $contents, $def_matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE ) ) {
×
NEW
167
                foreach ( $def_matches as $row ) {
×
NEW
168
                        $fn        = $row[1][0];
×
NEW
169
                        $hook_name = $row[3][0];
×
NEW
170
                        $pos       = $row[0][1];
×
NEW
171
                        $line      = 1;
×
NEW
172
                        if ( false !== $pos ) {
×
NEW
173
                                $line = substr_count( substr( $contents, 0, $pos ), "\n" ) + 1;
×
174
                        }
175

NEW
176
                        if ( preg_match( '/^(edac_|edacp_)/', $hook_name ) ) {
×
NEW
177
                                $relpath = substr( $filepath, strlen( $root ) + 1 );
×
178

179
                                // Try to capture the nearest PHPDoc block (within 10 lines) for summary and @since.
NEW
180
                                $doc_info                   = edac_find_nearest_docblock( $contents, $pos, 10 );
×
NEW
181
                                $candidates[ $hook_name ][] = [
×
NEW
182
                                        'hook'    => $hook_name,
×
NEW
183
                                        'type'    => ( 'do_action' === $fn ) ? 'action' : 'filter',
×
NEW
184
                                        'file'    => $relpath,
×
NEW
185
                                        'line'    => $line,
×
NEW
186
                                        'source'  => 'definition',
×
NEW
187
                                        'summary' => $doc_info['summary'],
×
NEW
188
                                        'since'   => $doc_info['since'],
×
NEW
189
                                ];
×
190
                        }
191
                }
192
        }
193

194
        // 2) Find listeners (add_action/add_filter) - less authoritative for a definition.
NEW
195
        if ( preg_match_all( '/add_(action|filter)\s*\(\s*([\'\"])([^\2]+?)\2/', $contents, $matches_rows, PREG_SET_ORDER ) ) {
×
NEW
196
                foreach ( $matches_rows as $row ) {
×
NEW
197
                        $hook_type = $row[1];
×
NEW
198
                        $hook_name = $row[3];
×
NEW
199
                        $pos       = strpos( $contents, $row[0] );
×
NEW
200
                        $line      = 1;
×
NEW
201
                        if ( false !== $pos ) {
×
NEW
202
                                $line = substr_count( substr( $contents, 0, $pos ), "\n" ) + 1;
×
203
                        }
204

NEW
205
                        if ( preg_match( '/^(edac_|edacp_)/', $hook_name ) ) {
×
NEW
206
                                $relpath = substr( $filepath, strlen( $root ) + 1 );
×
207

208
                                // For listeners, also try to find a nearby docblock (within 6 lines).
NEW
209
                                $doc_info = edac_find_nearest_docblock( $contents, $pos, 6 );
×
210

NEW
211
                                $candidates[ $hook_name ][] = [
×
NEW
212
                                        'hook'    => $hook_name,
×
NEW
213
                                        'type'    => $hook_type,
×
NEW
214
                                        'file'    => $relpath,
×
NEW
215
                                        'line'    => $line,
×
NEW
216
                                        'source'  => 'listener',
×
NEW
217
                                        'summary' => $doc_info['summary'],
×
NEW
218
                                        'since'   => $doc_info['since'],
×
NEW
219
                                ];
×
220
                        }
221
                }
222
        }
223
}
224

NEW
225
$github_base = sprintf( 'https://github.com/%s/%s/blob/%s/', $github_owner, $github_repo, $github_branch );
×
226

227
// Choose the best candidate per hook. Strategy: prefer definitions when present, and prefer files
228
// outside of tests/, dist/, build/, vendor/ etc.
NEW
229
$hooks = [];
×
NEW
230
foreach ( $candidates as $hook_name => $list ) {
×
231
        // First, filter out entries from non-authoritative directories.
NEW
232
        $filtered_list = array_filter(
×
NEW
233
                $list,
×
NEW
234
                static function ( $entry ) {
×
235
                        // Skip entries from test/build/vendor directories.
NEW
236
                        $excluded_paths = [
×
NEW
237
                                '/tests/',
×
NEW
238
                                '/dist/',
×
NEW
239
                                '/build/',
×
NEW
240
                                '/docs/',
×
NEW
241
                                '/.github/',
×
NEW
242
                                '/node_modules/',
×
NEW
243
                                '/vendor/',
×
NEW
244
                        ];
×
245
                        
NEW
246
                        foreach ( $excluded_paths as $excluded_path ) {
×
NEW
247
                                if ( false !== strpos( $entry['file'], $excluded_path ) ) {
×
NEW
248
                                        return false;
×
249
                                }
250
                        }
NEW
251
                        return true;
×
NEW
252
                }
×
NEW
253
        );
×
254
        
255
        // If we filtered out everything, fall back to the original list.
NEW
256
        if ( ! empty( $filtered_list ) ) {
×
NEW
257
                $list = array_values( $filtered_list );
×
258
        }
259
        
260
        // If any definition candidates exist, narrow to them. Otherwise keep listeners.
NEW
261
        $defs = array_filter(
×
NEW
262
                $list,
×
NEW
263
                static function ( $e ) {
×
NEW
264
                        return isset( $e['source'] ) && 'definition' === $e['source'];
×
NEW
265
                } 
×
NEW
266
        );
×
NEW
267
        if ( ! empty( $defs ) ) {
×
NEW
268
                $list = array_values( $defs );
×
269
        }
270

NEW
271
        $best = null;
×
NEW
272
        foreach ( $list as $entry ) {
×
NEW
273
                $score = 0;
×
274
                // Heuristics to penalize less-authoritative locations.
NEW
275
                if ( false !== strpos( $entry['file'], '/tests/' ) ) {
×
NEW
276
                        $score += 1000;
×
277
                }
NEW
278
                if ( false !== strpos( $entry['file'], '/dist/' ) ) {
×
NEW
279
                        $score += 800;
×
280
                }
NEW
281
                if ( false !== strpos( $entry['file'], '/build/' ) ) {
×
NEW
282
                        $score += 600;
×
283
                }
NEW
284
                if ( false !== strpos( $entry['file'], '/vendor/' ) ) {
×
NEW
285
                        $score += 500;
×
286
                }
NEW
287
                if ( false !== strpos( $entry['file'], '/docs/' ) ) {
×
NEW
288
                        $score += 400;
×
289
                }
NEW
290
                if ( false !== strpos( $entry['file'], '/.github/' ) ) {
×
NEW
291
                        $score += 300;
×
292
                }
NEW
293
                if ( false !== strpos( $entry['file'], '/node_modules/' ) ) {
×
NEW
294
                        $score += 200;
×
295
                }
296

297
                // Lower line numbers are slightly preferred (first definition in file).
NEW
298
                $score = $score * 10000 + $entry['line'];
×
299

NEW
300
                if ( null === $best || $score < $best['score'] ) {
×
NEW
301
                        $best          = $entry;
×
NEW
302
                        $best['score'] = $score;
×
303
                }
304
        }
305

NEW
306
        if ( $best ) {
×
NEW
307
                $hooks[] = [
×
NEW
308
                        'hook'    => $best['hook'],
×
NEW
309
                        'type'    => $best['type'],
×
NEW
310
                        'file'    => $best['file'],
×
NEW
311
                        'line'    => $best['line'],
×
NEW
312
                        'summary' => isset( $best['summary'] ) ? $best['summary'] : '',
×
NEW
313
                        'since'   => isset( $best['since'] ) ? $best['since'] : '',
×
NEW
314
                ];
×
315
        }
316
}
317

NEW
318
usort(
×
NEW
319
        $hooks,
×
NEW
320
        static function ( $a, $b ) {
×
NEW
321
                return strcmp( $a['hook'], $b['hook'] );
×
NEW
322
        }
×
NEW
323
);
×
324

NEW
325
$out  = "# EDAC Hooks Reference\n\n";
×
NEW
326
$out .= "This document is auto-generated by `tools/generate-hooks-docs.php`. It lists only plugin-specific hooks (prefixed with `edac_` or `edacp_`) and links files to the plugin's GitHub branch.\n\n";
×
NEW
327
$out .= "| Hook | Type | File | Line | Description | Since |\n";
×
NEW
328
$out .= "| ---- | ---- | ---- | ---- | ----------- | ----- |\n";
×
NEW
329
foreach ( $hooks as $entry ) {
×
330
        // Create a GitHub link for the file and line.
NEW
331
        $parts     = explode( DIRECTORY_SEPARATOR, $entry['file'] );
×
NEW
332
        $enc_parts = array_map( 'rawurlencode', $parts );
×
NEW
333
        $rel       = implode( '/', $enc_parts );
×
NEW
334
        $url       = $github_base . $rel . '#L' . $entry['line'];
×
335

336
        // Clean summary for table cell (escape pipes and newlines).
NEW
337
        $summary = str_replace( [ "\n", "\r", '|' ], [ ' ', ' ', '\|' ], $entry['summary'] );
×
NEW
338
        $since   = $entry['since'];
×
339

NEW
340
        $out .= sprintf( "| `%s` | %s | [%s](%s) | %d | %s | %s |\n", $entry['hook'], $entry['type'], $entry['file'], $url, $entry['line'], $summary, $since );
×
341
}
342

NEW
343
$docs_path = realpath( __DIR__ . '/..' ) . '/docs/hooks.md';
×
NEW
344
file_put_contents( $docs_path, $out ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.file_ops_file_put_contents
×
345

NEW
346
printf( "Generated docs/hooks.md with %d hooks.\n", count( $hooks ) );
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc