magector 2.13.0 → 2.13.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.
Files changed (2) hide show
  1. package/package.json +5 -5
  2. package/src/mcp-server.js +59 -31
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magector",
3
- "version": "2.13.0",
3
+ "version": "2.13.1",
4
4
  "description": "Semantic code search for Magento 2 — index, search, MCP server",
5
5
  "type": "module",
6
6
  "main": "src/mcp-server.js",
@@ -33,10 +33,10 @@
33
33
  "ruvector": "^0.1.96"
34
34
  },
35
35
  "optionalDependencies": {
36
- "@magector/cli-darwin-arm64": "2.13.0",
37
- "@magector/cli-linux-x64": "2.13.0",
38
- "@magector/cli-linux-arm64": "2.13.0",
39
- "@magector/cli-win32-x64": "2.13.0"
36
+ "@magector/cli-darwin-arm64": "2.13.1",
37
+ "@magector/cli-linux-x64": "2.13.1",
38
+ "@magector/cli-linux-arm64": "2.13.1",
39
+ "@magector/cli-win32-x64": "2.13.1"
40
40
  },
41
41
  "keywords": [
42
42
  "magento",
package/src/mcp-server.js CHANGED
@@ -3439,9 +3439,11 @@ const ENRICHMENT_DB_PATH = (root) => path.join(root, '.magector', 'enrichment.db
3439
3439
  function hasNullGuard(lines, matchLineIdx, receiverExpr, guardRadius = 6) {
3440
3440
  const start = Math.max(0, matchLineIdx - guardRadius);
3441
3441
  const end = Math.min(lines.length - 1, matchLineIdx + guardRadius);
3442
+ const matchLine = lines[matchLineIdx] || '';
3442
3443
  const window = lines.slice(start, end + 1).join('\n');
3443
3444
 
3444
- if (window.includes('?->')) return true;
3445
+ // ?-> only counts if it's on the same line as the chain (avoid false positives from unrelated variables)
3446
+ if (matchLine.includes('?->')) return true;
3445
3447
  if (/\?\?|\?:/.test(window)) return true;
3446
3448
 
3447
3449
  if (receiverExpr) {
@@ -3455,7 +3457,7 @@ function hasNullGuard(lines, matchLineIdx, receiverExpr, guardRadius = 6) {
3455
3457
  * Scan vendor/ PHP files for ->first()->second() chains and store null-safety
3456
3458
  * analysis in enrichment.db. Called by magento_enrich and after magento_index.
3457
3459
  */
3458
- async function enrichMethodChains(root, options = {}) {
3460
+ async function enrichMethodChains(root) {
3459
3461
  const dbPath = ENRICHMENT_DB_PATH(root);
3460
3462
 
3461
3463
  // Use node:sqlite (built-in, no deps)
@@ -3497,36 +3499,63 @@ async function enrichMethodChains(root, options = {}) {
3497
3499
  );
3498
3500
  const deleteFile = db.prepare('DELETE FROM method_chains WHERE file = ?');
3499
3501
 
3500
- // Process files in batches for memory efficiency
3501
- for (const phpFile of phpFiles) {
3502
- let content;
3503
- try { content = readFileSync(phpFile, 'utf-8'); } catch { continue; }
3504
- if (!content.includes('->')) continue;
3505
-
3506
- const relPath = phpFile.replace(root + '/', '');
3507
- const lines = content.split('\n');
3508
- const rows = [];
3502
+ // Build line-offset index for O(1) line number lookups
3503
+ function buildLineIndex(content) {
3504
+ const offsets = [0];
3505
+ let idx = 0;
3506
+ while ((idx = content.indexOf('\n', idx)) !== -1) {
3507
+ idx++;
3508
+ offsets.push(idx);
3509
+ }
3510
+ return offsets;
3511
+ }
3509
3512
 
3510
- chainRegex.lastIndex = 0;
3511
- let m;
3512
- while ((m = chainRegex.exec(content)) !== null) {
3513
- const lineNum = content.slice(0, m.index).split('\n').length;
3514
- rows.push({
3515
- file: relPath, line: lineNum,
3516
- chain: `->${m[2]}()->${m[3]}()`,
3517
- firstMethod: m[2], secondMethod: m[3],
3518
- hasNullGuard: hasNullGuard(lines, lineNum - 1, m[1]) ? 1 : 0
3519
- });
3520
- chains++;
3513
+ function lineFromOffset(offsets, charIndex) {
3514
+ let lo = 0, hi = offsets.length - 1;
3515
+ while (lo < hi) {
3516
+ const mid = (lo + hi + 1) >> 1;
3517
+ if (offsets[mid] <= charIndex) lo = mid; else hi = mid - 1;
3521
3518
  }
3519
+ return lo + 1; // 1-based
3520
+ }
3521
+
3522
+ db.exec('BEGIN');
3523
+ try {
3524
+ for (const phpFile of phpFiles) {
3525
+ let content;
3526
+ try { content = readFileSync(phpFile, 'utf-8'); } catch { continue; }
3527
+ if (!content.includes('->')) continue;
3528
+
3529
+ const relPath = phpFile.replace(root + '/', '');
3530
+ const lines = content.split('\n');
3531
+ const lineOffsets = buildLineIndex(content);
3532
+ const rows = [];
3522
3533
 
3523
- if (rows.length > 0) {
3524
- deleteFile.run(relPath);
3525
- for (const r of rows) {
3526
- insertStmt.run(r.file, r.line, r.chain, r.firstMethod, r.secondMethod, r.hasNullGuard, now);
3534
+ chainRegex.lastIndex = 0;
3535
+ let m;
3536
+ while ((m = chainRegex.exec(content)) !== null) {
3537
+ const lineNum = lineFromOffset(lineOffsets, m.index);
3538
+ rows.push({
3539
+ file: relPath, line: lineNum,
3540
+ chain: `->${m[2]}()->${m[3]}()`,
3541
+ firstMethod: m[2], secondMethod: m[3],
3542
+ hasNullGuard: hasNullGuard(lines, lineNum - 1, m[1]) ? 1 : 0
3543
+ });
3544
+ chains++;
3527
3545
  }
3546
+
3547
+ if (rows.length > 0) {
3548
+ deleteFile.run(relPath);
3549
+ for (const r of rows) {
3550
+ insertStmt.run(r.file, r.line, r.chain, r.firstMethod, r.secondMethod, r.hasNullGuard, now);
3551
+ }
3552
+ }
3553
+ scanned++;
3528
3554
  }
3529
- scanned++;
3555
+ db.exec('COMMIT');
3556
+ } catch (err) {
3557
+ db.exec('ROLLBACK');
3558
+ throw err;
3530
3559
  }
3531
3560
 
3532
3561
  db.close();
@@ -4736,7 +4765,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4736
4765
  const root = args.path || config.magentoRoot;
4737
4766
  const output = rustIndex(root);
4738
4767
  // Auto-enrich after indexing: runs in background, doesn't block response
4739
- enrichMethodChains(root, { verbose: true }).then(({ scanned, chains }) => {
4768
+ enrichMethodChains(root).then(({ scanned, chains }) => {
4740
4769
  logToFile('INFO', `Auto-enrich complete: ${scanned} files, ${chains} chains`);
4741
4770
  }).catch(err => {
4742
4771
  logToFile('WARN', `Auto-enrich failed: ${err.message}`);
@@ -6342,8 +6371,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
6342
6371
  const maxRes = Math.min(a.maxResults || 30, 100);
6343
6372
  const batchCtx = a.context !== undefined ? a.context : 4;
6344
6373
  const batchFilesOnly = a.filesOnly || false;
6345
- const gArgs = ['-rn', '-E'];
6346
- if (batchFilesOnly) { gArgs[0] = '-rl'; gArgs.splice(1, 1); } // -rl = recursive + files-only, drop -n
6374
+ const gArgs = batchFilesOnly ? ['-rl', '-E'] : ['-rn', '-E'];
6347
6375
  if (a.ignoreCase) gArgs.push('-i');
6348
6376
  if (!batchFilesOnly && batchCtx > 0) gArgs.push('-C', String(batchCtx));
6349
6377
  for (const pat of include.split(',').map(p => p.trim())) gArgs.push('--include=' + pat);
@@ -6647,7 +6675,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
6647
6675
  if (!root) return { content: [{ type: 'text', text: 'MAGENTO_ROOT not set.' }], isError: true };
6648
6676
  let text = `## magento_enrich\n\nScanning vendor/ PHP files for method chains...\n`;
6649
6677
  try {
6650
- const { scanned, chains } = await enrichMethodChains(root, { verbose: true });
6678
+ const { scanned, chains } = await enrichMethodChains(root);
6651
6679
  text += `\n✅ **Done**\n- Files scanned: ${scanned}\n- Method chains indexed: ${chains}\n- Null-risk index saved to: \`.magector/enrichment.db\`\n\nUse \`magento_find_null_risks\` to query unsafe chains.`;
6652
6680
  } catch (err) {
6653
6681
  text += `\n❌ Error: ${err.message}`;