rip-lang 3.17.3 → 3.17.5
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 +1 -1
- package/docs/RIP-LANG.md +90 -3
- package/docs/RIP-TYPES.md +33 -0
- package/docs/demo/routes/card.rip +1 -1
- package/docs/dist/rip.js +531 -123
- package/docs/dist/rip.min.js +237 -203
- package/docs/dist/rip.min.js.br +0 -0
- package/docs/example/index.json +1 -1
- package/docs/example/index.json.br +0 -0
- package/docs/extensions/vscode/print/index.html +2 -1
- package/docs/extensions/vscode/print/print-1.0.16.vsix +0 -0
- package/docs/extensions/vscode/print/print-latest.vsix +0 -0
- package/docs/extensions/vscode/rip/index.html +2 -1
- package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
- package/docs/extensions/vscode/rip/vscode-rip-0.8.0.vsix +0 -0
- package/docs/ui/bundle.json +40 -40
- package/docs/ui/bundle.json.br +0 -0
- package/docs/ui/index.css +8 -0
- package/docs/ui/index.html +4 -4
- package/package.json +5 -5
- package/src/AGENTS.md +43 -10
- package/src/compiler.js +38 -4
- package/src/components.js +474 -89
- package/src/dts.js +26 -28
- package/src/generated/dom-events.js +2 -1
- package/src/generated/dom-tags.js +2 -1
- package/src/grammar/grammar.rip +8 -0
- package/src/parser.js +28 -27
- package/src/schema/runtime-validate.js +7 -0
- package/src/schema/runtime.generated.js +7 -0
- package/src/typecheck.js +159 -35
- package/src/types.js +153 -0
|
@@ -904,6 +904,13 @@ class __SchemaDef {
|
|
|
904
904
|
// a non-enumerable accessor so order.user_id and order.userId read
|
|
905
905
|
// the same slot — useful when DB column names leak into user code
|
|
906
906
|
// via raw SQL helpers.
|
|
907
|
+
//
|
|
908
|
+
// Temporal values arrive already decoded by the adapter: @rip-lang/db
|
|
909
|
+
// decodes date/datetime columns to real `Date` at the wire boundary
|
|
910
|
+
// (naive TIMESTAMP is read as UTC), so hydrate stores them verbatim —
|
|
911
|
+
// do NOT add a decode here. That keeps a single decode seam and honors
|
|
912
|
+
// the `date`/`datetime` -> `Date` .d.ts contract. (`_coerceDates` below
|
|
913
|
+
// is parse-only, for JSON/HTTP input — it never runs on this path.)
|
|
907
914
|
const data = {};
|
|
908
915
|
for (let i = 0; i < columns.length; i++) {
|
|
909
916
|
data[__schemaCamel(columns[i].name)] = row[i];
|
|
@@ -992,6 +992,13 @@ class __SchemaDef {
|
|
|
992
992
|
// a non-enumerable accessor so order.user_id and order.userId read
|
|
993
993
|
// the same slot — useful when DB column names leak into user code
|
|
994
994
|
// via raw SQL helpers.
|
|
995
|
+
//
|
|
996
|
+
// Temporal values arrive already decoded by the adapter: @rip-lang/db
|
|
997
|
+
// decodes date/datetime columns to real \`Date\` at the wire boundary
|
|
998
|
+
// (naive TIMESTAMP is read as UTC), so hydrate stores them verbatim —
|
|
999
|
+
// do NOT add a decode here. That keeps a single decode seam and honors
|
|
1000
|
+
// the \`date\`/\`datetime\` -> \`Date\` .d.ts contract. (\`_coerceDates\` below
|
|
1001
|
+
// is parse-only, for JSON/HTTP input — it never runs on this path.)
|
|
995
1002
|
const data = {};
|
|
996
1003
|
for (let i = 0; i < columns.length; i++) {
|
|
997
1004
|
data[__schemaCamel(columns[i].name)] = row[i];
|
package/src/typecheck.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { Compiler, getStdlibCode } from './compiler.js';
|
|
14
14
|
import { Lexer } from './lexer.js';
|
|
15
15
|
import { STDLIB_TYPE_DECLS } from './stdlib.js';
|
|
16
|
-
import { INTRINSIC_TYPE_DECLS, INTRINSIC_FN_DECL,
|
|
16
|
+
import { INTRINSIC_TYPE_DECLS, INTRINSIC_FN_DECL, SIGNAL_INTERFACE, SIGNAL_FN, COMPUTED_INTERFACE, COMPUTED_FN, EFFECT_FN, ripDestructuredNames } from './dts.js';
|
|
17
17
|
import './schema/loader-server.js'; // registers full schema runtime provider
|
|
18
18
|
import { createRequire } from 'module';
|
|
19
19
|
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
@@ -407,25 +407,28 @@ export function validatePropDefault(type, defVal) {
|
|
|
407
407
|
// A string literal in either quote style: "a" or 'a'.
|
|
408
408
|
const isStrLit = s => /^"[^"]*"$/.test(s) || /^'[^']*'$/.test(s);
|
|
409
409
|
const strInner = s => s.slice(1, -1);
|
|
410
|
+
// The literal kind of a default, or null when it isn't a recognizable literal
|
|
411
|
+
// (an identifier, call, or other expression). Only literal defaults can be
|
|
412
|
+
// judged here; a non-literal carries no statically known type, so it is left
|
|
413
|
+
// to the type checker and never flagged.
|
|
414
|
+
const litKind = s =>
|
|
415
|
+
isStrLit(s) ? 'string' :
|
|
416
|
+
/^-?\d+(\.\d+)?$/.test(s) ? 'number' :
|
|
417
|
+
(s === 'true' || s === 'false') ? 'boolean' : null;
|
|
410
418
|
const parts = type.split('|').map(s => s.trim());
|
|
411
|
-
// String literal union: "a" | "b" | "c" (either quote style)
|
|
419
|
+
// String literal union: "a" | "b" | "c" (either quote style). Only a literal
|
|
420
|
+
// default naming no member is wrong; a non-literal default is left alone.
|
|
412
421
|
if (parts.every(p => isStrLit(p))) {
|
|
413
422
|
if (isStrLit(defVal) && !parts.some(p => strInner(p) === strInner(defVal))) {
|
|
414
423
|
return `Type '${defVal}' is not assignable to type '${type}'`;
|
|
415
424
|
}
|
|
416
425
|
return null;
|
|
417
426
|
}
|
|
418
|
-
// Single type
|
|
419
|
-
if (parts.length === 1) {
|
|
420
|
-
const
|
|
421
|
-
if (
|
|
422
|
-
return `Type '${defVal}' is not assignable to type '
|
|
423
|
-
}
|
|
424
|
-
if (t === 'number' && !/^-?\d+(\.\d+)?$/.test(defVal)) {
|
|
425
|
-
return `Type '${defVal}' is not assignable to type 'number'`;
|
|
426
|
-
}
|
|
427
|
-
if (t === 'string' && !isStrLit(defVal)) {
|
|
428
|
-
return `Type '${defVal}' is not assignable to type 'string'`;
|
|
427
|
+
// Single primitive type: flag only a literal default of a different kind.
|
|
428
|
+
if (parts.length === 1 && (parts[0] === 'boolean' || parts[0] === 'number' || parts[0] === 'string')) {
|
|
429
|
+
const kind = litKind(defVal);
|
|
430
|
+
if (kind && kind !== parts[0]) {
|
|
431
|
+
return `Type '${defVal}' is not assignable to type '${parts[0]}'`;
|
|
429
432
|
}
|
|
430
433
|
}
|
|
431
434
|
return null;
|
|
@@ -536,25 +539,28 @@ export function parseComponentDTS(dtsString) {
|
|
|
536
539
|
return result;
|
|
537
540
|
}
|
|
538
541
|
|
|
539
|
-
// Check component prop definitions for
|
|
542
|
+
// Check component prop definitions for invalid defaults.
|
|
540
543
|
// Returns array of { line (0-based), col, len, message } error objects.
|
|
544
|
+
//
|
|
545
|
+
// Type annotations use a single ':' (e.g. `@count: number := 0`); `:=`/`=` is
|
|
546
|
+
// an untyped initializer whose type is inferred from the default, and `::` is
|
|
547
|
+
// prototype access — never a type. A typed prop with a `:= default` has its
|
|
548
|
+
// default validated against the annotation; untyped props carry no annotation
|
|
549
|
+
// to check against and are left alone.
|
|
541
550
|
export function checkComponentDefs(compProps, srcLines, startLine = 0) {
|
|
542
551
|
const errors = [];
|
|
543
552
|
for (const prop of compProps) {
|
|
553
|
+
// `@name` + optional `?`, then a single `:` that is NOT part of `:=`.
|
|
554
|
+
const re = new RegExp('(@' + prop.name + ')\\b\\??\\s*:(?!=)\\s*(.+)$');
|
|
544
555
|
for (let s = startLine; s < srcLines.length; s++) {
|
|
545
|
-
const m =
|
|
556
|
+
const m = re.exec(srcLines[s]);
|
|
546
557
|
if (!m) continue;
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
const err = validatePropDefault(dm[1].trim(), defVal);
|
|
554
|
-
if (err) {
|
|
555
|
-
errors.push({ line: s, col: m.index, len: m[1].length, propName: prop.name, message: err });
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
+
// Validate a `:= default` against the annotation, if one is present.
|
|
559
|
+
const dm = m[2].match(/^(.+?)\s*:=\s*(.+)$/);
|
|
560
|
+
if (dm) {
|
|
561
|
+
const defVal = stripDefaultComment(dm[2]).trim();
|
|
562
|
+
const err = validatePropDefault(dm[1].trim(), defVal);
|
|
563
|
+
if (err) errors.push({ line: s, col: m.index, len: m[1].length, message: err });
|
|
558
564
|
}
|
|
559
565
|
break;
|
|
560
566
|
}
|
|
@@ -562,6 +568,19 @@ export function checkComponentDefs(compProps, srcLines, startLine = 0) {
|
|
|
562
568
|
return errors;
|
|
563
569
|
}
|
|
564
570
|
|
|
571
|
+
// Strip a trailing `# comment` from a default-value expression, ignoring a `#`
|
|
572
|
+
// inside a quoted string literal (e.g. a CSS color `'#fff'`).
|
|
573
|
+
function stripDefaultComment(s) {
|
|
574
|
+
let quote = null;
|
|
575
|
+
for (let i = 0; i < s.length; i++) {
|
|
576
|
+
const c = s[i];
|
|
577
|
+
if (quote) { if (c === quote) quote = null; }
|
|
578
|
+
else if (c === '"' || c === "'") quote = c;
|
|
579
|
+
else if (c === '#') return s.slice(0, i);
|
|
580
|
+
}
|
|
581
|
+
return s;
|
|
582
|
+
}
|
|
583
|
+
|
|
565
584
|
// Patch uninitialized, untyped variables with inferred types from their
|
|
566
585
|
// first assignment. This makes `let total; total = count + ratio;` behave
|
|
567
586
|
// like `let total: number;` — so a later `total = "string"` is caught.
|
|
@@ -2078,10 +2097,6 @@ export function compileForCheck(filePath, source, compiler, opts = {}) {
|
|
|
2078
2097
|
headerDts = parts.join('\n') + '\n' + headerDts;
|
|
2079
2098
|
}
|
|
2080
2099
|
|
|
2081
|
-
if (hasTypes && /\bARIA\./.test(code) && !/\bdeclare const ARIA\b/.test(headerDts)) {
|
|
2082
|
-
headerDts = ARIA_TYPE_DECLS.join('\n') + '\n' + headerDts;
|
|
2083
|
-
}
|
|
2084
|
-
|
|
2085
2100
|
// Inline hoisted `let` declarations at their first assignment in the shadow
|
|
2086
2101
|
// TS file, then merge DTS header types into the inlined declarations.
|
|
2087
2102
|
//
|
|
@@ -3523,6 +3538,113 @@ export function readProjectConfig(dir) {
|
|
|
3523
3538
|
return config;
|
|
3524
3539
|
}
|
|
3525
3540
|
|
|
3541
|
+
// Resolve a project's ambient `.d.ts` includes — both explicit and automatic —
|
|
3542
|
+
// to a deduped list of absolute paths, added as explicit TS program roots
|
|
3543
|
+
// (appended to the language-service host's getScriptFileNames). An explicit
|
|
3544
|
+
// root-file `.d.ts` contributes its global `declare`s to every other file in the
|
|
3545
|
+
// program regardless of the `types` compiler option (like tsconfig `files`), so
|
|
3546
|
+
// this is how a project pulls in a shared ambient contract without a per-file
|
|
3547
|
+
// `/// <reference>`. Two mechanisms feed it:
|
|
3548
|
+
//
|
|
3549
|
+
// ① explicit — `package.json#rip.types: ["path/to/x.d.ts", ...]`. The escape
|
|
3550
|
+
// hatch for ad-hoc / non-dependency `.d.ts`; paths resolve relative to the
|
|
3551
|
+
// project root.
|
|
3552
|
+
// ② automatic — every declared `@rip-lang/*` dependency that ships ambient
|
|
3553
|
+
// types (its own `package.json#rip.ambient: ["x.d.ts", ...]`) has those
|
|
3554
|
+
// files auto-included, resolved relative to the dependency's installed
|
|
3555
|
+
// location. So depending on `@rip-lang/app` makes its global `ARIA` contract
|
|
3556
|
+
// "just there" with zero config — "you used the framework, so its types are
|
|
3557
|
+
// present."
|
|
3558
|
+
//
|
|
3559
|
+
// Deduped by absolute path (an explicit `rip.types` pointing at a file a
|
|
3560
|
+
// dependency already provides isn't included twice). Missing/unresolvable
|
|
3561
|
+
// entries are warned and skipped, never fatal.
|
|
3562
|
+
export function resolveTypeIncludes(ripConfig, rootPath) {
|
|
3563
|
+
const out = [];
|
|
3564
|
+
const seen = new Set();
|
|
3565
|
+
const add = (abs) => { if (abs && !seen.has(abs)) { seen.add(abs); out.push(abs); } };
|
|
3566
|
+
|
|
3567
|
+
// ① explicit rip.types
|
|
3568
|
+
const list = Array.isArray(ripConfig?.types) ? ripConfig.types : [];
|
|
3569
|
+
for (const entry of list) {
|
|
3570
|
+
if (typeof entry !== 'string' || !entry) continue;
|
|
3571
|
+
const abs = resolve(rootPath, entry);
|
|
3572
|
+
if (existsSync(abs)) add(abs);
|
|
3573
|
+
else console.warn(`[rip] rip.types include not found, skipping: ${entry}`);
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
// ② auto-include ambient .d.ts from declared @rip-lang/* dependencies
|
|
3577
|
+
for (const abs of collectDependencyAmbients(rootPath)) add(abs);
|
|
3578
|
+
|
|
3579
|
+
return out;
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3582
|
+
// Walk the project's declared `@rip-lang/*` dependencies (deps + devDeps +
|
|
3583
|
+
// peerDeps + optionalDeps) and collect the ambient `.d.ts` files each one
|
|
3584
|
+
// advertises via its own `package.json#rip.ambient`. Each file is resolved
|
|
3585
|
+
// relative to the dependency's resolved package directory (handles workspace
|
|
3586
|
+
// symlinks and real node_modules alike). Returns absolute paths of files that
|
|
3587
|
+
// exist; everything else is skipped — a declared-but-missing ambient file warns,
|
|
3588
|
+
// while an unresolvable dependency is silent (its absence surfaces through the
|
|
3589
|
+
// undeclared-import / module-resolution paths, not here).
|
|
3590
|
+
export function collectDependencyAmbients(rootPath) {
|
|
3591
|
+
const out = [];
|
|
3592
|
+
let projPkg;
|
|
3593
|
+
try {
|
|
3594
|
+
const projPkgPath = resolve(rootPath, 'package.json');
|
|
3595
|
+
if (!existsSync(projPkgPath)) return out;
|
|
3596
|
+
projPkg = JSON.parse(readFileSync(projPkgPath, 'utf8'));
|
|
3597
|
+
} catch (e) {
|
|
3598
|
+
console.warn(`[rip] ambient dep scan: cannot read project package.json: ${e.message}`);
|
|
3599
|
+
return out;
|
|
3600
|
+
}
|
|
3601
|
+
const deps = [...new Set([
|
|
3602
|
+
...Object.keys(projPkg.dependencies || {}),
|
|
3603
|
+
...Object.keys(projPkg.devDependencies || {}),
|
|
3604
|
+
...Object.keys(projPkg.peerDependencies || {}),
|
|
3605
|
+
...Object.keys(projPkg.optionalDependencies || {}),
|
|
3606
|
+
])].filter(name => name.startsWith('@rip-lang/'));
|
|
3607
|
+
if (deps.length === 0) return out;
|
|
3608
|
+
|
|
3609
|
+
const req = createRequire(resolve(rootPath, 'package.json'));
|
|
3610
|
+
for (const dep of deps) {
|
|
3611
|
+
const depPkgJson = resolveDepPackageJson(dep, rootPath, req);
|
|
3612
|
+
if (!depPkgJson) continue; // not installed/resolvable — not ours to report
|
|
3613
|
+
let depPkg;
|
|
3614
|
+
try { depPkg = JSON.parse(readFileSync(depPkgJson, 'utf8')); } catch { continue; }
|
|
3615
|
+
const ambient = depPkg?.rip?.ambient;
|
|
3616
|
+
if (!Array.isArray(ambient)) continue;
|
|
3617
|
+
const depDir = dirname(depPkgJson);
|
|
3618
|
+
for (const rel of ambient) {
|
|
3619
|
+
if (typeof rel !== 'string' || !rel) continue;
|
|
3620
|
+
const abs = resolve(depDir, rel);
|
|
3621
|
+
if (existsSync(abs)) out.push(abs);
|
|
3622
|
+
else console.warn(`[rip] ${dep} declares ambient '${rel}' but it was not found, skipping`);
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
return out;
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
// Locate a dependency's package.json. Prefer Node/Bun module resolution
|
|
3629
|
+
// (`require.resolve('<dep>/package.json')`); fall back to walking node_modules up
|
|
3630
|
+
// from the project root — robust against `exports` maps that don't list
|
|
3631
|
+
// `./package.json`, and against either runtime (CLI/Bun or the LSP/Node).
|
|
3632
|
+
function resolveDepPackageJson(dep, rootPath, req) {
|
|
3633
|
+
try {
|
|
3634
|
+
const r = req.resolve(`${dep}/package.json`);
|
|
3635
|
+
if (typeof r === 'string' && existsSync(r)) return r;
|
|
3636
|
+
} catch {}
|
|
3637
|
+
let dir = resolve(rootPath);
|
|
3638
|
+
while (true) {
|
|
3639
|
+
const cand = resolve(dir, 'node_modules', dep, 'package.json');
|
|
3640
|
+
if (existsSync(cand)) return cand;
|
|
3641
|
+
const parent = dirname(dir);
|
|
3642
|
+
if (parent === dir) break;
|
|
3643
|
+
dir = parent;
|
|
3644
|
+
}
|
|
3645
|
+
return null;
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3526
3648
|
// ── CLI batch type-checker ─────────────────────────────────────────
|
|
3527
3649
|
|
|
3528
3650
|
// Convert a simple glob pattern to a RegExp for matching relative paths.
|
|
@@ -3602,6 +3724,7 @@ export async function runCheck(targetDir, opts = {}) {
|
|
|
3602
3724
|
const checkAll = ripConfig.checkAll === true;
|
|
3603
3725
|
const excludeGlobs = Array.isArray(ripConfig.exclude) ? ripConfig.exclude : [];
|
|
3604
3726
|
const excludePatterns = excludeGlobs.map(globToRegex);
|
|
3727
|
+
const typeIncludes = resolveTypeIncludes(ripConfig, rootPath);
|
|
3605
3728
|
|
|
3606
3729
|
const allFiles = findRipFiles(rootPath, [], excludePatterns);
|
|
3607
3730
|
if (allFiles.length === 0) {
|
|
@@ -3875,7 +3998,7 @@ export async function runCheck(targetDir, opts = {}) {
|
|
|
3875
3998
|
});
|
|
3876
3999
|
|
|
3877
4000
|
const host = {
|
|
3878
|
-
getScriptFileNames: () => [...compiled.keys()].map(toVirtual),
|
|
4001
|
+
getScriptFileNames: () => [...[...compiled.keys()].map(toVirtual), ...typeIncludes],
|
|
3879
4002
|
getScriptVersion: () => '1',
|
|
3880
4003
|
getScriptSnapshot(f) {
|
|
3881
4004
|
const c = compiled.get(fromVirtual(f));
|
|
@@ -4071,11 +4194,11 @@ export async function runCheck(targetDir, opts = {}) {
|
|
|
4071
4194
|
}
|
|
4072
4195
|
}
|
|
4073
4196
|
|
|
4074
|
-
//
|
|
4197
|
+
// Component prop checking — validate prop default values against their types
|
|
4075
4198
|
if (entry.dts) {
|
|
4076
|
-
for (const [
|
|
4199
|
+
for (const [, compInfo] of parseComponentDTS(entry.dts)) {
|
|
4077
4200
|
for (const e of checkComponentDefs(compInfo.props, srcLines)) {
|
|
4078
|
-
errors.push({ line: e.line + 1, col: e.col + 1, len: e.len, message: e.message
|
|
4201
|
+
errors.push({ line: e.line + 1, col: e.col + 1, len: e.len, message: e.message, severity: 'error', code: 'rip', srcLine: srcLines[e.line], related: [] });
|
|
4079
4202
|
totalErrors++;
|
|
4080
4203
|
}
|
|
4081
4204
|
}
|
|
@@ -4663,6 +4786,7 @@ export async function runAudit(targetDir) {
|
|
|
4663
4786
|
}
|
|
4664
4787
|
|
|
4665
4788
|
const { typeRoots, types: ambientTypes } = collectAmbientTypes(rootPath);
|
|
4789
|
+
const typeIncludes = resolveTypeIncludes(pkg.rip, rootPath);
|
|
4666
4790
|
const settings = createTypeCheckSettings(ts, {
|
|
4667
4791
|
strict: true,
|
|
4668
4792
|
...(typeRoots.length ? { typeRoots } : {}),
|
|
@@ -4670,7 +4794,7 @@ export async function runAudit(targetDir) {
|
|
|
4670
4794
|
});
|
|
4671
4795
|
|
|
4672
4796
|
const host = {
|
|
4673
|
-
getScriptFileNames: () => [...compiled.keys()].map(toVirtual),
|
|
4797
|
+
getScriptFileNames: () => [...[...compiled.keys()].map(toVirtual), ...typeIncludes],
|
|
4674
4798
|
getScriptVersion: () => '1',
|
|
4675
4799
|
getScriptSnapshot(f) {
|
|
4676
4800
|
const c = compiled.get(fromVirtual(f));
|
package/src/types.js
CHANGED
|
@@ -14,6 +14,44 @@
|
|
|
14
14
|
|
|
15
15
|
import { RipError } from './error.js';
|
|
16
16
|
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// `expr as Type` cast — recognition sets
|
|
19
|
+
// ============================================================================
|
|
20
|
+
//
|
|
21
|
+
// The cast is a contextual postfix operator: a bare `as` (already an
|
|
22
|
+
// IDENTIFIER here — the lexer emits AS / FORAS / FORASAWAIT for the
|
|
23
|
+
// import/export and for-loop cases, so an `as` that survives as an
|
|
24
|
+
// IDENTIFIER is never one of those) glued to a value on its left and a type
|
|
25
|
+
// on its right. We recognize it purely from token shape: the previous token
|
|
26
|
+
// must be able to END an expression, and the next must be able to START a
|
|
27
|
+
// type. Detection runs in rewriteTypes(), BEFORE addImplicitBracesAndParens(),
|
|
28
|
+
// so `x as Foo` is still three bare identifiers (not yet `x(as(Foo))`).
|
|
29
|
+
|
|
30
|
+
// Tokens whose token can be the tail of the cast's left-hand expression.
|
|
31
|
+
// CAST is included so chains (`x as A as B`) work: the first cast collapses
|
|
32
|
+
// to `<value> CAST`, and the second `as` sees CAST as its left edge.
|
|
33
|
+
const CAST_LHS_ENDERS = new Set([
|
|
34
|
+
'IDENTIFIER', 'PROPERTY', 'NUMBER', 'STRING', 'STRING_END',
|
|
35
|
+
'REGEX', 'REGEX_END', 'BOOL', 'NULL', 'UNDEFINED', 'INFINITY', 'NAN', 'JS',
|
|
36
|
+
')', 'CALL_END', 'PARAM_END', ']', 'INDEX_END', '}',
|
|
37
|
+
'THIS', 'SUPER', '?', 'PRESENCE', 'CAST',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
// Tokens that can begin a type expression on the cast's right-hand side.
|
|
41
|
+
const CAST_TYPE_STARTERS = new Set([
|
|
42
|
+
'IDENTIFIER', '(', 'CALL_START', 'PARAM_START', '{', '[', 'INDEX_START',
|
|
43
|
+
'STRING', 'STRING_START', 'NUMBER', 'BOOL', 'NULL', 'UNDEFINED', '-', 'UNARY',
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
// Does the `as` token at index i look like a cast (vs. a stray identifier)?
|
|
47
|
+
function isCastAs(tokens, i) {
|
|
48
|
+
let prev = tokens[i - 1];
|
|
49
|
+
if (!prev || prev[0] === '.' || prev[0] === '?.') return false;
|
|
50
|
+
if (!CAST_LHS_ENDERS.has(prev[0])) return false;
|
|
51
|
+
let next = tokens[i + 1];
|
|
52
|
+
return !!next && CAST_TYPE_STARTERS.has(next[0]);
|
|
53
|
+
}
|
|
54
|
+
|
|
17
55
|
// ============================================================================
|
|
18
56
|
// installTypeSupport — adds rewriteTypes() to Lexer.prototype
|
|
19
57
|
// ============================================================================
|
|
@@ -60,6 +98,45 @@ export function installTypeSupport(Lexer) {
|
|
|
60
98
|
this.scanTokens((token, i, tokens) => {
|
|
61
99
|
let tag = token[0];
|
|
62
100
|
|
|
101
|
+
// ── `expr as Type` cast — postfix type assertion (route A) ──────────
|
|
102
|
+
// A bare `as` glued to a value on its left and a type on its right. The
|
|
103
|
+
// Type RHS is collected as a string (the same machinery as `::`
|
|
104
|
+
// annotations) and the whole `as Type` run COLLAPSES into a single
|
|
105
|
+
// in-stream CAST marker token carrying that string as its value. The
|
|
106
|
+
// grammar reduces `Expression CAST` to `['cast', expr, typeStr]` — so
|
|
107
|
+
// the parser sees a structural postfix node (like `?`/`!`) but never a
|
|
108
|
+
// type. The compiler erases it at runtime and re-materializes it as a
|
|
109
|
+
// real TS `(expr as Type)` only in the shadow-TS check path. Because the
|
|
110
|
+
// marker is a real token, the reduction fires AFTER the full postfix
|
|
111
|
+
// expression is built, so call/index/paren results narrow too.
|
|
112
|
+
// Chaining (`x as A as B`) emits one CAST per `as` (`x CAST CAST`); the
|
|
113
|
+
// grammar's left-associativity nests them: `['cast',['cast',x,'A'],'B']`.
|
|
114
|
+
if (tag === 'IDENTIFIER' && token[1] === 'as' && !token.data?.bang &&
|
|
115
|
+
isCastAs(tokens, i)) {
|
|
116
|
+
let typeTokens = collectTypeExpression(tokens, i + 1, { castContext: true });
|
|
117
|
+
if (typeTokens.length > 0) {
|
|
118
|
+
let typeStr = buildTypeString(typeTokens);
|
|
119
|
+
for (let tt of typeTokens) {
|
|
120
|
+
if (tt[0] === 'IDENTIFIER') typeRefNames.add(tt[1]);
|
|
121
|
+
}
|
|
122
|
+
let castTok = gen('CAST', typeStr, token);
|
|
123
|
+
tokens.splice(i, 1 + typeTokens.consumed, castTok);
|
|
124
|
+
// A trailing `>` (or other "unfinished" tail) makes the lexer
|
|
125
|
+
// suppress the newline after the type, so once the `as Type` run is
|
|
126
|
+
// collapsed the statement can collide with the next line. If what now
|
|
127
|
+
// follows the marker sits on a later row and isn't already a
|
|
128
|
+
// separator, restore the TERMINATOR the lexer ate.
|
|
129
|
+
let follow = tokens[i + 1];
|
|
130
|
+
if (follow && follow[0] !== 'TERMINATOR' && follow[0] !== 'INDENT' &&
|
|
131
|
+
follow[0] !== 'CAST' &&
|
|
132
|
+
follow.loc?.r != null && castTok.loc?.r != null &&
|
|
133
|
+
follow.loc.r > castTok.loc.r) {
|
|
134
|
+
tokens.splice(i + 1, 0, gen('TERMINATOR', '\n', follow));
|
|
135
|
+
}
|
|
136
|
+
return 1; // advance past the CAST marker (a chained `as` follows it)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
63
140
|
// ── Generic type parameters: DEF name<T>(...) or Name<T> = component ──
|
|
64
141
|
// (Generic params on type aliases are handled by the `type` keyword handler below)
|
|
65
142
|
if (tag === 'IDENTIFIER') {
|
|
@@ -595,6 +672,38 @@ function reclassifyColonTypes(tokens) {
|
|
|
595
672
|
return atStatementStart(tokens[i - 2]);
|
|
596
673
|
};
|
|
597
674
|
|
|
675
|
+
// Is this top-level `=` the assignment of a `type Name [<generics>] =`
|
|
676
|
+
// declaration? Its RHS is a whole type expression, so we arm `inType` there —
|
|
677
|
+
// otherwise a braced type literal RHS (`type X = { f: (e: Event) => void }`,
|
|
678
|
+
// incl. intersections `… & { … }`) reaches the field colon at bracket-depth
|
|
679
|
+
// ≥ 1, where none of the `:`-reclassify branches (all `stack.length === 0`)
|
|
680
|
+
// fire, so `inType` is never armed and the function-type's `(e: Event)` param
|
|
681
|
+
// is wrongly reclassified — putting a synthetic `::` inside a structural type
|
|
682
|
+
// literal. Mirrors `isDefParamStart`'s backward generic skip. (The indented
|
|
683
|
+
// `type T =` ⏎ INDENT form is unaffected: it disarms `inType` at the INDENT
|
|
684
|
+
// and is collected per-field by `collectStructuralType`.)
|
|
685
|
+
const isTypeDeclEq = (i) => {
|
|
686
|
+
let j = i - 1;
|
|
687
|
+
const g0 = tokens[j]?.[0], v0 = tokens[j]?.[1];
|
|
688
|
+
if ((g0 === 'COMPARE' && v0 === '>') || (g0 === 'SHIFT' && v0 === '>>')) {
|
|
689
|
+
let depth = g0 === 'SHIFT' ? 2 : 1;
|
|
690
|
+
j--;
|
|
691
|
+
while (j >= 0 && depth > 0) {
|
|
692
|
+
const g = tokens[j][0], v = tokens[j][1];
|
|
693
|
+
if (g === 'COMPARE' && v === '>') depth++;
|
|
694
|
+
else if (g === 'SHIFT' && v === '>>') depth += 2;
|
|
695
|
+
else if (g === 'COMPARE' && v === '<') depth--;
|
|
696
|
+
else if (g === 'SHIFT' && v === '<<') depth -= 2;
|
|
697
|
+
j--;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
// tokens[j] is the type name, preceded by the `type` contextual keyword at
|
|
701
|
+
// a statement boundary (the same shape the `type Name =` collector matches).
|
|
702
|
+
if (tokens[j]?.[0] !== 'IDENTIFIER') return false;
|
|
703
|
+
if (!(tokens[j - 1]?.[0] === 'IDENTIFIER' && tokens[j - 1]?.[1] === 'type')) return false;
|
|
704
|
+
return atStatementStart(tokens[j - 2]);
|
|
705
|
+
};
|
|
706
|
+
|
|
598
707
|
// A type expression (opened by any `::` / TYPE_ANNOTATION, or by a `:` we
|
|
599
708
|
// reclassify) must never have its interior touched: a function type like
|
|
600
709
|
// `{ cb: (r: R) => void }` carries single `:` colons that are TS member/param
|
|
@@ -660,6 +769,16 @@ function reclassifyColonTypes(tokens) {
|
|
|
660
769
|
// Commit per-line key:value state at statement boundaries (depth 0).
|
|
661
770
|
if (tag === 'TERMINATOR' && stack.length === 0) { prevSiblingKV = curLineKV; curLineKV = false; }
|
|
662
771
|
|
|
772
|
+
// Type-alias RHS: `type Name [<generics>] = <type>`. The whole RHS is a
|
|
773
|
+
// type expression — arm `inType` so its interior colons (a braced literal's
|
|
774
|
+
// field `:`, a function-type param's `:`) are left as TS separators rather
|
|
775
|
+
// than reclassified. Disarms at the RHS terminator / on exiting its depth,
|
|
776
|
+
// like every other `enterType`. A value-position `=` (`x = { a: 1 }`) is not
|
|
777
|
+
// a type decl, so `isTypeDeclEq` rejects it and object literals stay intact.
|
|
778
|
+
if (tag === '=' && stack.length === 0 && !inType && isTypeDeclEq(i)) {
|
|
779
|
+
enterType(); continue;
|
|
780
|
+
}
|
|
781
|
+
|
|
663
782
|
if (tag === ':') {
|
|
664
783
|
const prev = tokens[i - 1];
|
|
665
784
|
const prevTag = prev?.[0];
|
|
@@ -829,6 +948,25 @@ function collectTypeExpression(tokens, j, opts = {}) {
|
|
|
829
948
|
break;
|
|
830
949
|
}
|
|
831
950
|
|
|
951
|
+
// Cast context: a cast's type lives on one logical line, but the lexer
|
|
952
|
+
// suppresses the newline after a trailing `>` (a COMPARE token is
|
|
953
|
+
// "unfinished"), so no TERMINATOR separates `x as Map<K, V>` from the next
|
|
954
|
+
// statement. Stop at a depth-0 row change so the collector can't run past
|
|
955
|
+
// end-of-line into the following code (the `Map<K, V> nextStmt` footgun).
|
|
956
|
+
if (opts.castContext && depth === 0 && j > startJ) {
|
|
957
|
+
let prevRow = tokens[j - 1]?.loc?.r;
|
|
958
|
+
let curRow = t.loc?.r;
|
|
959
|
+
if (prevRow != null && curRow != null && curRow > prevRow) break;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Cast context: a chained cast (`x as A as B`) — the type RHS is just `A`;
|
|
963
|
+
// the second `as` starts a new cast on the result. Stop here so each `as`
|
|
964
|
+
// becomes its own CAST marker and the grammar nests them left-to-right.
|
|
965
|
+
// (`as` never appears inside a real type expression, so this is safe.)
|
|
966
|
+
if (opts.castContext && depth === 0 && tTag === 'IDENTIFIER' && t[1] === 'as') {
|
|
967
|
+
break;
|
|
968
|
+
}
|
|
969
|
+
|
|
832
970
|
// Delimiters that end the type at depth 0
|
|
833
971
|
if (depth === 0) {
|
|
834
972
|
// Arrow-return context: a depth-0 `=>` is the arrow OPERATOR, so it ends
|
|
@@ -855,6 +993,21 @@ function collectTypeExpression(tokens, j, opts = {}) {
|
|
|
855
993
|
tTag === '->' || tTag === ',') {
|
|
856
994
|
break;
|
|
857
995
|
}
|
|
996
|
+
// Cast context (`expr as Type`): the type RHS lives inside a larger
|
|
997
|
+
// expression, so it must also end at any depth-0 binary/relational/
|
|
998
|
+
// ternary operator. `|` and `&` are NOT stops — they're union /
|
|
999
|
+
// intersection type operators (TS reads everything after `as` as a
|
|
1000
|
+
// type, so `x as A & B` is `x as (A & B)`). COMPARE's `<`/`>` never
|
|
1001
|
+
// reach here (handled as generic brackets above), leaving only
|
|
1002
|
+
// `== != <= >=`, which do stop.
|
|
1003
|
+
if (opts.castContext && (
|
|
1004
|
+
tTag === '+' || tTag === '-' || tTag === 'MATH' || tTag === '**' ||
|
|
1005
|
+
tTag === 'SHIFT' || tTag === 'COMPARE' || tTag === '&&' ||
|
|
1006
|
+
tTag === '||' || tTag === '??' || tTag === '^' ||
|
|
1007
|
+
tTag === 'RELATION' || tTag === 'TERNARY' || tTag === '?' ||
|
|
1008
|
+
tTag === 'PRESENCE' || tTag === ':')) {
|
|
1009
|
+
break;
|
|
1010
|
+
}
|
|
858
1011
|
}
|
|
859
1012
|
|
|
860
1013
|
// Inside a bracketed type expression, INDENT/OUTDENT/TERMINATOR are
|