rip-lang 3.13.136 → 3.14.0

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
@@ -12,9 +12,10 @@
12
12
 
13
13
  import { Compiler, getStdlibCode } from './compiler.js';
14
14
  import { INTRINSIC_TYPE_DECLS, INTRINSIC_FN_DECL, ARIA_TYPE_DECLS, SIGNAL_INTERFACE, SIGNAL_FN, COMPUTED_INTERFACE, COMPUTED_FN, EFFECT_FN } from './types.js';
15
+ import { hasSchemas } from './schema.js';
15
16
  import { createRequire } from 'module';
16
17
  import { readFileSync, existsSync, readdirSync } from 'fs';
17
- import { mapToSourcePos, offsetToLine, getLineText, findNearestWord, lineColToOffset, offsetToLineCol, adjustSwitchDiagnostic, isInjectedOverload } from './sourcemap-utils.js';
18
+ import { mapToSourcePos, offsetToLine, getLineText, findNearestWord, lineColToOffset, offsetToLineCol, adjustSwitchDiagnostic, isInjectedOverload, srcToOffset } from './sourcemap-utils.js';
18
19
  import { resolve, relative, dirname } from 'path';
19
20
  import { buildLineMap } from './sourcemaps.js';
20
21
 
@@ -198,6 +199,54 @@ export function patchUninitializedTypes(ts, service, compiledEntries) {
198
199
  if (ts.isFunctionDeclaration(stmt) && stmt.body) {
199
200
  patchStatements(stmt.body.statements);
200
201
  }
202
+ // Recurse into function expressions / arrows in known patterns:
203
+ // - Variable initializers: const x = __computed(() => { ... })
204
+ // - Call expression arguments: __effect(() => { ... })
205
+ // Avoids broad ts.forEachChild to prevent patching unrelated nested scopes.
206
+ const walkInitializersAndArgs = (node) => {
207
+ if (!node) return;
208
+ if ((ts.isFunctionExpression(node) || ts.isArrowFunction(node)) && node.body) {
209
+ if (ts.isBlock(node.body)) patchStatements(node.body.statements);
210
+ return;
211
+ }
212
+ // Variable declarations: recurse into each initializer
213
+ if (ts.isVariableStatement(node)) {
214
+ for (const decl of node.declarationList.declarations) {
215
+ if (decl.initializer) walkInitializersAndArgs(decl.initializer);
216
+ }
217
+ return;
218
+ }
219
+ // Expression statements: recurse into the expression
220
+ if (ts.isExpressionStatement(node)) {
221
+ walkInitializersAndArgs(node.expression);
222
+ return;
223
+ }
224
+ // Call expressions: recurse into each argument
225
+ if (ts.isCallExpression(node)) {
226
+ for (const arg of node.arguments) walkInitializersAndArgs(arg);
227
+ // Also check the expression being called (e.g., chained calls)
228
+ if (ts.isCallExpression(node.expression)) walkInitializersAndArgs(node.expression);
229
+ return;
230
+ }
231
+ // Parenthesized: unwrap
232
+ if (ts.isParenthesizedExpression(node)) {
233
+ walkInitializersAndArgs(node.expression);
234
+ return;
235
+ }
236
+ // Class declarations/expressions: recurse into property initializers and method bodies
237
+ if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
238
+ for (const member of node.members) {
239
+ if (ts.isPropertyDeclaration(member) && member.initializer) {
240
+ walkInitializersAndArgs(member.initializer);
241
+ }
242
+ if ((ts.isMethodDeclaration(member) || ts.isConstructorDeclaration(member)) && member.body) {
243
+ patchStatements(member.body.statements);
244
+ }
245
+ }
246
+ return;
247
+ }
248
+ };
249
+ walkInitializersAndArgs(stmt);
201
250
  }
202
251
  }
203
252
 
@@ -482,7 +531,7 @@ export function compileForCheck(filePath, source, compiler, opts = {}) {
482
531
  // A `# @nocheck` comment near the top of the file opts out entirely.
483
532
  // In strict mode, all non-nocheck files are type-checked.
484
533
  const nocheck = /^#\s*@nocheck\b/m.test(source.slice(0, NOCHECK_SCAN_LIMIT));
485
- const hasOwnTypes = !nocheck && (hasTypeAnnotations(source) || !!opts.strict);
534
+ const hasOwnTypes = !nocheck && (hasTypeAnnotations(source) || hasSchemas(source) || !!opts.strict);
486
535
  let importsTyped = false;
487
536
  if (!hasOwnTypes && !nocheck) {
488
537
  const ripImports = [...source.matchAll(/from\s+['"]([^'"]*\.rip)['"]/g)];
@@ -716,6 +765,28 @@ export function compileForCheck(filePath, source, compiler, opts = {}) {
716
765
  }
717
766
  }
718
767
 
768
+ // Remove non-exported `declare class X` blocks from the DTS header when the
769
+ // code body has `X = class { ... }` (non-exported component stubs). TS treats
770
+ // `declare class X` as a class declaration and reports TS2629 when the body
771
+ // later reassigns `X = class { ... }`. The body's class expression already
772
+ // contains all type info (from stubComponents), so the header block is redundant.
773
+ if (hasTypes && headerDts && code) {
774
+ const dl = headerDts.split('\n');
775
+ const removedLines = new Set();
776
+ for (let i = 0; i < dl.length; i++) {
777
+ const m = dl[i].match(/^declare\s+class\s+(\w+)/);
778
+ if (!m) continue;
779
+ const name = m[1];
780
+ if (!new RegExp('^' + name + '\\s*=\\s*class\\b', 'm').test(code)) continue;
781
+ let j = i;
782
+ while (j < dl.length && !dl[j].match(/^\}/)) j++;
783
+ for (let k = i; k <= j; k++) removedLines.add(k);
784
+ }
785
+ if (removedLines.size > 0) {
786
+ headerDts = dl.filter((_, i) => !removedLines.has(i)).join('\n').trimEnd() + '\n';
787
+ }
788
+ }
789
+
719
790
  // Copy typed constructor props parameter to _init(props) in component classes.
720
791
  // Components compile constructor(props: T) and _init(props) separately — TS
721
792
  // needs _init to have the same props type to avoid noImplicitAny.
@@ -917,13 +988,47 @@ export function compileForCheck(filePath, source, compiler, opts = {}) {
917
988
  }
918
989
 
919
990
  // Inline hoisted `let` declarations at their first assignment in the shadow
920
- // TS file. Rip always hoists `let` to the top of scope for correct JS
921
- // scoping. For TypeScript inference, we move the declaration inline so TS
922
- // can infer types from initializers (e.g., `let x; x = 1` → `let x = 1`).
923
- // Only rewrites same-scope, straight-line assignments stops at control
924
- // flow to avoid changing binding visibility in the shadow file.
991
+ // TS file, then merge DTS header types into the inlined declarations.
992
+ //
993
+ // Phase 1 straight-line: scan same-indent lines from the hoisted `let`
994
+ // downward, stopping at structural statements (if/for/while/etc.). This
995
+ // handles the common case where assignment immediately follows declaration.
996
+ //
997
+ // Phase 2 — block-confined: for variables not resolved in phase 1, check
998
+ // if the first assignment is inside a deeper block and ALL references to
999
+ // the variable are confined to that block. If so, inline there. TS still
1000
+ // enforces block scoping in non-executed code, so we must verify the
1001
+ // variable isn't referenced after the block exits.
1002
+ //
1003
+ // DTS header types are merged during inlining: `let x: Type = value;`.
1004
+ // Header lines that were merged are removed afterward to avoid TS2454.
925
1005
  if (hasTypes && code) {
926
1006
  const cl = code.split('\n');
1007
+ const reEsc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1008
+
1009
+ // Build DTS header type map for merging
1010
+ const letTypes = new Map();
1011
+ const movedDts = new Set();
1012
+ let dl;
1013
+ if (headerDts) {
1014
+ dl = headerDts.split('\n');
1015
+ for (let i = 0; i < dl.length; i++) {
1016
+ const m = dl[i].match(/^(?:export\s+)?(?:declare\s+)?let\s+(\w+):\s+(.+);$/);
1017
+ if (m) letTypes.set(m[1], { type: m[2], idx: i });
1018
+ }
1019
+ }
1020
+
1021
+ // Helper: inline a variable at the given line
1022
+ const doInline = (v, lineIdx, indent, rhs) => {
1023
+ const dts = letTypes.get(v);
1024
+ if (dts) {
1025
+ cl[lineIdx] = `${indent}let ${v}: ${dts.type} = ${rhs};`;
1026
+ movedDts.add(dts.idx);
1027
+ } else {
1028
+ cl[lineIdx] = `${indent}let ${v} = ${rhs};`;
1029
+ }
1030
+ };
1031
+
927
1032
  for (let i = 0; i < cl.length; i++) {
928
1033
  const m = cl[i].match(/^(\s*)let\s+([A-Za-z_$][\w$]*(?:\s*,\s*[A-Za-z_$][\w$]*)*)\s*;\s*$/);
929
1034
  if (!m) continue;
@@ -932,29 +1037,29 @@ export function compileForCheck(filePath, source, compiler, opts = {}) {
932
1037
  for (let k = i - 1; k >= 0; k--) { if (cl[k].trim() !== '') { prev = cl[k]; break; } }
933
1038
  if (prev !== null && !/\{\s*$/.test(prev)) continue;
934
1039
 
935
- const indent = m[1];
1040
+ const baseIndent = m[1];
936
1041
  const vars = m[2].split(/\s*,\s*/);
937
1042
  const inlined = new Set();
938
1043
  const bailed = new Set();
939
- const reEsc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1044
+ const scopeEndRe = new RegExp('^' + reEsc(baseIndent) + '}');
940
1045
 
1046
+ // Phase 1: straight-line scan at base indent
941
1047
  for (let j = i + 1; j < cl.length; j++) {
942
1048
  const line = cl[j];
943
1049
  if (line.trim() === '') continue;
944
- // End of scope
945
- if (new RegExp('^' + reEsc(indent) + '}').test(line)) break;
1050
+ if (scopeEndRe.test(line)) break;
946
1051
  // Skip deeper-indented lines
947
- if (line.startsWith(indent + ' ')) continue;
948
- // Stop at structural statements
949
- if (new RegExp('^' + reEsc(indent) + '(?:if\\b|for\\b|while\\b|switch\\b|try\\b|catch\\b|finally\\b|function\\b|class\\b|do\\b|\\{|\\})').test(line)) break;
950
-
1052
+ if (line.startsWith(baseIndent + ' ')) continue;
1053
+ // Stop at structural statements (if/for/while/switch/try/do/function/class)
1054
+ if (/^\s*(?:if|for|while|switch|try|do|function|class)\s*[\s({]/.test(line)) break;
1055
+ if (/^\s*\} (?:else|catch|finally)/.test(line)) break;
951
1056
  for (const v of vars) {
952
1057
  if (inlined.has(v) || bailed.has(v)) continue;
953
1058
  const ve = reEsc(v);
954
- const assignRe = new RegExp('^' + reEsc(indent) + ve + '\\s*=\\s*(.*);\\s*$');
1059
+ const assignRe = new RegExp('^' + reEsc(baseIndent) + ve + '\\s*=(?!=)\\s*(.*);\\s*$');
955
1060
  const assign = line.match(assignRe);
956
1061
  if (assign) {
957
- cl[j] = `${indent}let ${v} = ${assign[1]};`;
1062
+ doInline(v, j, baseIndent, assign[1]);
958
1063
  inlined.add(v);
959
1064
  continue;
960
1065
  }
@@ -963,10 +1068,70 @@ export function compileForCheck(filePath, source, compiler, opts = {}) {
963
1068
  if (vars.every(v => inlined.has(v) || bailed.has(v))) break;
964
1069
  }
965
1070
 
1071
+ // Phase 2: block-confined scan for remaining variables
1072
+ for (const v of vars) {
1073
+ if (inlined.has(v) || bailed.has(v)) continue;
1074
+ const ve = reEsc(v);
1075
+ const vRe = new RegExp('\\b' + ve + '\\b');
1076
+
1077
+ // Find the first reference and first assignment anywhere in the scope
1078
+ let firstRefLine = -1, foundAssign = null;
1079
+ for (let j = i + 1; j < cl.length; j++) {
1080
+ const line = cl[j];
1081
+ if (line.trim() === '') continue;
1082
+ if (scopeEndRe.test(line)) break;
1083
+ if (!vRe.test(line)) continue;
1084
+ if (firstRefLine < 0) firstRefLine = j;
1085
+ if (!foundAssign) {
1086
+ const lineIndent = line.match(/^(\s*)/)[1];
1087
+ const assignRe = new RegExp('^' + reEsc(lineIndent) + ve + '\\s*=(?!=)\\s*(.*);\\s*$');
1088
+ const am = line.match(assignRe);
1089
+ if (am) foundAssign = { line: j, indent: lineIndent, rhs: am[1] };
1090
+ }
1091
+ if (foundAssign) break;
1092
+ }
1093
+
1094
+ if (!foundAssign) continue;
1095
+ if (foundAssign.indent === baseIndent) continue; // phase 1 territory
1096
+ if (firstRefLine !== foundAssign.line) continue; // read before write
1097
+
1098
+ // Find where the enclosing block exits (first line at indent < assignment indent)
1099
+ let blockEndLine = -1;
1100
+ for (let j = foundAssign.line + 1; j < cl.length; j++) {
1101
+ const line = cl[j];
1102
+ if (line.trim() === '') continue;
1103
+ if (scopeEndRe.test(line)) { blockEndLine = j; break; }
1104
+ const li = line.match(/^(\s*)/)[1];
1105
+ if (li.length < foundAssign.indent.length) { blockEndLine = j; break; }
1106
+ }
1107
+
1108
+ // Check if the variable is referenced after the block exits
1109
+ let hasRefAfterBlock = false;
1110
+ if (blockEndLine >= 0) {
1111
+ for (let j = blockEndLine + 1; j < cl.length; j++) {
1112
+ const line = cl[j];
1113
+ if (line.trim() === '') continue;
1114
+ if (scopeEndRe.test(line)) break;
1115
+ if (vRe.test(line)) { hasRefAfterBlock = true; break; }
1116
+ }
1117
+ }
1118
+
1119
+ if (hasRefAfterBlock) continue; // used outside the block — leave hoisted
1120
+
1121
+ doInline(v, foundAssign.line, foundAssign.indent, foundAssign.rhs);
1122
+ inlined.add(v);
1123
+ }
1124
+
966
1125
  const remaining = vars.filter(v => !inlined.has(v));
967
- cl[i] = remaining.length ? `${indent}let ${remaining.join(', ')};` : '';
1126
+ if (remaining.length) cl[i] = `${baseIndent}let ${remaining.join(', ')};`;
1127
+ else cl[i] = '';
1128
+ }
1129
+ code = cl.join('\n');
1130
+
1131
+ // Remove DTS header lines that were merged into body declarations
1132
+ if (movedDts.size > 0 && dl) {
1133
+ headerDts = dl.filter((_, i) => !movedDts.has(i)).join('\n').trimEnd() + '\n';
968
1134
  }
969
- code = cl.filter(l => l !== '').join('\n');
970
1135
  }
971
1136
 
972
1137
  let tsContent = (hasTypes ? headerDts + '\n' : '') + code;
@@ -975,6 +1140,25 @@ export function compileForCheck(filePath, source, compiler, opts = {}) {
975
1140
  // Build bidirectional line maps
976
1141
  const { srcToGen, genToSrc, srcColToGen } = buildLineMap(result.reverseMap, result.map, headerLines);
977
1142
 
1143
+ // Fix srcToGen entries that point to lines emptied by Phase 2 inlining.
1144
+ // When a hoisted `let` is inlined, its original line becomes empty (""), but
1145
+ // buildLineMap still maps source lines to that position. Redirect to the
1146
+ // nearest non-empty alternative from srcColToGen.
1147
+ const tsLines = tsContent.split('\n');
1148
+ for (const [srcLine, genLine] of srcToGen) {
1149
+ if (genLine >= 0 && genLine < tsLines.length && tsLines[genLine] === '') {
1150
+ const colEntries = srcColToGen.get(srcLine);
1151
+ if (colEntries) {
1152
+ for (const e of colEntries) {
1153
+ if (e.genLine >= 0 && e.genLine < tsLines.length && tsLines[e.genLine] !== '') {
1154
+ srcToGen.set(srcLine, e.genLine);
1155
+ break;
1156
+ }
1157
+ }
1158
+ }
1159
+ }
1160
+ }
1161
+
978
1162
  // Snapshot code-section mappings before DTS mapping can overwrite them.
979
1163
  // Needed by @ts-expect-error injection which must target code lines, not DTS.
980
1164
  const codeSrcToGen = new Map(srcToGen);
@@ -1117,7 +1301,7 @@ export function compileForCheck(filePath, source, compiler, opts = {}) {
1117
1301
 
1118
1302
  // ── Source mapping helpers (delegated to sourcemap-utils.js) ───────
1119
1303
 
1120
- export { mapToSourcePos, offsetToLine, getLineText, findNearestWord, lineColToOffset, offsetToLineCol, adjustSwitchDiagnostic, isInjectedOverload } from './sourcemap-utils.js';
1304
+ export { mapToSourcePos, offsetToLine, getLineText, findNearestWord, lineColToOffset, offsetToLineCol, adjustSwitchDiagnostic, isInjectedOverload, srcToOffset } from './sourcemap-utils.js';
1121
1305
 
1122
1306
  // Map a TypeScript diagnostic offset back to a Rip source line number.
1123
1307
  // Returns -1 if the offset falls in the DTS header.
@@ -1156,7 +1340,9 @@ export function readProjectConfig(dir) {
1156
1340
  if (parent === d) break;
1157
1341
  d = parent;
1158
1342
  }
1159
- } catch {}
1343
+ } catch (e) {
1344
+ console.warn(`[rip] readProjectConfig error: ${e.message}`);
1345
+ }
1160
1346
  return config;
1161
1347
  }
1162
1348
 
@@ -1305,7 +1491,9 @@ export async function runCheck(targetDir, opts = {}) {
1305
1491
  try {
1306
1492
  const impSrc = readFileSync(imported, 'utf8');
1307
1493
  compiled.set(imported, compileForCheck(imported, impSrc, new Compiler()));
1308
- } catch {}
1494
+ } catch (e) {
1495
+ console.warn(`[rip] cross-module compile failed for ${imported}: ${e.message}`);
1496
+ }
1309
1497
  }
1310
1498
  }
1311
1499
  }
@@ -1386,7 +1574,10 @@ export async function runCheck(targetDir, opts = {}) {
1386
1574
  const sem = service.getSemanticDiagnostics(vf);
1387
1575
  const syn = service.getSyntacticDiagnostics(vf);
1388
1576
  diags = [...syn, ...sem];
1389
- } catch {
1577
+ } catch (e) {
1578
+ const rel = relative(rootPath, fp);
1579
+ console.error(`${red('error')} ${cyan(rel)}: diagnostics failed — ${e.message}`);
1580
+ totalErrors++;
1390
1581
  continue;
1391
1582
  }
1392
1583
 
@@ -1555,6 +1746,187 @@ export async function runCheck(targetDir, opts = {}) {
1555
1746
  }
1556
1747
  }
1557
1748
 
1749
+ // ── Source map audit ─────────────────────────────────────────────
1750
+ // Walk every identifier in each Rip source file and verify the source map
1751
+ // round-trip: srcToOffset must resolve, and getQuickInfoAtPosition must
1752
+ // return hover info for it. Failures indicate source map gaps that make
1753
+ // hover/definition/completion silently break in the editor.
1754
+
1755
+ const AUDIT_SKIP = new Set([
1756
+ 'if', 'else', 'then', 'unless', 'switch', 'when', 'for', 'while', 'until',
1757
+ 'loop', 'do', 'try', 'catch', 'finally', 'throw', 'return', 'break',
1758
+ 'continue', 'yield', 'await', 'new', 'delete', 'typeof', 'instanceof',
1759
+ 'in', 'of', 'as', 'is', 'isnt', 'not', 'and', 'or', 'yes', 'no',
1760
+ 'true', 'false', 'null', 'undefined', 'this', 'super', 'class', 'extends',
1761
+ 'import', 'export', 'from', 'default', 'def', 'render', 'component',
1762
+ 'type', 'interface', 'enum', 'const', 'let', 'var', 'void', 'async',
1763
+ 'static', 'get', 'set', 'constructor', 'declare', 'implements', 'readonly',
1764
+ 'offer', 'accept', 'it', 'stash',
1765
+ // Type keywords — never have hover info in value position
1766
+ 'number', 'string', 'boolean', 'any', 'unknown', 'never', 'object',
1767
+ 'symbol', 'bigint',
1768
+ ]);
1769
+ // Standard library: runtime globals injected by Rip (no TS declaration)
1770
+ const STDLIB = new Set([
1771
+ 'abort', 'assert', 'exit', 'kind', 'noop', 'p', 'pp', 'raise', 'rand',
1772
+ 'sleep', 'todo', 'warn', 'zip',
1773
+ ]);
1774
+ // Build string and comment regions for a source line so the audit can
1775
+ // accurately skip identifiers inside strings/comments without being fooled
1776
+ // by interpolation `#{}`, escaped quotes, or apostrophes in double-quoted
1777
+ // strings. Returns an array of [start, end] ranges that are "non-code".
1778
+ function nonCodeRegions(line) {
1779
+ const regions = [];
1780
+ let i = 0;
1781
+ while (i < line.length) {
1782
+ const ch = line[i];
1783
+ // Single-line comment — any # outside a string starts a comment
1784
+ // (#{} interpolation only exists inside double-quoted strings)
1785
+ if (ch === '#') {
1786
+ regions.push([i, line.length]);
1787
+ return regions;
1788
+ }
1789
+ // String literal
1790
+ if (ch === '"' || ch === "'") {
1791
+ const quote = ch;
1792
+ const start = i;
1793
+ i++;
1794
+ while (i < line.length) {
1795
+ if (line[i] === '\\') { i += 2; continue; }
1796
+ if (line[i] === '#' && line[i + 1] === '{' && quote === '"') {
1797
+ // Interpolation — skip to matching }
1798
+ let depth = 1;
1799
+ i += 2;
1800
+ while (i < line.length && depth > 0) {
1801
+ if (line[i] === '{') depth++;
1802
+ else if (line[i] === '}') depth--;
1803
+ if (depth > 0) i++;
1804
+ }
1805
+ if (i < line.length) i++; // skip closing }
1806
+ continue;
1807
+ }
1808
+ if (line[i] === quote) { i++; break; }
1809
+ i++;
1810
+ }
1811
+ regions.push([start, i]);
1812
+ continue;
1813
+ }
1814
+ i++;
1815
+ }
1816
+ return regions;
1817
+ }
1818
+ let auditGaps = 0;
1819
+ const auditResults = [];
1820
+
1821
+ for (const [fp, entry] of compiled) {
1822
+ if (!entry.hasTypes) continue;
1823
+ const srcLines = entry.source.split('\n');
1824
+ const vf = toVirtual(fp);
1825
+ const gaps = [];
1826
+
1827
+ // Detect render block line ranges (indented under `render`)
1828
+ const renderLines = new Set();
1829
+ let renderIndent = -1;
1830
+ for (let i = 0; i < srcLines.length; i++) {
1831
+ const line = srcLines[i];
1832
+ const trimmed = line.trimStart();
1833
+ const indent = line.length - trimmed.length;
1834
+ if (/^render\b/.test(trimmed)) {
1835
+ renderIndent = indent;
1836
+ renderLines.add(i);
1837
+ continue;
1838
+ }
1839
+ if (renderIndent >= 0) {
1840
+ if (trimmed === '' || indent > renderIndent) { renderLines.add(i); continue; }
1841
+ renderIndent = -1;
1842
+ }
1843
+ }
1844
+
1845
+ for (let line = 0; line < srcLines.length; line++) {
1846
+ const srcLine = srcLines[line];
1847
+ // Skip comments, blank lines, and render blocks
1848
+ if (/^\s*(#|$)/.test(srcLine)) continue;
1849
+ if (renderLines.has(line)) continue;
1850
+
1851
+ // Build string/comment regions for accurate skipping
1852
+ const skipRegions = nonCodeRegions(srcLine);
1853
+
1854
+ // Detect type-annotation region: everything after :: (but not ::=)
1855
+ // e.g. "x:: number = 42" — skip "number" but not "x"
1856
+ // e.g. "def add(a:: number, b:: number):: number" — skip all type words
1857
+ let typeRegions = [];
1858
+ const typeRe = /::\s*/g;
1859
+ let tm;
1860
+ while ((tm = typeRe.exec(srcLine)) !== null) {
1861
+ // Skip :: inside string/comment regions
1862
+ if (skipRegions.some(([s, e]) => tm.index >= s && tm.index < e)) continue;
1863
+ // Find the end of this type annotation (next = or , or ) or EOL)
1864
+ const start = tm.index + tm[0].length;
1865
+ // Walk forward to find the boundary
1866
+ let depth = 0, end = srcLine.length;
1867
+ for (let i = start; i < srcLine.length; i++) {
1868
+ const ch = srcLine[i];
1869
+ if (ch === '(' || ch === '[' || ch === '{' || ch === '<') depth++;
1870
+ else if (ch === ')' || ch === ']' || ch === '}' || ch === '>') {
1871
+ if (depth > 0) depth--;
1872
+ else { end = i; break; }
1873
+ }
1874
+ else if (depth === 0 && (ch === ',' || (ch === '=' && srcLine[i + 1] !== '>'))) { end = i; break; }
1875
+ }
1876
+ typeRegions.push([start, end]);
1877
+ }
1878
+
1879
+ const re = /\b([a-zA-Z_$]\w*)\b/g;
1880
+ let m;
1881
+ while ((m = re.exec(srcLine)) !== null) {
1882
+ const word = m[1];
1883
+ if (AUDIT_SKIP.has(word)) continue;
1884
+ if (STDLIB.has(word)) continue;
1885
+ // Skip @prop references (start with @)
1886
+ if (m.index > 0 && srcLine[m.index - 1] === '@') continue;
1887
+ // Skip base element name in `component extends <element>`
1888
+ if (/\bextends\s+$/.test(srcLine.slice(0, m.index)) && /\bcomponent\b/.test(srcLine)) continue;
1889
+ // Skip words in type-annotation position
1890
+ if (typeRegions.some(([s, e]) => m.index >= s && m.index < e)) continue;
1891
+ // Skip words inside strings or comments
1892
+ if (skipRegions.some(([s, e]) => m.index >= s && m.index < e)) continue;
1893
+
1894
+ const col = m.index;
1895
+ const offset = srcToOffset(entry, line, col);
1896
+ if (offset === undefined) {
1897
+ gaps.push({ line: line + 1, col: col + 1, word, issue: 'no mapping' });
1898
+ continue;
1899
+ }
1900
+ try {
1901
+ const info = service.getQuickInfoAtPosition(vf, offset);
1902
+ if (!info) {
1903
+ gaps.push({ line: line + 1, col: col + 1, word, issue: 'no hover info' });
1904
+ }
1905
+ } catch {
1906
+ gaps.push({ line: line + 1, col: col + 1, word, issue: 'hover query failed' });
1907
+ }
1908
+ }
1909
+ }
1910
+
1911
+ if (gaps.length > 0) {
1912
+ auditResults.push({ file: fp, gaps });
1913
+ auditGaps += gaps.length;
1914
+ }
1915
+ }
1916
+
1917
+ // Print audit results
1918
+ if (auditResults.length > 0) {
1919
+ console.log(bold('\n── Source Map Audit ──\n'));
1920
+ for (const { file, gaps } of auditResults) {
1921
+ const rel = relative(rootPath, file);
1922
+ for (const g of gaps) {
1923
+ const loc = `${cyan(rel)}${dim(':')}${yellow(String(g.line))}${dim(':')}${yellow(String(g.col))}`;
1924
+ console.log(`${loc} ${dim('-')} ${yellow('warning')} ${dim('audit:')} ${g.issue} for '${g.word}'`);
1925
+ }
1926
+ }
1927
+ console.log(`\n${yellow(String(auditGaps))} source map gap${auditGaps === 1 ? '' : 's'} found\n`);
1928
+ }
1929
+
1558
1930
  // Print results — tsc format with Rip source positions
1559
1931
  for (const { file, errors } of fileResults) {
1560
1932
  const rel = relative(rootPath, file);
package/src/types.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { SCHEMA_INTRINSIC_DECLS, emitSchemaTypes } from './schema.js';
2
+
1
3
  // Type System — Optional type annotations and .d.ts emission for Rip
2
4
  //
3
5
  // Architecture:
@@ -1320,6 +1322,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
1320
1322
 
1321
1323
  // Walk s-expression tree for component declarations
1322
1324
  let componentVars = new Set();
1325
+ let hasSchemaDecls = false;
1323
1326
  if (sexpr) {
1324
1327
  usesRipIntrinsicProps = emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars, sourceLines) || usesRipIntrinsicProps;
1325
1328
 
@@ -1330,6 +1333,23 @@ export function emitTypes(tokens, sexpr = null, source = '') {
1330
1333
  if (match && componentVars.has(match[1])) lines.splice(k, 1);
1331
1334
  }
1332
1335
  }
1336
+
1337
+ // Schema declarations — strip any prior auto-emitted `declare let Foo`
1338
+ // for the same bindings (they are re-emitted as typed Schema<T>).
1339
+ let schemaLines = [];
1340
+ hasSchemaDecls = emitSchemaTypes(sexpr, schemaLines);
1341
+ if (hasSchemaDecls) {
1342
+ let bindings = new Set();
1343
+ for (let line of schemaLines) {
1344
+ let m = line.match(/(?:declare |export )*const (\w+)/);
1345
+ if (m) bindings.add(m[1]);
1346
+ }
1347
+ for (let k = lines.length - 1; k >= 0; k--) {
1348
+ let m = lines[k].match(/(?:declare |export )*(?:const|let) (\w+)/);
1349
+ if (m && bindings.has(m[1])) lines.splice(k, 1);
1350
+ }
1351
+ lines.push(...schemaLines);
1352
+ }
1333
1353
  }
1334
1354
 
1335
1355
  if (lines.length === 0) return null;
@@ -1353,6 +1373,9 @@ export function emitTypes(tokens, sexpr = null, source = '') {
1353
1373
  if (usesSignal || usesComputed) {
1354
1374
  preamble.push(EFFECT_FN);
1355
1375
  }
1376
+ if (hasSchemaDecls) {
1377
+ preamble.push(...SCHEMA_INTRINSIC_DECLS);
1378
+ }
1356
1379
  if (preamble.length > 0) {
1357
1380
  preamble.push('');
1358
1381
  }
@@ -1546,6 +1569,8 @@ function emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars, so
1546
1569
  } else {
1547
1570
  lines.push(` constructor(props${propsOpt}: ${inheritedPropsType});`);
1548
1571
  }
1572
+ } else {
1573
+ lines.push(` constructor(props?: {});`);
1549
1574
  }
1550
1575
  for (let [refName, refType] of refMembers) {
1551
1576
  bodyMembers.push(` ${refName}: ${refType};`);