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/README.md +46 -4
- package/docs/RIP-LANG.md +116 -11
- package/docs/RIP-SCHEMA.md +2390 -0
- package/docs/RIP-TYPES.md +21 -14
- package/docs/assets/rip-schema-logo-960w.png +0 -0
- package/docs/assets/rip-schema-social.png +0 -0
- package/docs/dist/rip.js +6817 -3670
- package/docs/dist/rip.min.js +1454 -211
- package/docs/dist/rip.min.js.br +0 -0
- package/package.json +10 -4
- package/src/AGENTS.md +130 -0
- package/src/compiler.js +65 -2
- package/src/components.js +19 -5
- package/src/grammar/grammar.rip +20 -1
- package/src/lexer.js +42 -0
- package/src/parser.js +222 -220
- package/src/schema.js +3298 -0
- package/src/sourcemap-utils.js +155 -0
- package/src/typecheck.js +395 -23
- package/src/types.js +25 -0
- package/src/ui.rip +203 -45
- package/CHANGELOG.md +0 -1500
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
|
|
921
|
-
//
|
|
922
|
-
//
|
|
923
|
-
//
|
|
924
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
948
|
-
// Stop at structural statements
|
|
949
|
-
if (
|
|
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(
|
|
1059
|
+
const assignRe = new RegExp('^' + reEsc(baseIndent) + ve + '\\s*=(?!=)\\s*(.*);\\s*$');
|
|
955
1060
|
const assign = line.match(assignRe);
|
|
956
1061
|
if (assign) {
|
|
957
|
-
|
|
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] =
|
|
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};`);
|