rip-lang 3.9.2 → 3.10.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.
@@ -0,0 +1,367 @@
1
+ // Shared type-checking infrastructure for Rip
2
+ //
3
+ // Used by both the CLI type-checker (bin/rip check) and the
4
+ // VS Code language server (packages/vscode/src/lsp.js).
5
+ //
6
+ // compileForCheck() — the shared compilation pipeline that transforms
7
+ // .rip source into TypeScript content suitable for type-checking.
8
+ //
9
+ // runCheck() — the CLI batch type-checker that compiles all .rip files
10
+ // in a directory, creates a TypeScript language service, and reports
11
+ // type errors mapped back to Rip source positions.
12
+
13
+ import { Compiler } from './compiler.js';
14
+ import { readFileSync, existsSync, readdirSync } from 'fs';
15
+ import { resolve, relative, dirname } from 'path';
16
+ import { buildLineMap } from './sourcemaps.js';
17
+
18
+ // ── Shared helpers ─────────────────────────────────────────────────
19
+
20
+ // Detect type annotations (:: followed by space or =) ignoring comments
21
+ // and prototype syntax (Class::method).
22
+ export function hasTypeAnnotations(source) {
23
+ return source.split('\n').some(line => /::[ \t=]/.test(line.replace(/#.*$/, '')));
24
+ }
25
+
26
+ export function countLines(str) {
27
+ let n = 0;
28
+ for (let i = 0; i < str.length; i++) if (str[i] === '\n') n++;
29
+ return n;
30
+ }
31
+
32
+ export function toVirtual(p) { return p + '.ts'; }
33
+ export function fromVirtual(p) { return p.endsWith('.rip.ts') ? p.slice(0, -3) : p; }
34
+
35
+ // TS error codes to skip — Rip resolves modules differently and
36
+ // treats async return types transparently.
37
+ export const SKIP_CODES = new Set([
38
+ 2307, // Cannot find module
39
+ 2304, // Cannot find name
40
+ 1064, // Return type of async function must be Promise
41
+ 2582, // Cannot find name 'test' (test runner globals)
42
+ 2593, // Cannot find name 'describe' (test runner globals)
43
+ ]);
44
+
45
+ // Base TypeScript compiler settings for type-checking. Callers can
46
+ // pass overrides (e.g. { noImplicitAny: true } for the CLI).
47
+ export function createTypeCheckSettings(ts, overrides = {}) {
48
+ return {
49
+ target: ts.ScriptTarget.ESNext,
50
+ module: ts.ModuleKind.ESNext,
51
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
52
+ allowJs: true,
53
+ strict: false,
54
+ strictNullChecks: true,
55
+ noEmit: true,
56
+ skipLibCheck: true,
57
+ ...overrides,
58
+ };
59
+ }
60
+
61
+ // ── Shared compilation pipeline ────────────────────────────────────
62
+
63
+ // Compile a .rip file for type-checking. Merges .d.ts declarations into
64
+ // the compiled JS, detects type annotations, and builds bidirectional
65
+ // source maps. Returns everything both the CLI and LSP need.
66
+ export function compileForCheck(filePath, source, compiler) {
67
+ const result = compiler.compile(source, { sourceMap: true, types: true });
68
+ let code = result.code || '';
69
+ let dts = result.dts ? result.dts.trimEnd() + '\n' : '';
70
+
71
+ // Strip .d.ts imports — compiled JS already has them
72
+ dts = dts.replace(/^import\s.*;\s*\n/gm, '');
73
+
74
+ // Extract well-formed function signatures and merge into JS.
75
+ // Leaving them as bare declarations causes TypeScript to treat
76
+ // them as overload signatures that conflict with the implementations.
77
+ const funcSigs = new Map();
78
+ dts = dts.replace(
79
+ /^(?:export|declare)\s+function\s+(\w+)\(([^)]*)\):\s*(.+);\s*$/gm,
80
+ (_m, name, params, ret) => { funcSigs.set(name, { params, ret }); return ''; },
81
+ );
82
+ dts = dts.replace(/^\s*\n/gm, '');
83
+
84
+ // Strip remaining malformed multi-line declarations
85
+ dts = dts.replace(/(?:export|declare)\s+function\s+\w+\([\s\S]*?\);\s*/g, '');
86
+ dts = dts.replace(/^\s*\n/gm, '');
87
+
88
+ for (const [name, { params, ret }] of funcSigs) {
89
+ const paramTypes = new Map();
90
+ if (params.trim()) {
91
+ for (const p of params.split(',')) {
92
+ const colon = p.indexOf(':');
93
+ if (colon !== -1) paramTypes.set(p.slice(0, colon).trim(), p.slice(colon + 1).trim());
94
+ }
95
+ }
96
+ const funcRe = new RegExp(
97
+ `((?:export\\s+)?(?:async\\s+)?function\\s+${name})\\(([^)]*)\\)(\\s*\\{)`,
98
+ );
99
+ code = code.replace(funcRe, (_match, prefix, codeParams, brace) => {
100
+ const typed = codeParams.split(',').map(p => {
101
+ const n = p.trim();
102
+ const t = paramTypes.get(n);
103
+ return t ? `${n}: ${t}` : n;
104
+ }).join(', ');
105
+ return `${prefix}(${typed}): ${ret}${brace}`;
106
+ });
107
+ }
108
+
109
+ // Remove bare `let x;` declarations when the DTS already declares
110
+ // `let x: Type;` — avoids "Cannot redeclare" conflicts. Handles
111
+ // both single (`let x;`) and comma-separated (`let x, y;`) forms.
112
+ const dtsVars = new Set();
113
+ for (const m of dts.matchAll(/^(?:let|var)\s+(\w+)\s*:/gm)) dtsVars.add(m[1]);
114
+ if (dtsVars.size) {
115
+ code = code.replace(/^(let|var)\s+([\w\s,]+);[ \t]*$/gm, (_m, kw, vars) => {
116
+ const kept = vars.split(',').map(v => v.trim()).filter(v => !dtsVars.has(v));
117
+ return kept.length ? `${kw} ${kept.join(', ')};` : '';
118
+ });
119
+ }
120
+
121
+ // Determine if this file should be type-checked
122
+ const hasOwnTypes = hasTypeAnnotations(source);
123
+ let importsTyped = false;
124
+ if (!hasOwnTypes) {
125
+ const ripImports = [...source.matchAll(/from\s+['"]([^'"]*\.rip)['"]/g)];
126
+ for (const m of ripImports) {
127
+ const imported = resolve(dirname(filePath), m[1]);
128
+ try {
129
+ const impSrc = readFileSync(imported, 'utf8');
130
+ if (hasTypeAnnotations(impSrc)) { importsTyped = true; break; }
131
+ } catch {}
132
+ }
133
+ }
134
+ const hasTypes = hasOwnTypes || importsTyped;
135
+ if (!hasTypes) code = '// @ts-nocheck\n' + code;
136
+
137
+ // Ensure every file is treated as a module (not a global script)
138
+ if (!/\bexport\b/.test(code) && !/\bimport\b/.test(code)) code += '\nexport {};\n';
139
+
140
+ const tsContent = (hasTypes ? dts + '\n' : '') + code;
141
+ const headerLines = hasTypes ? countLines(dts + '\n') : 1;
142
+
143
+ // Build bidirectional line maps
144
+ const { srcToGen, genToSrc } = buildLineMap(result.reverseMap, result.map, headerLines);
145
+
146
+ // Map DTS variable declaration lines back to their source lines.
147
+ // TypeScript may report errors on the `let x: Type;` line in the
148
+ // DTS header, which has no entry in genToSrc. Fix by matching
149
+ // variable names to source lines with `x::`.
150
+ if (hasTypes && dts) {
151
+ const dtsLines = dts.split('\n');
152
+ const srcLines = source.split('\n');
153
+ for (let i = 0; i < dtsLines.length; i++) {
154
+ const m = dtsLines[i].match(/^(?:let|var)\s+(\w+)\s*:/);
155
+ if (!m) continue;
156
+ const varName = m[1];
157
+ for (let s = 0; s < srcLines.length; s++) {
158
+ if (new RegExp('\\b' + varName + '\\s*::').test(srcLines[s])) {
159
+ genToSrc.set(i, s);
160
+ break;
161
+ }
162
+ }
163
+ }
164
+ }
165
+
166
+ return { tsContent, headerLines, hasTypes, srcToGen, genToSrc, source };
167
+ }
168
+
169
+ // ── Source mapping helpers ──────────────────────────────────────────
170
+
171
+ export function offsetToLine(text, offset) {
172
+ let line = 0;
173
+ for (let i = 0; i < offset && i < text.length; i++) {
174
+ if (text[i] === '\n') line++;
175
+ }
176
+ return line;
177
+ }
178
+
179
+ // Map a TypeScript diagnostic offset back to a Rip source line number.
180
+ // Returns -1 if the offset falls in the DTS header.
181
+ export function mapToSource(entry, offset) {
182
+ const tsLine = offsetToLine(entry.tsContent, offset);
183
+ if (tsLine < entry.headerLines) return -1;
184
+
185
+ if (entry.genToSrc.has(tsLine)) return entry.genToSrc.get(tsLine);
186
+ for (let d = 1; d <= 3; d++) {
187
+ if (entry.genToSrc.has(tsLine - d)) return entry.genToSrc.get(tsLine - d);
188
+ if (entry.genToSrc.has(tsLine + d)) return entry.genToSrc.get(tsLine + d);
189
+ }
190
+ return tsLine - entry.headerLines;
191
+ }
192
+
193
+ // ── CLI batch type-checker ─────────────────────────────────────────
194
+
195
+ function findRipFiles(dir, files = []) {
196
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
197
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
198
+ const full = resolve(dir, entry.name);
199
+ if (entry.isDirectory()) findRipFiles(full, files);
200
+ else if (entry.name.endsWith('.rip')) files.push(full);
201
+ }
202
+ return files;
203
+ }
204
+
205
+ const isColor = process.stdout.isTTY !== false;
206
+ const red = (s) => isColor ? `\x1b[31m${s}\x1b[0m` : s;
207
+ const yellow = (s) => isColor ? `\x1b[33m${s}\x1b[0m` : s;
208
+ const cyan = (s) => isColor ? `\x1b[36m${s}\x1b[0m` : s;
209
+ const dim = (s) => isColor ? `\x1b[2m${s}\x1b[0m` : s;
210
+ const bold = (s) => isColor ? `\x1b[1m${s}\x1b[0m` : s;
211
+
212
+ export async function runCheck(targetDir, opts = {}) {
213
+ const ts = await import('typescript').then(m => m.default || m);
214
+ const rootPath = resolve(targetDir);
215
+
216
+ if (!existsSync(rootPath)) {
217
+ console.error(red(`Error: directory not found: ${targetDir}`));
218
+ return 1;
219
+ }
220
+
221
+ const allFiles = findRipFiles(rootPath);
222
+ if (allFiles.length === 0) {
223
+ console.error(red(`No .rip files found in ${targetDir}`));
224
+ return 1;
225
+ }
226
+
227
+ // Compile all files
228
+ const compiled = new Map();
229
+ const compiler = new Compiler();
230
+ let compileErrors = 0;
231
+
232
+ for (const fp of allFiles) {
233
+ try {
234
+ const source = readFileSync(fp, 'utf8');
235
+ compiled.set(fp, compileForCheck(fp, source, compiler));
236
+ } catch (e) {
237
+ compileErrors++;
238
+ const rel = relative(rootPath, fp);
239
+ console.error(`${red('error')} ${cyan(rel)}: compile error — ${e.message}`);
240
+ }
241
+ }
242
+
243
+ // Also compile any .rip files imported from typed files that aren't in rootPath
244
+ for (const [fp, entry] of [...compiled.entries()]) {
245
+ if (!entry.hasTypes) continue;
246
+ const ripImports = [...entry.source.matchAll(/from\s+['"]([^'"]*\.rip)['"]/g)];
247
+ for (const m of ripImports) {
248
+ const imported = resolve(dirname(fp), m[1]);
249
+ if (!compiled.has(imported) && existsSync(imported)) {
250
+ try {
251
+ const impSrc = readFileSync(imported, 'utf8');
252
+ compiled.set(imported, compileForCheck(imported, impSrc, compiler));
253
+ } catch {}
254
+ }
255
+ }
256
+ }
257
+
258
+ // Create TypeScript language service
259
+ const settings = createTypeCheckSettings(ts, { noImplicitAny: true });
260
+
261
+ const host = {
262
+ getScriptFileNames: () => [...compiled.keys()].map(toVirtual),
263
+ getScriptVersion: () => '1',
264
+ getScriptSnapshot(f) {
265
+ const c = compiled.get(fromVirtual(f));
266
+ if (c) return ts.ScriptSnapshot.fromString(c.tsContent);
267
+ try { return ts.ScriptSnapshot.fromString(readFileSync(f, 'utf8')); } catch { return undefined; }
268
+ },
269
+ getCompilationSettings: () => settings,
270
+ getDefaultLibFileName: (o) => ts.getDefaultLibFilePath(o),
271
+ getCurrentDirectory: () => rootPath,
272
+ fileExists(f) { return compiled.has(fromVirtual(f)) || ts.sys.fileExists(f); },
273
+ readFile(f) { return compiled.get(fromVirtual(f))?.tsContent || ts.sys.readFile(f); },
274
+ readDirectory: (...a) => ts.sys.readDirectory(...a),
275
+ getDirectories: (...a) => ts.sys.getDirectories(...a),
276
+ directoryExists: (...a) => ts.sys.directoryExists(...a),
277
+
278
+ resolveModuleNames(names, containingFile) {
279
+ return names.map((name) => {
280
+ if (name.endsWith('.rip')) {
281
+ const resolved = resolve(dirname(fromVirtual(containingFile)), name);
282
+ if (compiled.has(resolved)) {
283
+ return { resolvedFileName: toVirtual(resolved), extension: '.ts', isExternalLibraryImport: false };
284
+ }
285
+ }
286
+ const r = ts.resolveModuleName(name, containingFile, settings, {
287
+ fileExists: host.fileExists,
288
+ readFile: host.readFile,
289
+ directoryExists: host.directoryExists,
290
+ getCurrentDirectory: host.getCurrentDirectory,
291
+ getDirectories: host.getDirectories,
292
+ });
293
+ return r.resolvedModule;
294
+ });
295
+ },
296
+ };
297
+
298
+ const service = ts.createLanguageService(host, ts.createDocumentRegistry());
299
+
300
+ // Collect diagnostics
301
+ let totalErrors = 0;
302
+ let totalWarnings = 0;
303
+ const fileResults = [];
304
+
305
+ for (const [fp, entry] of compiled) {
306
+ if (!entry.hasTypes) continue;
307
+
308
+ const vf = toVirtual(fp);
309
+ let diags;
310
+ try {
311
+ const sem = service.getSemanticDiagnostics(vf);
312
+ const syn = service.getSyntacticDiagnostics(vf);
313
+ diags = [...syn, ...sem];
314
+ } catch {
315
+ continue;
316
+ }
317
+
318
+ const errors = [];
319
+ for (const d of diags) {
320
+ if (d.start === undefined) continue;
321
+ if (SKIP_CODES.has(d.code)) continue;
322
+
323
+ const srcLine = mapToSource(entry, d.start);
324
+ if (srcLine < 0) continue;
325
+
326
+ const message = ts.flattenDiagnosticMessageText(d.messageText, '\n');
327
+ const severity = d.category === 1 ? 'error' : d.category === 0 ? 'warning' : 'info';
328
+
329
+ errors.push({ line: srcLine + 1, message, severity, code: d.code });
330
+ if (severity === 'error') totalErrors++;
331
+ else if (severity === 'warning') totalWarnings++;
332
+ }
333
+
334
+ if (errors.length > 0) {
335
+ fileResults.push({ file: fp, errors });
336
+ }
337
+ }
338
+
339
+ // Print results
340
+ for (const { file, errors } of fileResults) {
341
+ const rel = relative(rootPath, file);
342
+ for (const e of errors) {
343
+ const loc = `${cyan(rel)}${dim(':')}${yellow(String(e.line))}`;
344
+ const sev = e.severity === 'error' ? red('error') : yellow('warning');
345
+ console.log(`${loc} ${sev} ${e.message} ${dim(`TS${e.code}`)}`);
346
+ }
347
+ }
348
+
349
+ // Summary
350
+ const typedFiles = [...compiled.values()].filter(e => e.hasTypes).length;
351
+ const totalFiles = compiled.size;
352
+
353
+ if (totalErrors === 0 && totalWarnings === 0) {
354
+ console.log(`\n${bold('✓')} ${typedFiles} typed file${typedFiles !== 1 ? 's' : ''} checked, no errors found`);
355
+ if (compileErrors > 0) {
356
+ console.log(dim(` (${compileErrors} file${compileErrors !== 1 ? 's' : ''} had compile errors)`));
357
+ }
358
+ return compileErrors > 0 ? 1 : 0;
359
+ }
360
+
361
+ const parts = [];
362
+ if (totalErrors > 0) parts.push(red(`${totalErrors} error${totalErrors !== 1 ? 's' : ''}`));
363
+ if (totalWarnings > 0) parts.push(yellow(`${totalWarnings} warning${totalWarnings !== 1 ? 's' : ''}`));
364
+ console.log(`\n${bold('✗')} ${parts.join(', ')} in ${fileResults.length} file${fileResults.length !== 1 ? 's' : ''} (${typedFiles} typed / ${totalFiles} total)`);
365
+
366
+ return totalErrors > 0 ? 1 : 0;
367
+ }