rip-lang 3.13.133 → 3.13.135
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 +2294 -1544
- package/docs/dist/rip.min.js +202 -192
- package/docs/dist/rip.min.js.br +0 -0
- package/package.json +1 -1
- package/rip-loader.js +2 -2
- package/src/AGENTS.md +76 -11
- package/src/browser.js +5 -5
- package/src/compiler.js +961 -639
- package/src/components.js +274 -109
- 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 +312 -80
- package/src/types.js +229 -54
- 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,15 @@ 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
|
-
'type __RipAriaNavHandlers = { next?: () => void; prev?: () => void; first?: () => void; last?: () => void; select?: () => void; dismiss?: () => void; tab?: () => void; char?: () => void; };',
|
|
769
|
-
"declare const ARIA: {",
|
|
770
|
-
" bindPopover(open: boolean, popover: () => Element | null | undefined, setOpen: (isOpen: boolean) => void, source?: (() => Element | null | undefined) | null): void;",
|
|
771
|
-
" bindDialog(open: boolean, dialog: () => Element | null | undefined, setOpen: (isOpen: boolean) => void, dismissable?: boolean): void;",
|
|
772
|
-
" popupDismiss(open: boolean, popup: () => Element | null | undefined, close: () => void, els?: Array<() => Element | null | undefined>, repos?: (() => void) | null): void;",
|
|
773
|
-
" popupGuard(delay?: number): any;",
|
|
774
|
-
" listNav(event: KeyboardEvent, handlers: __RipAriaNavHandlers): void;",
|
|
775
|
-
" rovingNav(event: KeyboardEvent, handlers: __RipAriaNavHandlers, orientation?: 'vertical' | 'horizontal' | 'both'): void;",
|
|
776
|
-
" positionBelow(trigger: Element | null | undefined, popup: Element | null | undefined, gap?: number, setVisible?: boolean): void;",
|
|
777
|
-
" position(trigger: Element | null | undefined, floating: Element | null | undefined, opts?: any): void;",
|
|
778
|
-
" trapFocus(panel: Element | null | undefined): void;",
|
|
779
|
-
" wireAria(panel: Element, id: string): void;",
|
|
780
|
-
" lockScroll(instance: any): void;",
|
|
781
|
-
" unlockScroll(instance: any): void;",
|
|
782
|
-
" hasAnchor: boolean;",
|
|
783
|
-
" [key: string]: any;",
|
|
784
|
-
"};",
|
|
785
|
-
];
|
|
786
|
-
headerDts = ariaDecls.join('\n') + '\n' + headerDts;
|
|
916
|
+
headerDts = ARIA_TYPE_DECLS.join('\n') + '\n' + headerDts;
|
|
787
917
|
}
|
|
788
918
|
|
|
789
919
|
let tsContent = (hasTypes ? headerDts + '\n' : '') + code;
|
|
@@ -805,6 +935,20 @@ export function compileForCheck(filePath, source, compiler) {
|
|
|
805
935
|
for (let i = 0; i < dtsLines.length; i++) {
|
|
806
936
|
const line = dtsLines[i];
|
|
807
937
|
|
|
938
|
+
// Map import lines by module path (from "..." or from '...')
|
|
939
|
+
const importMatch = line.match(/^import\s+.+\s+from\s+['"]([^'"]+)['"]/);
|
|
940
|
+
if (importMatch) {
|
|
941
|
+
const modulePath = importMatch[1];
|
|
942
|
+
for (let s = 0; s < srcLines.length; s++) {
|
|
943
|
+
if (srcLines[s].includes(modulePath) && /^import\s/.test(srcLines[s].trimStart())) {
|
|
944
|
+
genToSrc.set(i, s);
|
|
945
|
+
srcToGen.set(s, i);
|
|
946
|
+
break;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
|
|
808
952
|
const m = line.match(/^(?:export\s+)?(?:declare\s+)?(?:let|var|type|interface|enum|class)\s+(\w+)/);
|
|
809
953
|
if (!m) continue;
|
|
810
954
|
const name = m[1];
|
|
@@ -935,13 +1079,73 @@ export function mapToSource(entry, offset) {
|
|
|
935
1079
|
return tsLine - entry.headerLines;
|
|
936
1080
|
}
|
|
937
1081
|
|
|
1082
|
+
// ── Project config ─────────────────────────────────────────────────
|
|
1083
|
+
|
|
1084
|
+
// Read project config: rip.json in the given directory, or "rip" key in
|
|
1085
|
+
// the nearest ancestor package.json. Returns { strict, exclude }.
|
|
1086
|
+
export function readProjectConfig(dir) {
|
|
1087
|
+
const config = {};
|
|
1088
|
+
try {
|
|
1089
|
+
let d = resolve(dir);
|
|
1090
|
+
while (true) {
|
|
1091
|
+
const ripJsonPath = resolve(d, 'rip.json');
|
|
1092
|
+
if (existsSync(ripJsonPath)) {
|
|
1093
|
+
Object.assign(config, JSON.parse(readFileSync(ripJsonPath, 'utf8')));
|
|
1094
|
+
config._configDir = d;
|
|
1095
|
+
break;
|
|
1096
|
+
}
|
|
1097
|
+
const pkgPath = resolve(d, 'package.json');
|
|
1098
|
+
if (existsSync(pkgPath)) {
|
|
1099
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
1100
|
+
if (pkg.rip && typeof pkg.rip === 'object') { Object.assign(config, pkg.rip); config._configDir = d; break; }
|
|
1101
|
+
}
|
|
1102
|
+
const parent = dirname(d);
|
|
1103
|
+
if (parent === d) break;
|
|
1104
|
+
d = parent;
|
|
1105
|
+
}
|
|
1106
|
+
} catch {}
|
|
1107
|
+
return config;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
938
1110
|
// ── CLI batch type-checker ─────────────────────────────────────────
|
|
939
1111
|
|
|
940
|
-
|
|
1112
|
+
// Convert a simple glob pattern to a RegExp for matching relative paths.
|
|
1113
|
+
// Supports: ** (any path segments), * (any within segment), ? (single char).
|
|
1114
|
+
export function globToRegex(pattern) {
|
|
1115
|
+
let re = '';
|
|
1116
|
+
let i = 0;
|
|
1117
|
+
while (i < pattern.length) {
|
|
1118
|
+
const c = pattern[i];
|
|
1119
|
+
if (c === '*' && pattern[i + 1] === '*') {
|
|
1120
|
+
re += '.*';
|
|
1121
|
+
i += 2;
|
|
1122
|
+
if (pattern[i] === '/') i++; // skip trailing slash after **
|
|
1123
|
+
} else if (c === '*') {
|
|
1124
|
+
re += '[^/]*';
|
|
1125
|
+
i++;
|
|
1126
|
+
} else if (c === '?') {
|
|
1127
|
+
re += '[^/]';
|
|
1128
|
+
i++;
|
|
1129
|
+
} else if (c === '.') {
|
|
1130
|
+
re += '\\.';
|
|
1131
|
+
i++;
|
|
1132
|
+
} else {
|
|
1133
|
+
re += c;
|
|
1134
|
+
i++;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
return new RegExp('^' + re + '$');
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function findRipFiles(dir, files = [], excludePatterns = [], rootDir = dir) {
|
|
941
1141
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
942
1142
|
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
943
1143
|
const full = resolve(dir, entry.name);
|
|
944
|
-
if (
|
|
1144
|
+
if (excludePatterns.length > 0) {
|
|
1145
|
+
const rel = relative(rootDir, full);
|
|
1146
|
+
if (excludePatterns.some(p => p.test(rel))) continue;
|
|
1147
|
+
}
|
|
1148
|
+
if (entry.isDirectory()) findRipFiles(full, files, excludePatterns, rootDir);
|
|
945
1149
|
else if (entry.name.endsWith('.rip')) files.push(full);
|
|
946
1150
|
}
|
|
947
1151
|
return files;
|
|
@@ -973,23 +1177,31 @@ export async function runCheck(targetDir, opts = {}) {
|
|
|
973
1177
|
return 1;
|
|
974
1178
|
}
|
|
975
1179
|
|
|
976
|
-
const
|
|
1180
|
+
const ripConfig = readProjectConfig(rootPath);
|
|
1181
|
+
|
|
1182
|
+
// Merge: CLI flags override config file
|
|
1183
|
+
const strict = opts.strict || ripConfig.strict === true;
|
|
1184
|
+
const excludeGlobs = Array.isArray(ripConfig.exclude) ? ripConfig.exclude : [];
|
|
1185
|
+
const excludePatterns = excludeGlobs.map(globToRegex);
|
|
1186
|
+
|
|
1187
|
+
const allFiles = findRipFiles(rootPath, [], excludePatterns);
|
|
977
1188
|
if (allFiles.length === 0) {
|
|
978
1189
|
console.error(red(`No .rip files found in ${targetDir}`));
|
|
979
1190
|
return 1;
|
|
980
1191
|
}
|
|
981
1192
|
|
|
982
1193
|
// Pre-scan: only compile files that have type annotations or are imported by typed files.
|
|
983
|
-
//
|
|
1194
|
+
// In strict mode, all non-nocheck files are type-checked.
|
|
984
1195
|
const typedFiles = new Set();
|
|
985
1196
|
const sourcesByPath = new Map();
|
|
986
1197
|
for (const fp of allFiles) {
|
|
987
1198
|
const source = readFileSync(fp, 'utf8');
|
|
988
1199
|
sourcesByPath.set(fp, source);
|
|
989
|
-
const nocheck = /^#\s*@nocheck\b/m.test(source.slice(0,
|
|
990
|
-
if (!nocheck && hasTypeAnnotations(source)) typedFiles.add(fp);
|
|
1200
|
+
const nocheck = /^#\s*@nocheck\b/m.test(source.slice(0, NOCHECK_SCAN_LIMIT));
|
|
1201
|
+
if (!nocheck && (hasTypeAnnotations(source) || strict)) typedFiles.add(fp);
|
|
991
1202
|
}
|
|
992
|
-
|
|
1203
|
+
|
|
1204
|
+
// Include imports of typed files (files imported BY typed files)
|
|
993
1205
|
for (const fp of typedFiles) {
|
|
994
1206
|
const source = sourcesByPath.get(fp);
|
|
995
1207
|
const ripImports = [...source.matchAll(/from\s+['"]([^'"]*\.rip)['"]/g)];
|
|
@@ -1003,6 +1215,17 @@ export async function runCheck(targetDir, opts = {}) {
|
|
|
1003
1215
|
}
|
|
1004
1216
|
}
|
|
1005
1217
|
}
|
|
1218
|
+
// Include files that import FROM typed files (consumers of typed modules)
|
|
1219
|
+
for (const [fp, source] of sourcesByPath) {
|
|
1220
|
+
if (typedFiles.has(fp)) continue;
|
|
1221
|
+
const nocheck = /^#\s*@nocheck\b/m.test(source.slice(0, NOCHECK_SCAN_LIMIT));
|
|
1222
|
+
if (nocheck) continue;
|
|
1223
|
+
const ripImports = [...source.matchAll(/from\s+['"]([^'"]*\.rip)['"]/g)];
|
|
1224
|
+
for (const m of ripImports) {
|
|
1225
|
+
const imported = resolve(dirname(fp), m[1]);
|
|
1226
|
+
if (typedFiles.has(imported)) { typedFiles.add(fp); break; }
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1006
1229
|
|
|
1007
1230
|
// Compile only typed files (and their imports)
|
|
1008
1231
|
const compiled = new Map();
|
|
@@ -1011,7 +1234,7 @@ export async function runCheck(targetDir, opts = {}) {
|
|
|
1011
1234
|
for (const fp of typedFiles) {
|
|
1012
1235
|
try {
|
|
1013
1236
|
const source = sourcesByPath.get(fp);
|
|
1014
|
-
compiled.set(fp, compileForCheck(fp, source, new Compiler()));
|
|
1237
|
+
compiled.set(fp, compileForCheck(fp, source, new Compiler(), { strict }));
|
|
1015
1238
|
} catch (e) {
|
|
1016
1239
|
compileErrors++;
|
|
1017
1240
|
const rel = relative(rootPath, fp);
|
|
@@ -1120,12 +1343,15 @@ export async function runCheck(targetDir, opts = {}) {
|
|
|
1120
1343
|
if (d.start === undefined) continue;
|
|
1121
1344
|
if (SKIP_CODES.has(d.code)) continue;
|
|
1122
1345
|
|
|
1123
|
-
//
|
|
1124
|
-
if ((d.code
|
|
1125
|
-
const
|
|
1126
|
-
if (
|
|
1346
|
+
// Conditional suppression — narrowed instead of blanket
|
|
1347
|
+
if (CONDITIONAL_CODES.has(d.code)) {
|
|
1348
|
+
const flatMsg = d.code === 2307 ? ts.flattenDiagnosticMessageText(d.messageText, '\n') : null;
|
|
1349
|
+
if (shouldSuppressConditional(d.code, d.start, d.length, entry.tsContent, entry.headerLines, entry.dts, flatMsg, fp)) continue;
|
|
1127
1350
|
}
|
|
1128
1351
|
|
|
1352
|
+
// Skip 6133 on compiler-generated _render() construction variables (_0, _1, …)
|
|
1353
|
+
if ((d.code === 6133 || d.code === 6196) && isRenderConstructionVar(entry.tsContent, d.start, d.length)) continue;
|
|
1354
|
+
|
|
1129
1355
|
// Skip diagnostics on injected overload signatures — the real function
|
|
1130
1356
|
// definition already carries the same diagnostic.
|
|
1131
1357
|
if (isInjectedOverload(entry, d.start)) continue;
|
|
@@ -1133,6 +1359,10 @@ export async function runCheck(targetDir, opts = {}) {
|
|
|
1133
1359
|
const pos = mapToSourcePos(entry, d.start);
|
|
1134
1360
|
if (!pos) continue;
|
|
1135
1361
|
|
|
1362
|
+
// Drop diagnostics that map beyond the source file (e.g. from component
|
|
1363
|
+
// stubs where the compiled line has no real source counterpart).
|
|
1364
|
+
if (pos.line >= srcLines.length) continue;
|
|
1365
|
+
|
|
1136
1366
|
// Remap IIFE-switch diagnostics to the enclosing function declaration
|
|
1137
1367
|
const adj = adjustSwitchDiagnostic(entry.source, pos, d.code);
|
|
1138
1368
|
if (adj) { pos.line = adj.line; pos.col = adj.col; }
|
|
@@ -1285,7 +1515,9 @@ export async function runCheck(targetDir, opts = {}) {
|
|
|
1285
1515
|
console.log('');
|
|
1286
1516
|
const lineNum = String(e.line);
|
|
1287
1517
|
console.log(`${lineNum} ${e.srcLine}`);
|
|
1288
|
-
|
|
1518
|
+
const pad = Math.max(0, e.col - 1);
|
|
1519
|
+
const underline = Math.max(1, e.len);
|
|
1520
|
+
console.log(`${' '.repeat(lineNum.length)} ${' '.repeat(pad)}${red('~'.repeat(underline))}`);
|
|
1289
1521
|
}
|
|
1290
1522
|
|
|
1291
1523
|
if (e.related) {
|