rip-lang 3.14.0 → 3.14.2

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.
package/src/typecheck.js CHANGED
@@ -15,7 +15,6 @@ import { INTRINSIC_TYPE_DECLS, INTRINSIC_FN_DECL, ARIA_TYPE_DECLS, SIGNAL_INTERF
15
15
  import { hasSchemas } from './schema.js';
16
16
  import { createRequire } from 'module';
17
17
  import { readFileSync, existsSync, readdirSync } from 'fs';
18
- import { mapToSourcePos, offsetToLine, getLineText, findNearestWord, lineColToOffset, offsetToLineCol, adjustSwitchDiagnostic, isInjectedOverload, srcToOffset } from './sourcemap-utils.js';
19
18
  import { resolve, relative, dirname } from 'path';
20
19
  import { buildLineMap } from './sourcemaps.js';
21
20
 
@@ -1299,9 +1298,539 @@ export function compileForCheck(filePath, source, compiler, opts = {}) {
1299
1298
  return { tsContent, headerLines, hasTypes, srcToGen, genToSrc, srcColToGen, source, dts };
1300
1299
  }
1301
1300
 
1302
- // ── Source mapping helpers (delegated to sourcemap-utils.js) ───────
1301
+ // ── Source-position mapping helpers ────────────────────────────────
1302
+ //
1303
+ // These utilities turn a TypeScript-virtual-file offset (the TS language
1304
+ // service hands us one per diagnostic) back into a Rip `{line, col}`
1305
+ // position the user can navigate to. The virtual file has a compiler-
1306
+ // injected header (declarations, intrinsics) plus the compiled body;
1307
+ // each entry carries its own `genToSrc` / `srcToGen` / `srcColToGen`
1308
+ // maps (built by buildLineMap in sourcemaps.js) plus the raw tsContent
1309
+ // and original Rip source.
1310
+ //
1311
+ // Most of the complexity below is Rip-specific heuristics for cases
1312
+ // where a single generated line maps to multiple source lines, or
1313
+ // where the compiler injects code that has no direct source (hoisted
1314
+ // `let` aggregates, overload signatures, switch-IIFE wrappers, etc.).
1315
+
1316
+ // When a switch expression is the implicit return of a function, the compiler
1317
+ // wraps it in an IIFE: `return (() => { switch ... })()`. Non-exhaustive
1318
+ // switches produce TS2322 on the IIFE return, which source-maps to the `switch`
1319
+ // line. This helper detects that pattern and remaps the diagnostic to the
1320
+ // function's return-type annotation (matching TS behaviour on the raw .ts).
1321
+ // Returns { line, col, len } if remapped, or null if no adjustment needed.
1322
+ export function adjustSwitchDiagnostic(source, pos, code) {
1323
+ if (code !== 2322) return null;
1324
+ const srcLines = source.split('\n');
1325
+ const line = srcLines[pos.line] || '';
1326
+ if (!/^\s*switch\b/.test(line)) return null;
1327
+
1328
+ const switchIndent = line.match(/^(\s*)/)[1].length;
1329
+ for (let i = pos.line - 1; i >= 0; i--) {
1330
+ const defLine = srcLines[i];
1331
+ const defMatch = defLine.match(/^(\s*)def\b/);
1332
+ if (defMatch && defMatch[1].length < switchIndent) {
1333
+ // Found enclosing function — look for return-type annotation "):: Type"
1334
+ const retMatch = defLine.match(/\)\s*::\s*(\w+)\s*$/);
1335
+ if (retMatch) {
1336
+ const typeStart = defLine.lastIndexOf(retMatch[1]);
1337
+ return { line: i, col: typeStart, len: retMatch[1].length };
1338
+ }
1339
+ return { line: i, col: defMatch[1].length, len: 3 }; // fallback: highlight `def`
1340
+ }
1341
+ // Stop if we leave the indentation context
1342
+ if (/\S/.test(defLine) && !defLine.match(/^(\s*)/)[1].length && !/^\s*#/.test(defLine)) break;
1343
+ }
1344
+ return null;
1345
+ }
1346
+
1347
+ export function getLineText(text, lineNum) {
1348
+ let start = 0, line = 0;
1349
+ for (let i = 0; i <= text.length; i++) {
1350
+ if (i === text.length || text[i] === '\n') {
1351
+ if (line === lineNum) return text.slice(start, i);
1352
+ start = i + 1;
1353
+ line++;
1354
+ }
1355
+ }
1356
+ return '';
1357
+ }
1358
+
1359
+ export function findNearestWord(text, word, approx) {
1360
+ let bestIdx = -1, bestDist = Infinity, idx = 0;
1361
+ while ((idx = text.indexOf(word, idx)) >= 0) {
1362
+ const before = idx === 0 || /\W/.test(text[idx - 1]);
1363
+ const after = idx + word.length >= text.length || /\W/.test(text[idx + word.length]);
1364
+ if (before && after) {
1365
+ const dist = Math.abs(idx - approx);
1366
+ if (dist < bestDist) { bestDist = dist; bestIdx = idx; }
1367
+ }
1368
+ idx++;
1369
+ }
1370
+ return bestIdx;
1371
+ }
1372
+
1373
+ // Check whether an offset falls on an injected function overload signature line
1374
+ // (generated by compileForCheck, not from user code). These are body lines that
1375
+ // match `function NAME(...): TYPE;` and have no genToSrc entry.
1376
+ export function isInjectedOverload(entry, offset) {
1377
+ const tsLine = offsetToLine(entry.tsContent, offset);
1378
+ if (tsLine < entry.headerLines) return false;
1379
+ if (entry.genToSrc.get(tsLine) !== undefined) return false;
1380
+ const lineText = getLineText(entry.tsContent, tsLine);
1381
+ return /^(?:async\s+)?function\s+\w+\s*\(/.test(lineText) && lineText.trimEnd().endsWith(';');
1382
+ }
1383
+
1384
+ export function offsetToLine(text, offset) {
1385
+ let line = 0;
1386
+ for (let i = 0; i < offset && i < text.length; i++) {
1387
+ if (text[i] === '\n') line++;
1388
+ }
1389
+ return line;
1390
+ }
1391
+
1392
+ export function lineColToOffset(text, line, col) {
1393
+ let r = 0;
1394
+ for (let i = 0; i < text.length; i++) {
1395
+ if (r === line) return i + col;
1396
+ if (text[i] === '\n') r++;
1397
+ }
1398
+ return text.length;
1399
+ }
1400
+
1401
+ export function offsetToLineCol(text, offset) {
1402
+ let line = 0, ls = 0;
1403
+ for (let i = 0; i < offset && i < text.length; i++) {
1404
+ if (text[i] === '\n') { line++; ls = i + 1; }
1405
+ }
1406
+ return { line, col: offset - ls };
1407
+ }
1408
+
1409
+ // Map a TypeScript offset back to a Rip source { line, col } (0-based).
1410
+ // Returns null if the offset falls in the DTS header (and no match is found).
1411
+ //
1412
+ // `entry` must have: tsContent, headerLines, genToSrc, source, srcColToGen (optional)
1413
+ export function mapToSourcePos(entry, offset) {
1414
+ const tsLine = offsetToLine(entry.tsContent, offset);
1415
+ if (tsLine < entry.headerLines) {
1416
+ // DTS preamble — find the identifier at the offset and locate it in the source
1417
+ const genLineText = getLineText(entry.tsContent, tsLine);
1418
+
1419
+ // Skip compiler-injected stdlib declarations (declare function warn, etc.)
1420
+ // — diagnostics on these lines are never user-authored and would incorrectly
1421
+ // match string literals or identifiers in the source.
1422
+ if (/^declare\s+function\s/.test(genLineText)) return null;
1423
+
1424
+ // If genToSrc has a mapping for this header line (e.g. imports, declarations),
1425
+ // use it to target the correct source line for word matching.
1426
+ const mappedSrcLine = entry.genToSrc.get(tsLine);
1427
+
1428
+ let lineStart = 0, curLine = 0;
1429
+ for (let i = 0; i < entry.tsContent.length; i++) {
1430
+ if (curLine === tsLine) { lineStart = i; break; }
1431
+ if (entry.tsContent[i] === '\n') curLine++;
1432
+ }
1433
+ const genCol = offset - lineStart;
1434
+ const wordMatch = genLineText.slice(genCol).match(/^\w+/);
1435
+ if (wordMatch && entry.source) {
1436
+ const word = wordMatch[0];
1437
+ const srcLines = entry.source.split('\n');
1438
+ const re = new RegExp('\\b' + word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b');
1439
+
1440
+ // If we have a direct line mapping, try that source line first
1441
+ if (mappedSrcLine !== undefined) {
1442
+ const m = re.exec(srcLines[mappedSrcLine]);
1443
+ if (m) return { line: mappedSrcLine, col: m.index };
1444
+ return { line: mappedSrcLine, col: genCol };
1445
+ }
1446
+
1447
+ // For let/var declarations, the error word may appear on many source lines
1448
+ // (e.g. `Status` referenced in multiple variable annotations). Narrow the
1449
+ // search to the source line that declares the same variable.
1450
+ const letMatch = genLineText.match(/^(?:export\s+)?(?:declare\s+)?(?:let|var)\s+(\w+)/);
1451
+ if (letMatch) {
1452
+ const varName = letMatch[1];
1453
+ const varRe = new RegExp('\\b' + varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*::');
1454
+ for (let s = 0; s < srcLines.length; s++) {
1455
+ if (varRe.test(srcLines[s])) {
1456
+ const m = re.exec(srcLines[s]);
1457
+ if (m) return { line: s, col: m.index };
1458
+ return { line: s, col: 0 };
1459
+ }
1460
+ }
1461
+ }
1462
+
1463
+ // Find enclosing type/interface from DTS context to narrow search —
1464
+ // without this, duplicate member names (e.g. "host" in two types) always
1465
+ // resolve to the first occurrence in the source.
1466
+ let searchStart = 0;
1467
+ for (let t = tsLine; t >= 0; t--) {
1468
+ const tl = getLineText(entry.tsContent, t);
1469
+ const tm = tl.match(/^(?:type|interface)\s+(\w+)/);
1470
+ if (tm) {
1471
+ const typeRe = new RegExp('(?:type|interface)\\s+' + tm[1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b');
1472
+ for (let s = 0; s < srcLines.length; s++) {
1473
+ if (typeRe.test(srcLines[s])) { searchStart = s; break; }
1474
+ }
1475
+ break;
1476
+ }
1477
+ if (/^\}/.test(tl.trim())) break; // exited a type block — not inside one
1478
+ }
1479
+
1480
+ for (let s = searchStart; s < srcLines.length; s++) {
1481
+ const m = re.exec(srcLines[s]);
1482
+ if (m) return { line: s, col: m.index };
1483
+ }
1484
+ }
1485
+ return null;
1486
+ }
1487
+
1488
+ // Hoisted multi-variable `let` declaration (e.g. `let a, b, items, ...;`) —
1489
+ // the compiler aggregates variable declarations into one line with no useful
1490
+ // per-variable source mapping. Detect the pattern (both top-level and inside
1491
+ // functions), extract the word at the offset, and find its assignment in
1492
+ // the Rip source. Use the genToSrc mapping of the preceding TS line (the
1493
+ // function declaration) to scope the search and avoid matching a same-named
1494
+ // variable in a different function.
1495
+ const hoistLine = getLineText(entry.tsContent, tsLine);
1496
+ if (/^\s*let\s+[$\w]+\s*,/.test(hoistLine) && entry.source) {
1497
+ let hl = 0;
1498
+ for (let i = 0; i < entry.tsContent.length; i++) {
1499
+ if (hl === tsLine) { hl = i; break; }
1500
+ if (entry.tsContent[i] === '\n') hl++;
1501
+ }
1502
+ const hCol = offset - hl;
1503
+ const hWord = hoistLine.slice(hCol).match(/^[$\w]+/);
1504
+ if (hWord) {
1505
+ const word = hWord[0];
1506
+ const srcLines = entry.source.split('\n');
1507
+ const assignRe = new RegExp('^' + word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*(?:::|=!|:=|~=|=)');
1508
+
1509
+ // Scope the search: find the source line of the enclosing function by
1510
+ // checking genToSrc for the TS line just before the hoisted let.
1511
+ let searchStart = 0;
1512
+ if (entry.genToSrc) {
1513
+ for (let g = tsLine - 1; g >= 0; g--) {
1514
+ const s = entry.genToSrc.get(g);
1515
+ if (s !== undefined) { searchStart = s; break; }
1516
+ }
1517
+ }
1518
+
1519
+ for (let s = searchStart; s < srcLines.length; s++) {
1520
+ if (assignRe.test(srcLines[s].trimStart())) {
1521
+ const col = srcLines[s].indexOf(word);
1522
+ if (col >= 0) return { line: s, col };
1523
+ }
1524
+ }
1525
+ // Variable is on a hoisted let but has no recognisable assignment in
1526
+ // the source (e.g. for-loop iterators, destructured names). Return
1527
+ // null so callers skip it rather than producing a garbage mapping.
1528
+ return null;
1529
+ }
1530
+ }
1531
+
1532
+ // Injected function overload signatures (e.g. `function fetchUser(id: number): Promise<User>;`)
1533
+ // have no genToSrc entry. The backward-walk approximation below can map them to
1534
+ // wildly wrong source lines. Extract the function name and find its `def` in the source.
1535
+ const bodyLine = getLineText(entry.tsContent, tsLine);
1536
+ if (entry.genToSrc.get(tsLine) === undefined && entry.source) {
1537
+ const overloadMatch = bodyLine.match(/^(?:async\s+)?function\s+(\w+)\s*\(/);
1538
+ if (overloadMatch && bodyLine.trimEnd().endsWith(';')) {
1539
+ const fnName = overloadMatch[1];
1540
+ const srcLines = entry.source.split('\n');
1541
+ const defRe = new RegExp('\\bdef\\s+' + fnName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b');
1542
+ for (let s = 0; s < srcLines.length; s++) {
1543
+ if (defRe.test(srcLines[s])) {
1544
+ const col = srcLines[s].indexOf(fnName);
1545
+ return { line: s, col: col >= 0 ? col : 0 };
1546
+ }
1547
+ }
1548
+ }
1549
+ }
1550
+
1551
+ // Resolve source line from genToSrc
1552
+ let srcLine = entry.genToSrc.get(tsLine);
1553
+ if (srcLine === undefined) {
1554
+ // Walk backward to find nearest mapped gen line
1555
+ let best = -1;
1556
+ for (const [g] of entry.genToSrc) if (g <= tsLine && g > best) best = g;
1557
+ if (best >= 0) {
1558
+ srcLine = entry.genToSrc.get(best) + (tsLine - best);
1559
+ } else {
1560
+ srcLine = tsLine - entry.headerLines;
1561
+ }
1562
+ }
1563
+
1564
+ // Compute generated column
1565
+ let lineStart = 0, curLine = 0;
1566
+ for (let i = 0; i < entry.tsContent.length; i++) {
1567
+ if (curLine === tsLine) { lineStart = i; break; }
1568
+ if (entry.tsContent[i] === '\n') curLine++;
1569
+ }
1570
+ const genCol = offset - lineStart;
1571
+
1572
+ // Remap column via text matching
1573
+ const genText = getLineText(entry.tsContent, tsLine);
1574
+ let srcCol = genCol;
1575
+ let approx = genCol; // default: assume same column
1576
+ // Scan ALL source lines for mappings to this gen line — a multi-line Rip
1577
+ // expression (e.g. object literal) may compile to a single gen line, so
1578
+ // multiple source lines can share one gen line. Pick the closest genCol.
1579
+ // On ties, prefer the source line that genToSrc already identified (e.g.
1580
+ // from an @rip-src annotation) so that stub render-block expressions land
1581
+ // on their correct source lines instead of a sibling attribute line.
1582
+ if (entry.srcColToGen) {
1583
+ const origSrcLine = srcLine;
1584
+ let bestDist = Infinity;
1585
+ for (const [sl, entries] of entry.srcColToGen) {
1586
+ for (const e of entries) {
1587
+ if (e.genLine === tsLine) {
1588
+ const dist = Math.abs(e.genCol - genCol);
1589
+ if (dist < bestDist || (dist === bestDist && sl === origSrcLine)) {
1590
+ bestDist = dist;
1591
+ srcLine = sl;
1592
+ approx = e.srcCol + (genCol - e.genCol);
1593
+ }
1594
+ }
1595
+ }
1596
+ }
1597
+ }
1598
+ const srcText = entry.source ? getLineText(entry.source, srcLine) : '';
1599
+ // Text-match: find the word at genCol in the gen line, then locate it in the source line
1600
+ if (srcText) {
1601
+ let wordAt = genText.slice(genCol).match(/^\w+/);
1602
+ // Quoted string literal (e.g. __ripEl('tag')) — peek inside the quotes
1603
+ if (!wordAt && (genText[genCol] === "'" || genText[genCol] === '"')) {
1604
+ wordAt = genText.slice(genCol + 1).match(/^\w+/);
1605
+ }
1606
+ if (wordAt) {
1607
+ let word = wordAt[0];
1608
+ let idx = findNearestWord(srcText, word, approx);
1609
+ // __bind_xxx__ → xxx: two-way binding props use mangled names in gen
1610
+ if (idx < 0 && word.startsWith('__bind_') && word.endsWith('__')) {
1611
+ word = word.slice(7, -2);
1612
+ idx = findNearestWord(srcText, word, approx);
1613
+ }
1614
+ if (idx >= 0) return { line: srcLine, col: idx };
1615
+ }
1616
+ if (genCol > 0 && (!wordAt || genCol >= genText.length)) {
1617
+ let wordBefore = genText.slice(0, genCol).match(/(\w+)$/);
1618
+ // Closing quote — peek inside to find the word (e.g. end of __ripEl('tag'))
1619
+ if (!wordBefore && (genText[genCol - 1] === "'" || genText[genCol - 1] === '"')) {
1620
+ wordBefore = genText.slice(0, genCol - 1).match(/(\w+)$/);
1621
+ }
1622
+ if (wordBefore) {
1623
+ let word = wordBefore[0];
1624
+ let idx = findNearestWord(srcText, word, approx - word.length);
1625
+ // __bind_xxx__ → xxx: two-way binding props use mangled names in gen
1626
+ if (idx < 0 && word.startsWith('__bind_') && word.endsWith('__')) {
1627
+ word = word.slice(7, -2);
1628
+ idx = findNearestWord(srcText, word, approx - word.length);
1629
+ }
1630
+ if (idx >= 0) return { line: srcLine, col: idx + word.length };
1631
+ // Injected property access (e.g. clicks.value from clicks :=) — map to end of object identifier
1632
+ const dotMatch = genText.slice(0, genCol - wordBefore[0].length).match(/(\w+)\.$/);
1633
+ if (dotMatch) {
1634
+ const objIdx = findNearestWord(srcText, dotMatch[1], approx - wordBefore[0].length - dotMatch[1].length - 1);
1635
+ if (objIdx >= 0) return { line: srcLine, col: objIdx + dotMatch[1].length };
1636
+ }
1637
+ }
1638
+ }
1639
+ srcCol = Math.max(0, approx);
1640
+ }
1641
+
1642
+ // Word not found on mapped line (or line was empty) — search nearby lines
1643
+ // (handles cases where multiple source lines compress to one generated line,
1644
+ // e.g. constructor params, or srcLine is blank)
1645
+ {
1646
+ let wordFallback = genText.slice(genCol).match(/^\w+/);
1647
+ // Quoted string literal — peek inside the quotes (e.g. __RipProps<'inputz'>)
1648
+ if (!wordFallback && (genText[genCol] === "'" || genText[genCol] === '"')) {
1649
+ wordFallback = genText.slice(genCol + 1).match(/^\w+/);
1650
+ }
1651
+ if (wordFallback) {
1652
+ let word = wordFallback[0];
1653
+ if (word.startsWith('__bind_') && word.endsWith('__')) word = word.slice(7, -2);
1654
+ const srcLines = entry.source.split('\n');
1655
+ const re = new RegExp('\\b' + word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b');
1656
+ for (let delta = 0; delta <= 10; delta++) {
1657
+ for (const d of delta === 0 ? [srcLine] : [srcLine + delta, srcLine - delta]) {
1658
+ if (d >= 0 && d < srcLines.length) {
1659
+ const m = re.exec(srcLines[d]);
1660
+ if (m) return { line: d, col: m.index };
1661
+ }
1662
+ }
1663
+ }
1664
+ }
1665
+ }
1666
+
1667
+ // When text matching failed entirely (generated identifier like _2 doesn't
1668
+ // exist in source), srcCol may land on whitespace or past EOL. Fall back to
1669
+ // the first word on the source line so the diagnostic highlights something
1670
+ // meaningful (e.g. the component name on a `Button` line).
1671
+ if (srcText) {
1672
+ if (srcCol >= srcText.length || /^\s*$/.test(srcText.slice(srcCol, srcCol + 1))) {
1673
+ const firstWord = srcText.match(/^\s*(\w+)/);
1674
+ if (firstWord) return { line: srcLine, col: firstWord.index + firstWord[0].length - firstWord[1].length };
1675
+ }
1676
+ }
1677
+ return { line: srcLine, col: srcCol };
1678
+ }
1679
+
1680
+ // Map a Rip source (line, col) to a TypeScript virtual file byte offset.
1681
+ // This is the forward direction: source → generated (used for hover, definition, etc.)
1682
+ //
1683
+ // `entry` must have: tsContent, source, srcToGen, srcColToGen (optional)
1684
+ // Returns undefined if no mapping can be established.
1685
+ export function srcToOffset(entry, line, col) {
1686
+ if (!entry) return undefined;
1687
+ let genLine = entry.srcToGen.get(line);
1688
+ let genColHint = -1;
1689
+ let bestSrcCol = -1;
1690
+
1691
+ // Column-aware lookup
1692
+ if (entry.srcColToGen) {
1693
+ const colEntries = entry.srcColToGen.get(line);
1694
+ if (colEntries && colEntries.length > 0) {
1695
+ let best = colEntries[0];
1696
+ for (const e of colEntries) {
1697
+ if (e.srcCol <= col && (best.srcCol > col || e.srcCol > best.srcCol)) best = e;
1698
+ }
1699
+ if (best.srcCol > col) {
1700
+ for (const e of colEntries) {
1701
+ if (Math.abs(e.srcCol - col) < Math.abs(best.srcCol - col)) best = e;
1702
+ }
1703
+ }
1704
+ genLine = best.genLine;
1705
+ genColHint = best.genCol;
1706
+ bestSrcCol = best.srcCol;
1707
+ }
1708
+ }
1709
+
1710
+ if (genLine === undefined) {
1711
+ let best = -1;
1712
+ for (const [s] of entry.srcToGen) if (s <= line && s > best) best = s;
1713
+ if (best < 0) return undefined;
1714
+ genLine = entry.srcToGen.get(best);
1715
+ }
1716
+
1717
+ const srcLines = entry.source.split('\n');
1718
+ const genLines = entry.tsContent.split('\n');
1719
+ const KEYWORDS = new Set(['interface', 'type', 'enum', 'class', 'export', 'declare', 'extends', 'implements', 'import', 'from', 'def', 'const', 'let', 'var']);
1720
+
1721
+ if (srcLines[line] != null && genLines[genLine] != null) {
1722
+ const srcText = srcLines[line];
1723
+ const genText = genLines[genLine];
1724
+ const leftPart = srcText.substring(0, col).match(/\w*$/)?.[0] || '';
1725
+ const rightPart = srcText.substring(col).match(/^\w*/)?.[0] || '';
1726
+ let wordMatch = (leftPart + rightPart) ? [leftPart + rightPart] : null;
1727
+ if (wordMatch && KEYWORDS.has(wordMatch[0])) {
1728
+ const after = srcText.substring(col + wordMatch[0].length).match(/\s+(\w+)/);
1729
+ if (after) wordMatch = [after[1]];
1730
+ }
1731
+ if (wordMatch) {
1732
+ const word = wordMatch[0];
1733
+ const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1734
+ const wordStart = col - leftPart.length;
1735
+ const useHint = genColHint >= 0 && bestSrcCol >= wordStart && bestSrcCol < wordStart + word.length;
1736
+
1737
+ // Prefer the overload signature line (genLine-1) when it exists and
1738
+ // contains the same identifier — overloads carry typed parameters.
1739
+ let targetLine = genLine;
1740
+ let targetText = genText;
1741
+ if (genLine > 0) {
1742
+ const prevText = genLines[genLine - 1] || '';
1743
+ if (/^(?:export\s+)?function\s+\w+\(.*\).*;\s*$/.test(prevText)) {
1744
+ const re0 = new RegExp('\\b' + escaped + '\\b');
1745
+ if (re0.test(prevText)) { targetLine = genLine - 1; targetText = prevText; }
1746
+ }
1747
+ }
1748
+
1749
+ const re = new RegExp('\\b' + escaped + '\\b', 'g');
1750
+ let m, bestCol = -1, bestDist = Infinity;
1751
+ // When the word doesn't fall exactly on a mapped srcCol, extrapolate
1752
+ // the expected gen column from the nearest mapping entry. This avoids
1753
+ // picking a same-named word inside a string literal that happens to be
1754
+ // closer to the raw source column.
1755
+ const expectedGenCol = useHint ? genColHint
1756
+ : genColHint >= 0 ? genColHint + (col - bestSrcCol) : col;
1757
+ while ((m = re.exec(targetText)) !== null) {
1758
+ const dist = Math.abs(m.index - expectedGenCol);
1759
+ if (dist < bestDist) { bestDist = dist; bestCol = m.index; }
1760
+ }
1761
+ if (bestCol >= 0) return lineColToOffset(entry.tsContent, targetLine, bestCol);
1762
+
1763
+ // Fall back to original genLine if overload didn't match
1764
+ if (targetLine !== genLine) {
1765
+ const re1b = new RegExp('\\b' + escaped + '\\b', 'g');
1766
+ let m1b, bestCol1b = -1, bestDist1b = Infinity;
1767
+ while ((m1b = re1b.exec(genText)) !== null) {
1768
+ const dist = Math.abs(m1b.index - expectedGenCol);
1769
+ if (dist < bestDist1b) { bestDist1b = dist; bestCol1b = m1b.index; }
1770
+ }
1771
+ if (bestCol1b >= 0) return lineColToOffset(entry.tsContent, genLine, bestCol1b);
1772
+ }
1303
1773
 
1304
- export { mapToSourcePos, offsetToLine, getLineText, findNearestWord, lineColToOffset, offsetToLineCol, adjustSwitchDiagnostic, isInjectedOverload, srcToOffset } from './sourcemap-utils.js';
1774
+ // Word not on mapped line search nearby generated lines
1775
+ for (let delta = 1; delta <= 5; delta++) {
1776
+ for (const tryLine of [genLine + delta, genLine - delta]) {
1777
+ if (tryLine < 0 || tryLine >= genLines.length) continue;
1778
+ const tryText = genLines[tryLine];
1779
+ const re2 = new RegExp('\\b' + escaped + '\\b', 'g');
1780
+ let m2, best2 = -1, bestDist2 = Infinity;
1781
+ while ((m2 = re2.exec(tryText)) !== null) {
1782
+ const dist2 = Math.abs(m2.index - col);
1783
+ if (dist2 < bestDist2) { bestDist2 = dist2; best2 = m2.index; }
1784
+ }
1785
+ if (best2 >= 0) return lineColToOffset(entry.tsContent, tryLine, best2);
1786
+ }
1787
+ }
1788
+
1789
+ // Neighbor-line fallback: when the word isn't on the mapped gen line or
1790
+ // nearby ±5 lines, check neighboring source lines for srcColToGen entries
1791
+ // that point to gen lines containing the word. Handles multi-line
1792
+ // expressions collapsed to one gen line, bodiless overload signatures
1793
+ // mapped to wrong gen lines, etc.
1794
+ if (entry.srcColToGen) {
1795
+ const candidateGenLines = new Set();
1796
+ for (let d = 0; d <= 10; d++) {
1797
+ for (const sl of d === 0 ? [line] : [line - d, line + d]) {
1798
+ if (sl < 0) continue;
1799
+ const ce = entry.srcColToGen.get(sl);
1800
+ if (ce) {
1801
+ for (const e of ce) candidateGenLines.add(e.genLine);
1802
+ }
1803
+ }
1804
+ }
1805
+ // Also try gen lines near those candidates (overload signatures are
1806
+ // typically on the line just before the function body)
1807
+ const expanded = new Set(candidateGenLines);
1808
+ for (const gl of candidateGenLines) {
1809
+ for (let d = 1; d <= 3; d++) {
1810
+ expanded.add(gl - d);
1811
+ expanded.add(gl + d);
1812
+ }
1813
+ }
1814
+ let bestAlt = -1, bestAltCol = -1, bestAltDist = Infinity;
1815
+ for (const gl of expanded) {
1816
+ if (gl < 0 || gl >= genLines.length) continue;
1817
+ const altText = genLines[gl] || '';
1818
+ const re3 = new RegExp('\\b' + escaped + '\\b', 'g');
1819
+ let m3;
1820
+ while ((m3 = re3.exec(altText)) !== null) {
1821
+ const dist3 = Math.abs(m3.index - expectedGenCol);
1822
+ if (dist3 < bestAltDist) { bestAltDist = dist3; bestAltCol = m3.index; bestAlt = gl; }
1823
+ }
1824
+ }
1825
+ if (bestAltCol >= 0) return lineColToOffset(entry.tsContent, bestAlt, bestAltCol);
1826
+ }
1827
+ }
1828
+ }
1829
+
1830
+ const genText = entry.tsContent.split('\n')[genLine] || '';
1831
+ if (col < genText.length) return lineColToOffset(entry.tsContent, genLine, col);
1832
+ return lineColToOffset(entry.tsContent, genLine, 0);
1833
+ }
1305
1834
 
1306
1835
  // Map a TypeScript diagnostic offset back to a Rip source line number.
1307
1836
  // Returns -1 if the offset falls in the DTS header.