wp-devdocs-mcp 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,187 @@
1
+ import fg from 'fast-glob';
2
+ import { readFileSync, statSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { createHash } from 'node:crypto';
5
+ import { fetchSource } from './sources/index.js';
6
+ import { parsePhpFile } from './php-parser.js';
7
+ import { parseJsFile } from './js-parser.js';
8
+ import {
9
+ listSources,
10
+ getSource,
11
+ upsertHook,
12
+ markHooksRemoved,
13
+ upsertBlockRegistration,
14
+ upsertApiUsage,
15
+ getIndexedFile,
16
+ upsertIndexedFile,
17
+ updateSourceLastIndexed,
18
+ } from '../db/sqlite.js';
19
+ import { indexDocsSource } from '../docs/doc-index-manager.js';
20
+
21
+ const IGNORE_PATTERNS = [
22
+ '**/node_modules/**',
23
+ '**/vendor/**',
24
+ '**/dist/**',
25
+ '**/build/**',
26
+ '**/.git/**',
27
+ '**/tests/**',
28
+ '**/test/**',
29
+ '**/__tests__/**',
30
+ '**/spec/**',
31
+ ];
32
+
33
+ const PHP_PATTERNS = ['**/*.php'];
34
+ const JS_PATTERNS = ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'];
35
+
36
+ /**
37
+ * Index all enabled sources or a specific source.
38
+ * @param {object} opts - { sourceName, force }
39
+ * @returns {object} Stats about the indexing run
40
+ */
41
+ export async function indexSources(opts = {}) {
42
+ const { sourceName, force = false } = opts;
43
+
44
+ let sources;
45
+ if (sourceName) {
46
+ const source = getSource(sourceName);
47
+ if (!source) throw new Error(`Source not found: ${sourceName}`);
48
+ if (!source.enabled) throw new Error(`Source "${sourceName}" is disabled`);
49
+ sources = [source];
50
+ } else {
51
+ sources = listSources().filter(s => s.enabled);
52
+ }
53
+
54
+ if (sources.length === 0) {
55
+ return { message: 'No enabled sources found. Add a source first.' };
56
+ }
57
+
58
+ const stats = {
59
+ sources_processed: 0,
60
+ files_processed: 0,
61
+ files_skipped: 0,
62
+ hooks_inserted: 0,
63
+ hooks_updated: 0,
64
+ hooks_skipped: 0,
65
+ hooks_removed: 0,
66
+ blocks_indexed: 0,
67
+ apis_indexed: 0,
68
+ docs_inserted: 0,
69
+ docs_updated: 0,
70
+ docs_skipped: 0,
71
+ docs_removed: 0,
72
+ errors: [],
73
+ };
74
+
75
+ for (const source of sources) {
76
+ try {
77
+ console.error(`Fetching source: ${source.name} (${source.type})...`);
78
+ const localPath = await fetchSource(source);
79
+ console.error(`Indexing source: ${source.name} from ${localPath} (${source.content_type || 'source'})`);
80
+
81
+ if (source.content_type === 'docs') {
82
+ await indexDocsSource(source, localPath, force, stats);
83
+ } else {
84
+ await indexSource(source, localPath, force, stats);
85
+ }
86
+ stats.sources_processed++;
87
+ updateSourceLastIndexed(source.id);
88
+ } catch (err) {
89
+ const msg = `Error processing source "${source.name}": ${err.message}`;
90
+ console.error(msg);
91
+ stats.errors.push(msg);
92
+ }
93
+ }
94
+
95
+ return stats;
96
+ }
97
+
98
+ /**
99
+ * Index a single source — scans files, parses hooks/blocks/APIs, and upserts into the database.
100
+ * @param {object} source - Source row from the database
101
+ * @param {string} localPath - Absolute path to the source on disk
102
+ * @param {boolean} force - Skip mtime/hash caching when true
103
+ * @param {object} stats - Mutable stats object to accumulate counts
104
+ */
105
+ async function indexSource(source, localPath, force, stats) {
106
+ // Scan for PHP and JS/TS files
107
+ const files = await fg([...PHP_PATTERNS, ...JS_PATTERNS], {
108
+ cwd: localPath,
109
+ ignore: IGNORE_PATTERNS,
110
+ absolute: false,
111
+ onlyFiles: true,
112
+ });
113
+
114
+ console.error(`Found ${files.length} files to check in ${source.name}`);
115
+
116
+ for (const file of files) {
117
+ const fullPath = join(localPath, file);
118
+
119
+ try {
120
+ const fileStat = statSync(fullPath);
121
+ const mtimeMs = fileStat.mtimeMs;
122
+
123
+ // Check mtime for incremental skip
124
+ const indexed = !force ? getIndexedFile(source.id, file) : null;
125
+ if (indexed && indexed.mtime_ms === mtimeMs) {
126
+ stats.files_skipped++;
127
+ continue;
128
+ }
129
+
130
+ const content = readFileSync(fullPath, 'utf-8');
131
+ const contentHash = createHash('sha256').update(content).digest('hex').slice(0, 16);
132
+
133
+ // Skip if content hash matches (handles moved files / touched timestamps)
134
+ if (indexed && indexed.content_hash === contentHash) {
135
+ upsertIndexedFile(source.id, file, mtimeMs, contentHash);
136
+ stats.files_skipped++;
137
+ continue;
138
+ }
139
+
140
+ const isPhp = file.endsWith('.php');
141
+ const activeHookIds = [];
142
+
143
+ if (isPhp) {
144
+ const hooks = parsePhpFile(content, file, source.id);
145
+ for (const hook of hooks) {
146
+ const result = upsertHook(hook);
147
+ activeHookIds.push(result.id);
148
+ if (result.action === 'inserted') stats.hooks_inserted++;
149
+ else if (result.action === 'updated') stats.hooks_updated++;
150
+ else stats.hooks_skipped++;
151
+ }
152
+ } else {
153
+ const { hooks, blocks, apis } = parseJsFile(content, file, source.id);
154
+
155
+ for (const hook of hooks) {
156
+ const result = upsertHook(hook);
157
+ activeHookIds.push(result.id);
158
+ if (result.action === 'inserted') stats.hooks_inserted++;
159
+ else if (result.action === 'updated') stats.hooks_updated++;
160
+ else stats.hooks_skipped++;
161
+ }
162
+
163
+ for (const block of blocks) {
164
+ upsertBlockRegistration(block);
165
+ stats.blocks_indexed++;
166
+ }
167
+
168
+ for (const api of apis) {
169
+ upsertApiUsage(api);
170
+ stats.apis_indexed++;
171
+ }
172
+ }
173
+
174
+ // Soft-delete hooks that were in this file but no longer found
175
+ const removed = markHooksRemoved(source.id, file, activeHookIds);
176
+ stats.hooks_removed += removed;
177
+
178
+ // Track this file as indexed
179
+ upsertIndexedFile(source.id, file, mtimeMs, contentHash);
180
+ stats.files_processed++;
181
+ } catch (err) {
182
+ const msg = `Error indexing file ${file}: ${err.message}`;
183
+ console.error(msg);
184
+ stats.errors.push(msg);
185
+ }
186
+ }
187
+ }
@@ -0,0 +1,306 @@
1
+ import {
2
+ getLineNumber,
3
+ extractCodeWindow,
4
+ generateContentHash,
5
+ inferDescription,
6
+ extractDocblock,
7
+ findEnclosingFunction,
8
+ } from './parser-utils.js';
9
+
10
+ // JS hook patterns — addAction/addFilter from @wordpress/hooks or wp.hooks
11
+ const JS_HOOK_REGEX = /\b(?:addAction|addFilter|applyFilters|doAction)\s*\(\s*/g;
12
+
13
+ const JS_TYPE_MAP = {
14
+ addAction: 'js_action',
15
+ addFilter: 'js_filter',
16
+ applyFilters: 'js_filter',
17
+ doAction: 'js_action',
18
+ };
19
+
20
+ // Block registration patterns
21
+ const BLOCK_REG_REGEX = /\b(registerBlockType|registerBlockVariation|registerBlockStyle|registerBlockCollection)\s*\(\s*/g;
22
+
23
+ // WP API usage patterns
24
+ const API_USAGE_REGEX = /\bwp\.(blocks|editor|blockEditor|data|element|components|plugins|editPost|editSite|hooks|i18n|richText)\s*\.\s*(\w+)/g;
25
+
26
+ /**
27
+ * Parse a JS/TS file and extract hooks, block registrations, and API usages.
28
+ */
29
+ export function parseJsFile(content, filePath, sourceId) {
30
+ const hooks = [];
31
+ const blocks = [];
32
+ const apis = [];
33
+ const lines = content.split('\n');
34
+
35
+ // --- JS Hooks ---
36
+ let match;
37
+ JS_HOOK_REGEX.lastIndex = 0;
38
+ while ((match = JS_HOOK_REGEX.exec(content)) !== null) {
39
+ const funcName = match[0].match(/\b(addAction|addFilter|applyFilters|doAction)/)[1];
40
+ const type = JS_TYPE_MAP[funcName];
41
+ const startOffset = match.index + match[0].length;
42
+
43
+ const argsStr = extractJsArguments(content, startOffset);
44
+ if (!argsStr) continue;
45
+
46
+ const args = splitJsArguments(argsStr);
47
+ if (args.length === 0) continue;
48
+
49
+ const rawName = args[0].trim();
50
+ const hookName = cleanJsHookName(rawName);
51
+ if (!hookName) continue;
52
+
53
+ const isDynamic = rawName.includes('`') || rawName.includes('${') || rawName.includes('+');
54
+
55
+ const lineNumber = getLineNumber(content, match.index);
56
+ const lineIndex = lineNumber - 1;
57
+
58
+ const params = args.slice(1).map(p => p.trim()).filter(Boolean);
59
+ const docblock = extractDocblock(lines, lineIndex);
60
+ const { codeBefore, hookLine, codeAfter } = extractCodeWindow(lines, lineIndex);
61
+ const jsFunction = findEnclosingFunction(lines, lineIndex);
62
+
63
+ const hookData = {
64
+ source_id: sourceId,
65
+ file_path: filePath,
66
+ line_number: lineNumber,
67
+ name: hookName,
68
+ type,
69
+ php_function: jsFunction || null,
70
+ params: params.join(', ') || null,
71
+ param_count: params.length,
72
+ docblock: docblock || null,
73
+ inferred_description: null,
74
+ function_context: jsFunction || null,
75
+ class_name: null,
76
+ code_before: codeBefore || null,
77
+ code_after: codeAfter || null,
78
+ hook_line: hookLine || null,
79
+ is_dynamic: isDynamic ? 1 : 0,
80
+ content_hash: null,
81
+ };
82
+
83
+ hookData.inferred_description = inferDescription(hookData);
84
+ hookData.content_hash = generateContentHash(hookData);
85
+ hooks.push(hookData);
86
+ }
87
+
88
+ // --- Block Registrations ---
89
+ BLOCK_REG_REGEX.lastIndex = 0;
90
+ while ((match = BLOCK_REG_REGEX.exec(content)) !== null) {
91
+ const regFunc = match[1];
92
+ const startOffset = match.index + match[0].length;
93
+
94
+ const argsStr = extractJsArguments(content, startOffset);
95
+ if (!argsStr) continue;
96
+
97
+ const args = splitJsArguments(argsStr);
98
+ if (args.length === 0) continue;
99
+
100
+ const blockName = cleanJsHookName(args[0].trim());
101
+ if (!blockName) continue;
102
+
103
+ const lineNumber = getLineNumber(content, match.index);
104
+ const lineIndex = lineNumber - 1;
105
+ const { codeBefore, hookLine, codeAfter } = extractCodeWindow(lines, lineIndex, 4, 20);
106
+
107
+ // Try to extract settings object
108
+ const settingsStr = args.length > 1 ? args.slice(1).join(', ') : '';
109
+ const blockTitle = extractProperty(settingsStr, 'title');
110
+ const blockCategory = extractProperty(settingsStr, 'category');
111
+
112
+ const codeContext = [codeBefore, hookLine, codeAfter].filter(Boolean).join('\n');
113
+
114
+ const blockData = {
115
+ source_id: sourceId,
116
+ file_path: filePath,
117
+ line_number: lineNumber,
118
+ block_name: blockName,
119
+ block_title: blockTitle || null,
120
+ block_category: blockCategory || null,
121
+ block_attributes: null,
122
+ supports: null,
123
+ code_context: codeContext.slice(0, 2000) || null,
124
+ content_hash: null,
125
+ };
126
+
127
+ blockData.content_hash = generateContentHash({
128
+ name: blockName,
129
+ type: regFunc,
130
+ params: settingsStr,
131
+ docblock: '',
132
+ hookLine: hookLine,
133
+ });
134
+
135
+ blocks.push(blockData);
136
+ }
137
+
138
+ // --- API Usages ---
139
+ API_USAGE_REGEX.lastIndex = 0;
140
+ while ((match = API_USAGE_REGEX.exec(content)) !== null) {
141
+ const namespace = match[1];
142
+ const method = match[2];
143
+ const apiCall = `wp.${namespace}.${method}`;
144
+
145
+ const lineNumber = getLineNumber(content, match.index);
146
+ const lineIndex = lineNumber - 1;
147
+ const { codeBefore, hookLine, codeAfter } = extractCodeWindow(lines, lineIndex, 3, 3);
148
+ const codeContext = [codeBefore, hookLine, codeAfter].filter(Boolean).join('\n');
149
+
150
+ const apiData = {
151
+ source_id: sourceId,
152
+ file_path: filePath,
153
+ line_number: lineNumber,
154
+ api_call: apiCall,
155
+ namespace,
156
+ method,
157
+ code_context: codeContext.slice(0, 2000) || null,
158
+ content_hash: null,
159
+ };
160
+
161
+ apiData.content_hash = generateContentHash({
162
+ name: apiCall,
163
+ type: 'api_usage',
164
+ params: '',
165
+ docblock: '',
166
+ hookLine: hookLine,
167
+ });
168
+
169
+ apis.push(apiData);
170
+ }
171
+
172
+ return { hooks, blocks, apis };
173
+ }
174
+
175
+ /**
176
+ * Extract arguments string (handles nested parens, brackets, braces, strings, template literals).
177
+ */
178
+ function extractJsArguments(content, start) {
179
+ let depth = 1;
180
+ let i = start;
181
+ const max = Math.min(start + 5000, content.length);
182
+
183
+ while (i < max && depth > 0) {
184
+ const ch = content[i];
185
+ if (ch === '(') depth++;
186
+ else if (ch === ')') depth--;
187
+ else if (ch === "'" || ch === '"' || ch === '`') {
188
+ i = skipJsString(content, i);
189
+ } else if (ch === '/' && i + 1 < max && content[i + 1] === '/') {
190
+ // Line comment — skip to end of line
191
+ while (i < max && content[i] !== '\n') i++;
192
+ } else if (ch === '/' && i + 1 < max && content[i + 1] === '*') {
193
+ // Block comment
194
+ i += 2;
195
+ while (i < max && !(content[i] === '*' && content[i + 1] === '/')) i++;
196
+ i++; // skip past /
197
+ }
198
+ if (depth > 0) i++;
199
+ }
200
+
201
+ if (depth !== 0) return null;
202
+ return content.slice(start, i);
203
+ }
204
+
205
+ function skipJsString(content, i) {
206
+ const quote = content[i];
207
+ i++;
208
+ while (i < content.length) {
209
+ if (content[i] === '\\') {
210
+ i += 2;
211
+ continue;
212
+ }
213
+ if (quote === '`' && content[i] === '$' && content[i + 1] === '{') {
214
+ // Template literal expression — skip nested
215
+ i += 2;
216
+ let depth = 1;
217
+ while (i < content.length && depth > 0) {
218
+ if (content[i] === '{') depth++;
219
+ else if (content[i] === '}') depth--;
220
+ if (depth > 0) i++;
221
+ }
222
+ }
223
+ if (content[i] === quote) return i;
224
+ i++;
225
+ }
226
+ return i;
227
+ }
228
+
229
+ function splitJsArguments(argsStr) {
230
+ const args = [];
231
+ let current = '';
232
+ let depth = 0;
233
+ let inString = false;
234
+ let stringChar = '';
235
+
236
+ for (let i = 0; i < argsStr.length; i++) {
237
+ const ch = argsStr[i];
238
+
239
+ if (inString) {
240
+ current += ch;
241
+ if (ch === '\\') {
242
+ i++;
243
+ if (i < argsStr.length) current += argsStr[i];
244
+ continue;
245
+ }
246
+ if (ch === stringChar) inString = false;
247
+ continue;
248
+ }
249
+
250
+ if (ch === "'" || ch === '"' || ch === '`') {
251
+ inString = true;
252
+ stringChar = ch;
253
+ current += ch;
254
+ } else if (ch === '(' || ch === '[' || ch === '{') {
255
+ depth++;
256
+ current += ch;
257
+ } else if (ch === ')' || ch === ']' || ch === '}') {
258
+ depth--;
259
+ current += ch;
260
+ } else if (ch === ',' && depth === 0) {
261
+ args.push(current);
262
+ current = '';
263
+ } else {
264
+ current += ch;
265
+ }
266
+ }
267
+
268
+ if (current.trim()) args.push(current);
269
+ return args;
270
+ }
271
+
272
+ function cleanJsHookName(raw) {
273
+ const trimmed = raw.trim();
274
+
275
+ // Simple quoted string
276
+ const simpleMatch = trimmed.match(/^['"`]([^'"`]+)['"`]$/);
277
+ if (simpleMatch) return simpleMatch[1];
278
+
279
+ // Template literal without expressions
280
+ const templateMatch = trimmed.match(/^`([^$`]+)`$/);
281
+ if (templateMatch) return templateMatch[1];
282
+
283
+ // Template literal with expressions
284
+ if (trimmed.startsWith('`')) {
285
+ return trimmed.replace(/`/g, '').replace(/\$\{[^}]+\}/g, '{dynamic}') || null;
286
+ }
287
+
288
+ // String concatenation
289
+ if (trimmed.includes('+')) {
290
+ const parts = trimmed.split(/\s*\+\s*/);
291
+ const cleaned = parts.map(part => {
292
+ const qm = part.trim().match(/^['"`]([^'"`]*)['"`]$/);
293
+ if (qm) return qm[1];
294
+ return '{dynamic}';
295
+ }).join('');
296
+ return cleaned || null;
297
+ }
298
+
299
+ return null;
300
+ }
301
+
302
+ function extractProperty(objStr, prop) {
303
+ const regex = new RegExp(`(?:['"]?${prop}['"]?)\\s*:\\s*['"\`]([^'"\`]+)['"\`]`);
304
+ const match = objStr.match(regex);
305
+ return match ? match[1] : null;
306
+ }
@@ -0,0 +1,158 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ /**
4
+ * Get 1-based line number for a character offset in content.
5
+ */
6
+ export function getLineNumber(content, offset) {
7
+ let line = 1;
8
+ for (let i = 0; i < offset && i < content.length; i++) {
9
+ if (content[i] === '\n') line++;
10
+ }
11
+ return line;
12
+ }
13
+
14
+ /**
15
+ * Extract a code window around a given line.
16
+ * Returns { codeBefore, codeAfter, hookLine }.
17
+ */
18
+ export function extractCodeWindow(lines, lineIndex, before = 8, after = 4) {
19
+ const start = Math.max(0, lineIndex - before);
20
+ const end = Math.min(lines.length - 1, lineIndex + after);
21
+
22
+ return {
23
+ codeBefore: lines.slice(start, lineIndex).join('\n'),
24
+ hookLine: lines[lineIndex] || '',
25
+ codeAfter: lines.slice(lineIndex + 1, end + 1).join('\n'),
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Generate a content hash from the relevant parts of a hook for change detection.
31
+ */
32
+ export function generateContentHash(data) {
33
+ const hash = createHash('sha256');
34
+ hash.update(JSON.stringify({
35
+ name: data.name,
36
+ type: data.type,
37
+ params: data.params,
38
+ docblock: data.docblock,
39
+ hookLine: data.hookLine,
40
+ }));
41
+ return hash.digest('hex').slice(0, 16);
42
+ }
43
+
44
+ /**
45
+ * Infer a human-readable description from hook parts.
46
+ */
47
+ export function inferDescription(data) {
48
+ const parts = [];
49
+
50
+ const typeLabel = {
51
+ action: 'Action hook',
52
+ filter: 'Filter hook',
53
+ action_ref_array: 'Action hook (ref array)',
54
+ filter_ref_array: 'Filter hook (ref array)',
55
+ js_action: 'JavaScript action hook',
56
+ js_filter: 'JavaScript filter hook',
57
+ }[data.type] || 'Hook';
58
+
59
+ parts.push(typeLabel);
60
+
61
+ if (data.is_dynamic) {
62
+ parts.push('(dynamic name)');
63
+ }
64
+
65
+ parts.push(`"${data.name}"`);
66
+
67
+ if (data.php_function) {
68
+ parts.push(`in ${data.php_function}()`);
69
+ }
70
+ if (data.class_name) {
71
+ parts.push(`of class ${data.class_name}`);
72
+ }
73
+
74
+ if (data.param_count > 0) {
75
+ parts.push(`with ${data.param_count} parameter${data.param_count > 1 ? 's' : ''}`);
76
+ }
77
+
78
+ return parts.join(' ');
79
+ }
80
+
81
+ /**
82
+ * Extract docblock from lines above a given line index.
83
+ * Looks up to `maxLines` lines above for a closing doc comment.
84
+ */
85
+ export function extractDocblock(lines, lineIndex, maxLines = 5) {
86
+ const docLines = [];
87
+ let foundEnd = false;
88
+
89
+ for (let i = lineIndex - 1; i >= Math.max(0, lineIndex - maxLines); i--) {
90
+ const line = lines[i].trim();
91
+
92
+ if (!foundEnd) {
93
+ // Look for closing */ or a @-annotated line or * continuation
94
+ if (line.endsWith('*/') || line.startsWith('*') || line.startsWith('/**')) {
95
+ foundEnd = true;
96
+ docLines.unshift(lines[i]);
97
+ } else if (line === '' || line.startsWith('//')) {
98
+ continue;
99
+ } else {
100
+ break;
101
+ }
102
+ } else {
103
+ docLines.unshift(lines[i]);
104
+ if (line.startsWith('/**') || line.startsWith('/*')) {
105
+ break;
106
+ }
107
+ }
108
+ }
109
+
110
+ if (docLines.length === 0) return '';
111
+ return docLines.join('\n').trim();
112
+ }
113
+
114
+ /**
115
+ * Find the enclosing function/method name for a given line index by scanning upward.
116
+ */
117
+ export function findEnclosingFunction(lines, lineIndex) {
118
+ // PHP function pattern
119
+ const phpFuncRe = /(?:public|protected|private|static|\s)*function\s+(\w+)\s*\(/;
120
+ // JS function patterns
121
+ const jsFuncRe = /(?:(?:async\s+)?function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:function|\(|=>))/;
122
+
123
+ let braceDepth = 0;
124
+ for (let i = lineIndex; i >= 0; i--) {
125
+ const line = lines[i];
126
+
127
+ // Count braces to track nesting
128
+ for (let c = line.length - 1; c >= 0; c--) {
129
+ if (line[c] === '}') braceDepth++;
130
+ if (line[c] === '{') braceDepth--;
131
+ }
132
+
133
+ const phpMatch = line.match(phpFuncRe);
134
+ if (phpMatch && braceDepth <= 0) return phpMatch[1];
135
+
136
+ const jsMatch = line.match(jsFuncRe);
137
+ if (jsMatch && braceDepth <= 0) return jsMatch[1] || jsMatch[2];
138
+ }
139
+ return null;
140
+ }
141
+
142
+ /**
143
+ * Find the enclosing class name for a given line index by scanning upward.
144
+ */
145
+ export function findEnclosingClass(lines, lineIndex) {
146
+ const classRe = /(?:abstract\s+)?class\s+(\w+)/;
147
+ let braceDepth = 0;
148
+ for (let i = lineIndex; i >= 0; i--) {
149
+ const line = lines[i];
150
+ for (let c = line.length - 1; c >= 0; c--) {
151
+ if (line[c] === '}') braceDepth++;
152
+ if (line[c] === '{') braceDepth--;
153
+ }
154
+ const match = line.match(classRe);
155
+ if (match && braceDepth <= 0) return match[1];
156
+ }
157
+ return null;
158
+ }