rip-lang 3.13.134 → 3.13.136
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/CHANGELOG.md +0 -1
- package/README.md +4 -7
- package/bin/rip +16 -4
- package/docs/RIP-LANG.md +0 -42
- package/docs/RIP-TYPES.md +47 -52
- package/docs/demo.html +2 -2
- package/docs/dist/rip.js +1039 -449
- package/docs/dist/rip.min.js +168 -164
- package/docs/dist/rip.min.js.br +0 -0
- package/package.json +1 -1
- package/rip-loader.js +2 -2
- package/src/AGENTS.md +1 -2
- package/src/browser.js +5 -5
- package/src/compiler.js +106 -28
- package/src/components.js +176 -11
- package/src/error.js +250 -0
- package/src/grammar/grammar.rip +2 -12
- package/src/lexer.js +15 -11
- package/src/parser.js +220 -223
- package/src/repl.js +3 -2
- package/src/sourcemap-utils.js +39 -6
- package/src/typecheck.js +365 -80
- package/src/types.js +226 -51
- package/src/ui.rip +4 -0
package/src/typecheck.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// type errors mapped back to Rip source positions.
|
|
12
12
|
|
|
13
13
|
import { Compiler, getStdlibCode } from './compiler.js';
|
|
14
|
+
import { INTRINSIC_TYPE_DECLS, INTRINSIC_FN_DECL, ARIA_TYPE_DECLS, SIGNAL_INTERFACE, SIGNAL_FN, COMPUTED_INTERFACE, COMPUTED_FN, EFFECT_FN } from './types.js';
|
|
14
15
|
import { createRequire } from 'module';
|
|
15
16
|
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
16
17
|
import { mapToSourcePos, offsetToLine, getLineText, findNearestWord, lineColToOffset, offsetToLineCol, adjustSwitchDiagnostic, isInjectedOverload } from './sourcemap-utils.js';
|
|
@@ -32,7 +33,7 @@ export function hasTypeAnnotations(source) {
|
|
|
32
33
|
line = line.replace(/#.*$/, '');
|
|
33
34
|
// Strip string literals (single and double quoted)
|
|
34
35
|
line = line.replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)*'/g, "''");
|
|
35
|
-
return /::[ \t=]/.test(line);
|
|
36
|
+
return /::[ \t=]/.test(line) || /(?:^|export\s+)type\s+[A-Z]/.test(line.trimStart());
|
|
36
37
|
});
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -118,25 +119,29 @@ export function parseComponentDTS(dtsString) {
|
|
|
118
119
|
const name = cm[1];
|
|
119
120
|
const props = [];
|
|
120
121
|
let hasIntrinsicProps = false;
|
|
122
|
+
let inheritsTag = null;
|
|
121
123
|
let j = i + 1;
|
|
122
124
|
while (j < lines.length) {
|
|
123
125
|
if (/^\}/.test(lines[j])) break;
|
|
124
126
|
if (/constructor\(props\??/.test(lines[j])) {
|
|
125
127
|
if (lines[j].includes('__RipProps<')) hasIntrinsicProps = true;
|
|
128
|
+
const tagMatch = lines[j].match(/__RipProps<'(\w+)'>/);
|
|
129
|
+
if (tagMatch) inheritsTag = tagMatch[1];
|
|
126
130
|
if (!lines[j].includes('{')) { j++; continue; }
|
|
127
131
|
j++;
|
|
128
132
|
while (j < lines.length) {
|
|
129
133
|
if (lines[j].includes('__RipProps<')) hasIntrinsicProps = true;
|
|
134
|
+
if (!inheritsTag) { const tm = lines[j].match(/__RipProps<'(\w+)'>/); if (tm) inheritsTag = tm[1]; }
|
|
130
135
|
if (/^\s*\}\s*(?:&\s*.+)?\);\s*$/.test(lines[j])) { j++; break; }
|
|
131
136
|
const pm = lines[j].match(/^\s+(\w+)(\?)?\s*:\s*(.+);$/);
|
|
132
|
-
if (pm) props.push({ name: pm[1], type: pm[3].trim(), required: !pm[2] });
|
|
137
|
+
if (pm && !pm[1].startsWith('__bind_')) props.push({ name: pm[1], type: pm[3].trim(), required: !pm[2] });
|
|
133
138
|
j++;
|
|
134
139
|
}
|
|
135
140
|
continue;
|
|
136
141
|
}
|
|
137
142
|
j++;
|
|
138
143
|
}
|
|
139
|
-
if (props.length || hasIntrinsicProps) result.set(name, { props, hasIntrinsicProps });
|
|
144
|
+
if (props.length || hasIntrinsicProps) result.set(name, { props, hasIntrinsicProps, inheritsTag });
|
|
140
145
|
i = Math.max(i + 1, j);
|
|
141
146
|
}
|
|
142
147
|
return result;
|
|
@@ -188,49 +193,186 @@ export function patchUninitializedTypes(ts, service, compiledEntries) {
|
|
|
188
193
|
}
|
|
189
194
|
}
|
|
190
195
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
ts.isIdentifier(stmt.expression.left)) {
|
|
194
|
-
const name = stmt.expression.left.text;
|
|
195
|
-
const sym = uninitialized.get(name);
|
|
196
|
-
if (sym) {
|
|
197
|
-
const rhsType = checker.getTypeAtLocation(stmt.expression.right);
|
|
198
|
-
sym.flags |= ts.SymbolFlags.Transient;
|
|
199
|
-
sym.links = { type: rhsType };
|
|
200
|
-
uninitialized.delete(name);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
// Recurse into function bodies
|
|
196
|
+
patchAssignment(stmt, uninitialized);
|
|
197
|
+
// Recurse into function bodies (fresh scope)
|
|
204
198
|
if (ts.isFunctionDeclaration(stmt) && stmt.body) {
|
|
205
199
|
patchStatements(stmt.body.statements);
|
|
206
200
|
}
|
|
207
201
|
}
|
|
208
202
|
}
|
|
209
203
|
|
|
210
|
-
|
|
204
|
+
// Walk a statement (and nested blocks) looking for first assignments
|
|
205
|
+
// to uninitialized variables. Shares the outer scope's map so that
|
|
206
|
+
// assignments inside if/for/while/try/switch are discovered.
|
|
207
|
+
function patchAssignment(stmt, uninitialized) {
|
|
208
|
+
if (!uninitialized.size) return;
|
|
209
|
+
// Unwrap parenthesized expressions: ({a, b} = ...) → {a, b} = ...
|
|
210
|
+
const unwrap = (e) => ts.isParenthesizedExpression(e) ? unwrap(e.expression) : e;
|
|
211
|
+
if (ts.isExpressionStatement(stmt)) {
|
|
212
|
+
const expr = unwrap(stmt.expression);
|
|
213
|
+
if (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
214
|
+
if (ts.isIdentifier(expr.left)) {
|
|
215
|
+
const name = expr.left.text;
|
|
216
|
+
const sym = uninitialized.get(name);
|
|
217
|
+
if (sym) {
|
|
218
|
+
const rhsType = checker.getTypeAtLocation(expr.right);
|
|
219
|
+
sym.flags |= ts.SymbolFlags.Transient;
|
|
220
|
+
sym.links = { type: rhsType };
|
|
221
|
+
uninitialized.delete(name);
|
|
222
|
+
}
|
|
223
|
+
} else if (ts.isObjectLiteralExpression(expr.left) || ts.isArrayLiteralExpression(expr.left)) {
|
|
224
|
+
// Destructuring assignment: ({a, b} = {a: 1, b: "hello"})
|
|
225
|
+
// Walk the LHS pattern and patch each identifier from the RHS type.
|
|
226
|
+
const rhsType = checker.getTypeAtLocation(expr.right);
|
|
227
|
+
const patchDestructured = (pattern, contextType) => {
|
|
228
|
+
if (ts.isObjectLiteralExpression(pattern)) {
|
|
229
|
+
for (const prop of pattern.properties) {
|
|
230
|
+
if (ts.isShorthandPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
231
|
+
const name = prop.name.text;
|
|
232
|
+
const sym = uninitialized.get(name);
|
|
233
|
+
if (sym) {
|
|
234
|
+
const propSym = contextType.getProperty(name);
|
|
235
|
+
if (propSym) {
|
|
236
|
+
const propType = checker.getTypeOfSymbol(propSym);
|
|
237
|
+
sym.flags |= ts.SymbolFlags.Transient;
|
|
238
|
+
sym.links = { type: propType };
|
|
239
|
+
uninitialized.delete(name);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
} else if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.initializer)) {
|
|
243
|
+
const name = prop.initializer.text;
|
|
244
|
+
const sym = uninitialized.get(name);
|
|
245
|
+
if (sym) {
|
|
246
|
+
const key = ts.isIdentifier(prop.name) ? prop.name.text : undefined;
|
|
247
|
+
const propSym = key && contextType.getProperty(key);
|
|
248
|
+
if (propSym) {
|
|
249
|
+
const propType = checker.getTypeOfSymbol(propSym);
|
|
250
|
+
sym.flags |= ts.SymbolFlags.Transient;
|
|
251
|
+
sym.links = { type: propType };
|
|
252
|
+
uninitialized.delete(name);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} else if (ts.isArrayLiteralExpression(pattern)) {
|
|
258
|
+
const tupleTypes = checker.getTypeArguments(contextType);
|
|
259
|
+
for (let i = 0; i < pattern.elements.length; i++) {
|
|
260
|
+
const el = pattern.elements[i];
|
|
261
|
+
if (ts.isIdentifier(el)) {
|
|
262
|
+
const name = el.text;
|
|
263
|
+
const sym = uninitialized.get(name);
|
|
264
|
+
if (sym && tupleTypes && tupleTypes[i]) {
|
|
265
|
+
sym.flags |= ts.SymbolFlags.Transient;
|
|
266
|
+
sym.links = { type: tupleTypes[i] };
|
|
267
|
+
uninitialized.delete(name);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
patchDestructured(expr.left, rhsType);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Recurse into block-containing statements (but not functions — those get their own scope)
|
|
278
|
+
const walkBlock = (node) => { if (node) { if (ts.isBlock(node)) node.statements.forEach(s => patchAssignment(s, uninitialized)); else patchAssignment(node, uninitialized); } };
|
|
279
|
+
if (ts.isIfStatement(stmt)) { walkBlock(stmt.thenStatement); walkBlock(stmt.elseStatement); }
|
|
280
|
+
if (ts.isForStatement(stmt) || ts.isForInStatement(stmt) || ts.isForOfStatement(stmt) || ts.isWhileStatement(stmt) || ts.isDoStatement(stmt)) walkBlock(stmt.statement);
|
|
281
|
+
if (ts.isTryStatement(stmt)) { walkBlock(stmt.tryBlock); if (stmt.catchClause) walkBlock(stmt.catchClause.block); if (stmt.finallyBlock) walkBlock(stmt.finallyBlock); }
|
|
282
|
+
if (ts.isSwitchStatement(stmt)) { for (const clause of stmt.caseBlock.clauses) clause.statements.forEach(s => patchAssignment(s, uninitialized)); }
|
|
283
|
+
if (ts.isBlock(stmt)) stmt.statements.forEach(s => patchAssignment(s, uninitialized));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
for (const [filePath, entry] of compiledEntries) {
|
|
287
|
+
if (!entry.hasTypes) continue; // @ts-nocheck files skip diagnostics; no need to patch
|
|
211
288
|
const sf = program.getSourceFile(toVirtual(filePath));
|
|
212
289
|
if (!sf) continue;
|
|
213
|
-
patchStatements(sf.statements);
|
|
290
|
+
try { patchStatements(sf.statements); } catch (e) {
|
|
291
|
+
console.warn(`[rip] patchTypes failed for ${filePath}: ${e.message}`);
|
|
292
|
+
}
|
|
214
293
|
}
|
|
215
294
|
}
|
|
216
295
|
|
|
217
|
-
// TS error codes to skip —
|
|
218
|
-
//
|
|
296
|
+
// TS error codes to skip — structural artifacts of Rip's compilation model
|
|
297
|
+
// (DTS coexisting with compiled bodies, overload patterns, etc.)
|
|
219
298
|
export const SKIP_CODES = new Set([
|
|
220
|
-
2300, // Duplicate identifier (DTS declarations coexist with compiled class bodies)
|
|
221
|
-
2307, // Cannot find module
|
|
222
299
|
2389, // Function implementation name must match overload (DTS + compiled body)
|
|
223
300
|
2391, // Function implementation is missing (DTS overload sigs separated from implementations)
|
|
224
301
|
2393, // Duplicate function implementation
|
|
225
302
|
2394, // Overload signature not compatible with implementation (untyped compiled params)
|
|
226
|
-
2451, // Cannot redeclare block-scoped variable
|
|
227
303
|
2567, // Enum declarations can only merge with namespace or other enum (DTS + compiled body)
|
|
228
304
|
2842, // Unused renaming of destructured property (DTS overload has renamed param unused in declaration)
|
|
229
305
|
1064, // Return type of async function must be Promise
|
|
230
|
-
2582, // Cannot find name 'test' (test runner globals)
|
|
231
|
-
2593, // Cannot find name 'describe' (test runner globals)
|
|
232
306
|
]);
|
|
233
307
|
|
|
308
|
+
// Codes that need conditional suppression (not blanket).
|
|
309
|
+
// 2300/2451: Suppress only when one endpoint is in the DTS header (structural).
|
|
310
|
+
// Let through when both endpoints are in the compiled body (real shadowing).
|
|
311
|
+
// 2307: Suppress only for @rip-lang/* and .rip imports (Rip resolves these).
|
|
312
|
+
// Let through for genuinely broken npm/JS imports.
|
|
313
|
+
// 2582/2593: Suppress only in test files (test runner globals).
|
|
314
|
+
// Let through in non-test files so typos like `test(...)` are caught.
|
|
315
|
+
export const CONDITIONAL_CODES = new Set([2300, 2451, 2307, 2582, 2593]);
|
|
316
|
+
|
|
317
|
+
// Shared conditional suppression logic — used by both CLI (runCheck) and LSP
|
|
318
|
+
// (publishDiagnostics). Returns true if the diagnostic should be suppressed.
|
|
319
|
+
//
|
|
320
|
+
// Parameters:
|
|
321
|
+
// code — TS diagnostic code (e.g. 2300, 2451, 2307)
|
|
322
|
+
// start — byte offset in the virtual .ts content
|
|
323
|
+
// length — byte length of the diagnostic span
|
|
324
|
+
// tsContent — the full virtual .ts content
|
|
325
|
+
// headerLines — number of header lines in the virtual .ts
|
|
326
|
+
// dts — the .d.ts content (for identifier checks)
|
|
327
|
+
// flatMessage — flattened diagnostic message string (only needed for 2307)
|
|
328
|
+
// filePath — original .rip file path (only needed for 2582/2593)
|
|
329
|
+
export function shouldSuppressConditional(code, start, length, tsContent, headerLines, dts, flatMessage, filePath) {
|
|
330
|
+
if (code === 2300 || code === 2451) {
|
|
331
|
+
// Duplicate identifier: suppress when one endpoint is in the DTS header.
|
|
332
|
+
const diagLine = offsetToLine(tsContent, start);
|
|
333
|
+
if (diagLine < headerLines) return true; // diagnostic is on the header declaration
|
|
334
|
+
// Body-side: check if the identifier also lives in the DTS header
|
|
335
|
+
const ident = length ? tsContent.substring(start, start + length).trim() : '';
|
|
336
|
+
if (ident && dts) {
|
|
337
|
+
const escaped = ident.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
338
|
+
if (new RegExp('\\b' + escaped + '\\b').test(dts)) return true; // structural — DTS vs body
|
|
339
|
+
}
|
|
340
|
+
return false; // Identifier not in DTS → real shadowing bug
|
|
341
|
+
}
|
|
342
|
+
if (code === 2307) {
|
|
343
|
+
// Cannot find module: suppress only for @rip-lang/* and .rip imports
|
|
344
|
+
const modMatch = flatMessage?.match(/Cannot find module '([^']+)'/);
|
|
345
|
+
if (modMatch) {
|
|
346
|
+
const mod = modMatch[1];
|
|
347
|
+
if (mod.startsWith('@rip-lang/') || mod.endsWith('.rip')) return true;
|
|
348
|
+
}
|
|
349
|
+
return false; // Genuine broken import
|
|
350
|
+
}
|
|
351
|
+
if (code === 2582 || code === 2593) {
|
|
352
|
+
// test/describe globals: suppress only in test files
|
|
353
|
+
if (!filePath) return false;
|
|
354
|
+
const base = filePath.replace(/.*[\/]/, '');
|
|
355
|
+
if (/test|spec/i.test(base)) return true;
|
|
356
|
+
if (/[\/](test|tests|spec|specs|__tests__)[\/]/i.test(filePath)) return true;
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Maximum byte offset to scan for a `# @nocheck` comment at the top of a file.
|
|
363
|
+
// Long shebangs, license headers, or multi-line comments could push the directive
|
|
364
|
+
// further down, but 512 bytes covers typical preambles without scanning entire files.
|
|
365
|
+
export const NOCHECK_SCAN_LIMIT = 512;
|
|
366
|
+
|
|
367
|
+
// Check whether a diagnostic targets a compiler-generated _render() construction
|
|
368
|
+
// variable (_0, _1, …). These typed constants exist solely for prop type-checking
|
|
369
|
+
// and are never read — unused-variable diagnostics on them are spurious.
|
|
370
|
+
export function isRenderConstructionVar(tsContent, start, length) {
|
|
371
|
+
if (!length) return false;
|
|
372
|
+
const span = tsContent.substring(start, start + length);
|
|
373
|
+
return /^_\d+$/.test(span.trim());
|
|
374
|
+
}
|
|
375
|
+
|
|
234
376
|
// Clean diagnostic messages to hide Rip compiler internals from users.
|
|
235
377
|
// Strips Signal<T>/Computed<T> wrappers, __bind_X__ property names, and
|
|
236
378
|
// removes __bind_*__ entries from inline type displays.
|
|
@@ -253,7 +395,12 @@ export function cleanDiagnosticMessage(msg) {
|
|
|
253
395
|
// Rewrite verbose __ripEl tag union mismatch into a clean JSX-like message
|
|
254
396
|
msg = msg.replace(
|
|
255
397
|
/Argument of type '"([\w-]+)"' is not assignable to parameter of type '(?:__RipTag|[^']*\bkeyof HTMLElementTagNameMap\b[^']*)'\./,
|
|
256
|
-
"'$1' is not a known
|
|
398
|
+
"'$1' is not a known element."
|
|
399
|
+
);
|
|
400
|
+
// Rewrite verbose __RipTag constraint error (e.g. component extends unknown element)
|
|
401
|
+
msg = msg.replace(
|
|
402
|
+
/Type '"([\w-]+)"' does not satisfy the constraint '(?:__RipTag|[^']*\bkeyof HTMLElementTagNameMap\b[^']*)'\./,
|
|
403
|
+
"'$1' is not a known element."
|
|
257
404
|
);
|
|
258
405
|
// Deduplicate consecutive identical lines (unwrapping can collapse nested messages)
|
|
259
406
|
msg = msg.split('\n').filter((line, i, arr) => i === 0 || line.trim() !== arr[i - 1].trim()).join('\n');
|
|
@@ -325,15 +472,17 @@ function replaceFnParams(line, newParams) {
|
|
|
325
472
|
// Compile a .rip file for type-checking. Prepends DTS declarations to
|
|
326
473
|
// compiled JS, detects type annotations, and builds bidirectional
|
|
327
474
|
// source maps. Returns everything both the CLI and LSP need.
|
|
328
|
-
|
|
475
|
+
// When opts.strict is true, all non-nocheck files are type-checked.
|
|
476
|
+
export function compileForCheck(filePath, source, compiler, opts = {}) {
|
|
329
477
|
const result = compiler.compile(source, { sourceMap: true, types: 'emit', skipPreamble: true, stubComponents: true });
|
|
330
478
|
let code = result.code || '';
|
|
331
479
|
const dts = result.dts ? result.dts.trimEnd() + '\n' : '';
|
|
332
480
|
|
|
333
481
|
// Determine if this file should be type-checked.
|
|
334
482
|
// A `# @nocheck` comment near the top of the file opts out entirely.
|
|
335
|
-
|
|
336
|
-
const
|
|
483
|
+
// In strict mode, all non-nocheck files are type-checked.
|
|
484
|
+
const nocheck = /^#\s*@nocheck\b/m.test(source.slice(0, NOCHECK_SCAN_LIMIT));
|
|
485
|
+
const hasOwnTypes = !nocheck && (hasTypeAnnotations(source) || !!opts.strict);
|
|
337
486
|
let importsTyped = false;
|
|
338
487
|
if (!hasOwnTypes && !nocheck) {
|
|
339
488
|
const ripImports = [...source.matchAll(/from\s+['"]([^'"]*\.rip)['"]/g)];
|
|
@@ -414,7 +563,12 @@ export function compileForCheck(filePath, source, compiler) {
|
|
|
414
563
|
// First pass: copy typed params AND return types from signatures to
|
|
415
564
|
// implementations. Typed params give TS type info inside function bodies;
|
|
416
565
|
// return types let TS verify the body matches the declared return.
|
|
417
|
-
|
|
566
|
+
// When multiple sigs target the same codeLine (overloads), only apply
|
|
567
|
+
// from the last one — that's the implementation signature whose types
|
|
568
|
+
// should annotate the function body.
|
|
569
|
+
const lastByLine = new Map();
|
|
570
|
+
for (const inj of injections) lastByLine.set(inj.codeLine, inj);
|
|
571
|
+
for (const inj of lastByLine.values()) {
|
|
418
572
|
const sig = inj.sig.replace(/^declare /, '');
|
|
419
573
|
const sigParams = extractFnParams(sig);
|
|
420
574
|
if (sigParams !== null) {
|
|
@@ -512,6 +666,9 @@ export function compileForCheck(filePath, source, compiler) {
|
|
|
512
666
|
if (cl[k].match(/^(?:export\s+)?(?:class|const)\s+\w+/) && k > j + 1) break;
|
|
513
667
|
const fm = cl[k].match(/^\s+(?:declare\s+)?(\w+):\s+.+;$/);
|
|
514
668
|
if (fm) existingFields.add(fm[1]);
|
|
669
|
+
// Also match field assignments (e.g. `name = __computed(...)` in component stubs)
|
|
670
|
+
const am = cl[k].match(/^\s+(\w+)\s*=\s+/);
|
|
671
|
+
if (am) existingFields.add(am[1]);
|
|
515
672
|
if (cl[k].match(/^\s+_init\s*\(/)) break;
|
|
516
673
|
}
|
|
517
674
|
const missingFields = info.fields.filter(f => !existingFields.has(f.name));
|
|
@@ -692,14 +849,14 @@ export function compileForCheck(filePath, source, compiler) {
|
|
|
692
849
|
if (needSignal || needComputed || needEffect) {
|
|
693
850
|
const decls = [];
|
|
694
851
|
if (needSignal) {
|
|
695
|
-
if (!/\binterface Signal\b/.test(headerDts)) decls.push(
|
|
696
|
-
decls.push(
|
|
852
|
+
if (!/\binterface Signal\b/.test(headerDts)) decls.push(SIGNAL_INTERFACE);
|
|
853
|
+
decls.push(SIGNAL_FN);
|
|
697
854
|
}
|
|
698
855
|
if (needComputed) {
|
|
699
|
-
if (!/\binterface Computed\b/.test(headerDts)) decls.push(
|
|
700
|
-
decls.push(
|
|
856
|
+
if (!/\binterface Computed\b/.test(headerDts)) decls.push(COMPUTED_INTERFACE);
|
|
857
|
+
decls.push(COMPUTED_FN);
|
|
701
858
|
}
|
|
702
|
-
if (needEffect) decls.push(
|
|
859
|
+
if (needEffect) decls.push(EFFECT_FN);
|
|
703
860
|
headerDts = decls.join('\n') + '\n' + headerDts;
|
|
704
861
|
}
|
|
705
862
|
}
|
|
@@ -748,42 +905,68 @@ export function compileForCheck(filePath, source, compiler) {
|
|
|
748
905
|
// Inject intrinsic element type declarations for render block type-checking.
|
|
749
906
|
// Uses TypeScript's built-in DOM types (HTMLElementTagNameMap, etc.) as the
|
|
750
907
|
// source of truth for tag names, attribute types, and event handler types.
|
|
908
|
+
// Skip type aliases if the DTS already includes them (types.js prepends for component files).
|
|
751
909
|
if (hasTypes && (/\b__ripEl\b/.test(code) || /\b__RipProps\b/.test(headerDts))) {
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
"type __RipBrowserElement = Omit<HTMLElement, 'querySelector' | 'querySelectorAll' | 'closest' | 'setAttribute' | 'hidden'> & { hidden: boolean | 'until-found'; setAttribute(qualifiedName: string, value: any): void; querySelector(selectors: string): __RipBrowserElement | null; querySelectorAll(selectors: string): NodeListOf<__RipBrowserElement>; closest(selectors: string): __RipBrowserElement | null; };",
|
|
756
|
-
"type __RipDomEl<K extends __RipTag> = Omit<__RipElementMap[K], 'querySelector' | 'querySelectorAll' | 'closest' | 'setAttribute' | 'hidden'> & __RipBrowserElement;",
|
|
757
|
-
"type __RipAttrKeys<T> = { [K in keyof T]-?: K extends 'style' ? never : T[K] extends (...args: any[]) => any ? never : K }[keyof T] & string;",
|
|
758
|
-
'type __RipEvents = { [K in keyof HTMLElementEventMap as `@${K}`]?: ((event: HTMLElementEventMap[K]) => void) | null };',
|
|
759
|
-
'type __RipClassValue = string | boolean | null | undefined | Record<string, boolean> | __RipClassValue[];',
|
|
760
|
-
'type __RipProps<K extends __RipTag> = { [P in __RipAttrKeys<__RipElementMap[K]>]?: __RipElementMap[K][P] } & __RipEvents & { ref?: string; class?: __RipClassValue | __RipClassValue[]; style?: string; [k: `data-${string}`]: any; [k: `aria-${string}`]: any };',
|
|
761
|
-
'declare function __ripEl<K extends __RipTag>(tag: K, props?: __RipProps<K>): void;',
|
|
762
|
-
];
|
|
763
|
-
headerDts = intrinsicDecls.join('\n') + '\n' + headerDts;
|
|
910
|
+
const alreadyHasTypes = /\btype __RipElementMap\b/.test(headerDts);
|
|
911
|
+
const parts = alreadyHasTypes ? [INTRINSIC_FN_DECL] : [...INTRINSIC_TYPE_DECLS, INTRINSIC_FN_DECL];
|
|
912
|
+
headerDts = parts.join('\n') + '\n' + headerDts;
|
|
764
913
|
}
|
|
765
914
|
|
|
766
915
|
if (hasTypes && /\bARIA\./.test(code) && !/\bdeclare const ARIA\b/.test(headerDts)) {
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
916
|
+
headerDts = ARIA_TYPE_DECLS.join('\n') + '\n' + headerDts;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// 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.
|
|
925
|
+
if (hasTypes && code) {
|
|
926
|
+
const cl = code.split('\n');
|
|
927
|
+
for (let i = 0; i < cl.length; i++) {
|
|
928
|
+
const m = cl[i].match(/^(\s*)let\s+([A-Za-z_$][\w$]*(?:\s*,\s*[A-Za-z_$][\w$]*)*)\s*;\s*$/);
|
|
929
|
+
if (!m) continue;
|
|
930
|
+
// Only process hoist-position lets (first non-blank line after `{` or start of file)
|
|
931
|
+
let prev = null;
|
|
932
|
+
for (let k = i - 1; k >= 0; k--) { if (cl[k].trim() !== '') { prev = cl[k]; break; } }
|
|
933
|
+
if (prev !== null && !/\{\s*$/.test(prev)) continue;
|
|
934
|
+
|
|
935
|
+
const indent = m[1];
|
|
936
|
+
const vars = m[2].split(/\s*,\s*/);
|
|
937
|
+
const inlined = new Set();
|
|
938
|
+
const bailed = new Set();
|
|
939
|
+
const reEsc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
940
|
+
|
|
941
|
+
for (let j = i + 1; j < cl.length; j++) {
|
|
942
|
+
const line = cl[j];
|
|
943
|
+
if (line.trim() === '') continue;
|
|
944
|
+
// End of scope
|
|
945
|
+
if (new RegExp('^' + reEsc(indent) + '}').test(line)) break;
|
|
946
|
+
// 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
|
+
|
|
951
|
+
for (const v of vars) {
|
|
952
|
+
if (inlined.has(v) || bailed.has(v)) continue;
|
|
953
|
+
const ve = reEsc(v);
|
|
954
|
+
const assignRe = new RegExp('^' + reEsc(indent) + ve + '\\s*=\\s*(.*);\\s*$');
|
|
955
|
+
const assign = line.match(assignRe);
|
|
956
|
+
if (assign) {
|
|
957
|
+
cl[j] = `${indent}let ${v} = ${assign[1]};`;
|
|
958
|
+
inlined.add(v);
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
if (new RegExp('\\b' + ve + '\\b').test(line)) bailed.add(v);
|
|
962
|
+
}
|
|
963
|
+
if (vars.every(v => inlined.has(v) || bailed.has(v))) break;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const remaining = vars.filter(v => !inlined.has(v));
|
|
967
|
+
cl[i] = remaining.length ? `${indent}let ${remaining.join(', ')};` : '';
|
|
968
|
+
}
|
|
969
|
+
code = cl.filter(l => l !== '').join('\n');
|
|
787
970
|
}
|
|
788
971
|
|
|
789
972
|
let tsContent = (hasTypes ? headerDts + '\n' : '') + code;
|
|
@@ -805,6 +988,20 @@ export function compileForCheck(filePath, source, compiler) {
|
|
|
805
988
|
for (let i = 0; i < dtsLines.length; i++) {
|
|
806
989
|
const line = dtsLines[i];
|
|
807
990
|
|
|
991
|
+
// Map import lines by module path (from "..." or from '...')
|
|
992
|
+
const importMatch = line.match(/^import\s+.+\s+from\s+['"]([^'"]+)['"]/);
|
|
993
|
+
if (importMatch) {
|
|
994
|
+
const modulePath = importMatch[1];
|
|
995
|
+
for (let s = 0; s < srcLines.length; s++) {
|
|
996
|
+
if (srcLines[s].includes(modulePath) && /^import\s/.test(srcLines[s].trimStart())) {
|
|
997
|
+
genToSrc.set(i, s);
|
|
998
|
+
srcToGen.set(s, i);
|
|
999
|
+
break;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
808
1005
|
const m = line.match(/^(?:export\s+)?(?:declare\s+)?(?:let|var|type|interface|enum|class)\s+(\w+)/);
|
|
809
1006
|
if (!m) continue;
|
|
810
1007
|
const name = m[1];
|
|
@@ -935,13 +1132,73 @@ export function mapToSource(entry, offset) {
|
|
|
935
1132
|
return tsLine - entry.headerLines;
|
|
936
1133
|
}
|
|
937
1134
|
|
|
1135
|
+
// ── Project config ─────────────────────────────────────────────────
|
|
1136
|
+
|
|
1137
|
+
// Read project config: rip.json in the given directory, or "rip" key in
|
|
1138
|
+
// the nearest ancestor package.json. Returns { strict, exclude }.
|
|
1139
|
+
export function readProjectConfig(dir) {
|
|
1140
|
+
const config = {};
|
|
1141
|
+
try {
|
|
1142
|
+
let d = resolve(dir);
|
|
1143
|
+
while (true) {
|
|
1144
|
+
const ripJsonPath = resolve(d, 'rip.json');
|
|
1145
|
+
if (existsSync(ripJsonPath)) {
|
|
1146
|
+
Object.assign(config, JSON.parse(readFileSync(ripJsonPath, 'utf8')));
|
|
1147
|
+
config._configDir = d;
|
|
1148
|
+
break;
|
|
1149
|
+
}
|
|
1150
|
+
const pkgPath = resolve(d, 'package.json');
|
|
1151
|
+
if (existsSync(pkgPath)) {
|
|
1152
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
1153
|
+
if (pkg.rip && typeof pkg.rip === 'object') { Object.assign(config, pkg.rip); config._configDir = d; break; }
|
|
1154
|
+
}
|
|
1155
|
+
const parent = dirname(d);
|
|
1156
|
+
if (parent === d) break;
|
|
1157
|
+
d = parent;
|
|
1158
|
+
}
|
|
1159
|
+
} catch {}
|
|
1160
|
+
return config;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
938
1163
|
// ── CLI batch type-checker ─────────────────────────────────────────
|
|
939
1164
|
|
|
940
|
-
|
|
1165
|
+
// Convert a simple glob pattern to a RegExp for matching relative paths.
|
|
1166
|
+
// Supports: ** (any path segments), * (any within segment), ? (single char).
|
|
1167
|
+
export function globToRegex(pattern) {
|
|
1168
|
+
let re = '';
|
|
1169
|
+
let i = 0;
|
|
1170
|
+
while (i < pattern.length) {
|
|
1171
|
+
const c = pattern[i];
|
|
1172
|
+
if (c === '*' && pattern[i + 1] === '*') {
|
|
1173
|
+
re += '.*';
|
|
1174
|
+
i += 2;
|
|
1175
|
+
if (pattern[i] === '/') i++; // skip trailing slash after **
|
|
1176
|
+
} else if (c === '*') {
|
|
1177
|
+
re += '[^/]*';
|
|
1178
|
+
i++;
|
|
1179
|
+
} else if (c === '?') {
|
|
1180
|
+
re += '[^/]';
|
|
1181
|
+
i++;
|
|
1182
|
+
} else if (c === '.') {
|
|
1183
|
+
re += '\\.';
|
|
1184
|
+
i++;
|
|
1185
|
+
} else {
|
|
1186
|
+
re += c;
|
|
1187
|
+
i++;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
return new RegExp('^' + re + '$');
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function findRipFiles(dir, files = [], excludePatterns = [], rootDir = dir) {
|
|
941
1194
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
942
1195
|
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
943
1196
|
const full = resolve(dir, entry.name);
|
|
944
|
-
if (
|
|
1197
|
+
if (excludePatterns.length > 0) {
|
|
1198
|
+
const rel = relative(rootDir, full);
|
|
1199
|
+
if (excludePatterns.some(p => p.test(rel))) continue;
|
|
1200
|
+
}
|
|
1201
|
+
if (entry.isDirectory()) findRipFiles(full, files, excludePatterns, rootDir);
|
|
945
1202
|
else if (entry.name.endsWith('.rip')) files.push(full);
|
|
946
1203
|
}
|
|
947
1204
|
return files;
|
|
@@ -973,23 +1230,31 @@ export async function runCheck(targetDir, opts = {}) {
|
|
|
973
1230
|
return 1;
|
|
974
1231
|
}
|
|
975
1232
|
|
|
976
|
-
const
|
|
1233
|
+
const ripConfig = readProjectConfig(rootPath);
|
|
1234
|
+
|
|
1235
|
+
// Merge: CLI flags override config file
|
|
1236
|
+
const strict = opts.strict || ripConfig.strict === true;
|
|
1237
|
+
const excludeGlobs = Array.isArray(ripConfig.exclude) ? ripConfig.exclude : [];
|
|
1238
|
+
const excludePatterns = excludeGlobs.map(globToRegex);
|
|
1239
|
+
|
|
1240
|
+
const allFiles = findRipFiles(rootPath, [], excludePatterns);
|
|
977
1241
|
if (allFiles.length === 0) {
|
|
978
1242
|
console.error(red(`No .rip files found in ${targetDir}`));
|
|
979
1243
|
return 1;
|
|
980
1244
|
}
|
|
981
1245
|
|
|
982
1246
|
// Pre-scan: only compile files that have type annotations or are imported by typed files.
|
|
983
|
-
//
|
|
1247
|
+
// In strict mode, all non-nocheck files are type-checked.
|
|
984
1248
|
const typedFiles = new Set();
|
|
985
1249
|
const sourcesByPath = new Map();
|
|
986
1250
|
for (const fp of allFiles) {
|
|
987
1251
|
const source = readFileSync(fp, 'utf8');
|
|
988
1252
|
sourcesByPath.set(fp, source);
|
|
989
|
-
const nocheck = /^#\s*@nocheck\b/m.test(source.slice(0,
|
|
990
|
-
if (!nocheck && hasTypeAnnotations(source)) typedFiles.add(fp);
|
|
1253
|
+
const nocheck = /^#\s*@nocheck\b/m.test(source.slice(0, NOCHECK_SCAN_LIMIT));
|
|
1254
|
+
if (!nocheck && (hasTypeAnnotations(source) || strict)) typedFiles.add(fp);
|
|
991
1255
|
}
|
|
992
|
-
|
|
1256
|
+
|
|
1257
|
+
// Include imports of typed files (files imported BY typed files)
|
|
993
1258
|
for (const fp of typedFiles) {
|
|
994
1259
|
const source = sourcesByPath.get(fp);
|
|
995
1260
|
const ripImports = [...source.matchAll(/from\s+['"]([^'"]*\.rip)['"]/g)];
|
|
@@ -1003,6 +1268,17 @@ export async function runCheck(targetDir, opts = {}) {
|
|
|
1003
1268
|
}
|
|
1004
1269
|
}
|
|
1005
1270
|
}
|
|
1271
|
+
// Include files that import FROM typed files (consumers of typed modules)
|
|
1272
|
+
for (const [fp, source] of sourcesByPath) {
|
|
1273
|
+
if (typedFiles.has(fp)) continue;
|
|
1274
|
+
const nocheck = /^#\s*@nocheck\b/m.test(source.slice(0, NOCHECK_SCAN_LIMIT));
|
|
1275
|
+
if (nocheck) continue;
|
|
1276
|
+
const ripImports = [...source.matchAll(/from\s+['"]([^'"]*\.rip)['"]/g)];
|
|
1277
|
+
for (const m of ripImports) {
|
|
1278
|
+
const imported = resolve(dirname(fp), m[1]);
|
|
1279
|
+
if (typedFiles.has(imported)) { typedFiles.add(fp); break; }
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1006
1282
|
|
|
1007
1283
|
// Compile only typed files (and their imports)
|
|
1008
1284
|
const compiled = new Map();
|
|
@@ -1011,7 +1287,7 @@ export async function runCheck(targetDir, opts = {}) {
|
|
|
1011
1287
|
for (const fp of typedFiles) {
|
|
1012
1288
|
try {
|
|
1013
1289
|
const source = sourcesByPath.get(fp);
|
|
1014
|
-
compiled.set(fp, compileForCheck(fp, source, new Compiler()));
|
|
1290
|
+
compiled.set(fp, compileForCheck(fp, source, new Compiler(), { strict }));
|
|
1015
1291
|
} catch (e) {
|
|
1016
1292
|
compileErrors++;
|
|
1017
1293
|
const rel = relative(rootPath, fp);
|
|
@@ -1120,12 +1396,15 @@ export async function runCheck(targetDir, opts = {}) {
|
|
|
1120
1396
|
if (d.start === undefined) continue;
|
|
1121
1397
|
if (SKIP_CODES.has(d.code)) continue;
|
|
1122
1398
|
|
|
1123
|
-
//
|
|
1124
|
-
if ((d.code
|
|
1125
|
-
const
|
|
1126
|
-
if (
|
|
1399
|
+
// Conditional suppression — narrowed instead of blanket
|
|
1400
|
+
if (CONDITIONAL_CODES.has(d.code)) {
|
|
1401
|
+
const flatMsg = d.code === 2307 ? ts.flattenDiagnosticMessageText(d.messageText, '\n') : null;
|
|
1402
|
+
if (shouldSuppressConditional(d.code, d.start, d.length, entry.tsContent, entry.headerLines, entry.dts, flatMsg, fp)) continue;
|
|
1127
1403
|
}
|
|
1128
1404
|
|
|
1405
|
+
// Skip 6133 on compiler-generated _render() construction variables (_0, _1, …)
|
|
1406
|
+
if ((d.code === 6133 || d.code === 6196) && isRenderConstructionVar(entry.tsContent, d.start, d.length)) continue;
|
|
1407
|
+
|
|
1129
1408
|
// Skip diagnostics on injected overload signatures — the real function
|
|
1130
1409
|
// definition already carries the same diagnostic.
|
|
1131
1410
|
if (isInjectedOverload(entry, d.start)) continue;
|
|
@@ -1133,6 +1412,10 @@ export async function runCheck(targetDir, opts = {}) {
|
|
|
1133
1412
|
const pos = mapToSourcePos(entry, d.start);
|
|
1134
1413
|
if (!pos) continue;
|
|
1135
1414
|
|
|
1415
|
+
// Drop diagnostics that map beyond the source file (e.g. from component
|
|
1416
|
+
// stubs where the compiled line has no real source counterpart).
|
|
1417
|
+
if (pos.line >= srcLines.length) continue;
|
|
1418
|
+
|
|
1136
1419
|
// Remap IIFE-switch diagnostics to the enclosing function declaration
|
|
1137
1420
|
const adj = adjustSwitchDiagnostic(entry.source, pos, d.code);
|
|
1138
1421
|
if (adj) { pos.line = adj.line; pos.col = adj.col; }
|
|
@@ -1285,7 +1568,9 @@ export async function runCheck(targetDir, opts = {}) {
|
|
|
1285
1568
|
console.log('');
|
|
1286
1569
|
const lineNum = String(e.line);
|
|
1287
1570
|
console.log(`${lineNum} ${e.srcLine}`);
|
|
1288
|
-
|
|
1571
|
+
const pad = Math.max(0, e.col - 1);
|
|
1572
|
+
const underline = Math.max(1, e.len);
|
|
1573
|
+
console.log(`${' '.repeat(lineNum.length)} ${' '.repeat(pad)}${red('~'.repeat(underline))}`);
|
|
1289
1574
|
}
|
|
1290
1575
|
|
|
1291
1576
|
if (e.related) {
|