@nudojs/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/chunk-JW3RMCNY.js +1467 -0
- package/dist/chunk-RBHWWZII.js +1467 -0
- package/dist/chunk-VN5HMBGB.js +1467 -0
- package/dist/evaluator-api.d.ts +57 -0
- package/dist/evaluator-api.js +26 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3560 -0
- package/package.json +20 -0
- package/src/__tests__/evaluator-phase2.test.ts +564 -0
- package/src/__tests__/evaluator-phase3.test.ts +513 -0
- package/src/__tests__/evaluator.test.ts +243 -0
- package/src/__tests__/narrowing.test.ts +82 -0
- package/src/evaluator-api.ts +14 -0
- package/src/evaluator.ts +1576 -0
- package/src/index.ts +280 -0
- package/src/narrowing.ts +266 -0
- package/tsconfig.json +9 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { readFileSync, existsSync, watch, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve, dirname, relative, extname, join } from "node:path";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import {
|
|
5
|
+
T,
|
|
6
|
+
typeValueToString,
|
|
7
|
+
simplifyUnion,
|
|
8
|
+
createEnvironment,
|
|
9
|
+
isSubtypeOf,
|
|
10
|
+
} from "@nudojs/core";
|
|
11
|
+
import type { TypeValue } from "@nudojs/core";
|
|
12
|
+
import { parse, extractDirectives, parseTypeValueExpr } from "@nudojs/parser";
|
|
13
|
+
import { evaluateFunction, evaluateFunctionFull, evaluateProgram, setModuleResolver, setCurrentFileDir, resetMemo } from "./evaluator.ts";
|
|
14
|
+
|
|
15
|
+
const program = new Command();
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.name("nudo")
|
|
19
|
+
.description("Nudo type inference engine")
|
|
20
|
+
.version("0.0.1");
|
|
21
|
+
|
|
22
|
+
function applyMocks(
|
|
23
|
+
directives: ReturnType<typeof extractDirectives>[number]["directives"],
|
|
24
|
+
env: ReturnType<typeof createEnvironment>,
|
|
25
|
+
filePath: string,
|
|
26
|
+
): void {
|
|
27
|
+
for (const d of directives) {
|
|
28
|
+
if (d.kind !== "mock") continue;
|
|
29
|
+
if (d.expression) {
|
|
30
|
+
env.bind(d.name, parseTypeValueExpr(d.expression));
|
|
31
|
+
} else if (d.fromPath) {
|
|
32
|
+
const mockPath = resolve(dirname(filePath), d.fromPath);
|
|
33
|
+
const mockSource = readFileSync(mockPath, "utf-8");
|
|
34
|
+
const mockAst = parse(mockSource);
|
|
35
|
+
const mockEnv = createEnvironment();
|
|
36
|
+
evaluateProgram(mockAst, mockEnv);
|
|
37
|
+
const mockVal = mockEnv.lookup(d.name);
|
|
38
|
+
env.bind(d.name, mockVal);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveModule(source: string, fromDir: string): { ast: ReturnType<typeof parse>; filePath: string } | null {
|
|
44
|
+
const extensions = [".js", ".ts", ".mjs"];
|
|
45
|
+
const basePath = resolve(fromDir, source);
|
|
46
|
+
|
|
47
|
+
for (const ext of ["", ...extensions]) {
|
|
48
|
+
const candidate = basePath + ext;
|
|
49
|
+
if (existsSync(candidate)) {
|
|
50
|
+
const src = readFileSync(candidate, "utf-8");
|
|
51
|
+
return { ast: parse(src), filePath: candidate };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function typeValueToTSType(tv: TypeValue): string {
|
|
58
|
+
switch (tv.kind) {
|
|
59
|
+
case "literal": {
|
|
60
|
+
const v = tv.value;
|
|
61
|
+
if (v === null) return "null";
|
|
62
|
+
if (v === undefined) return "undefined";
|
|
63
|
+
if (typeof v === "string") return JSON.stringify(v);
|
|
64
|
+
return String(v);
|
|
65
|
+
}
|
|
66
|
+
case "primitive":
|
|
67
|
+
return tv.type;
|
|
68
|
+
case "object": {
|
|
69
|
+
const entries = Object.entries(tv.properties);
|
|
70
|
+
if (entries.length === 0) return "{}";
|
|
71
|
+
const inner = entries.map(([k, v]) => `${k}: ${typeValueToTSType(v)}`).join("; ");
|
|
72
|
+
return `{ ${inner} }`;
|
|
73
|
+
}
|
|
74
|
+
case "array": {
|
|
75
|
+
const el = typeValueToTSType(tv.element);
|
|
76
|
+
return tv.element.kind === "union" ? `(${el})[]` : `${el}[]`;
|
|
77
|
+
}
|
|
78
|
+
case "tuple":
|
|
79
|
+
return `[${tv.elements.map(typeValueToTSType).join(", ")}]`;
|
|
80
|
+
case "function":
|
|
81
|
+
return `(${tv.params.map((p) => `${p}: unknown`).join(", ")}) => unknown`;
|
|
82
|
+
case "promise":
|
|
83
|
+
return `Promise<${typeValueToTSType(tv.value)}>`;
|
|
84
|
+
case "instance":
|
|
85
|
+
return tv.className;
|
|
86
|
+
case "union":
|
|
87
|
+
return tv.members.map(typeValueToTSType).join(" | ");
|
|
88
|
+
case "never":
|
|
89
|
+
return "never";
|
|
90
|
+
case "unknown":
|
|
91
|
+
return "unknown";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function runInfer(file: string, options: { dts?: boolean; showLoc?: boolean } = {}): void {
|
|
96
|
+
const filePath = resolve(file);
|
|
97
|
+
const source = readFileSync(filePath, "utf-8");
|
|
98
|
+
const ast = parse(source);
|
|
99
|
+
const functions = extractDirectives(ast);
|
|
100
|
+
|
|
101
|
+
if (functions.length === 0) {
|
|
102
|
+
console.log("No functions with @nudo:case directives found.");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
resetMemo();
|
|
107
|
+
setModuleResolver(resolveModule);
|
|
108
|
+
setCurrentFileDir(dirname(filePath));
|
|
109
|
+
|
|
110
|
+
const globalEnv = createEnvironment();
|
|
111
|
+
evaluateProgram(ast, globalEnv);
|
|
112
|
+
|
|
113
|
+
const dtsLines: string[] = [];
|
|
114
|
+
|
|
115
|
+
for (const fn of functions) {
|
|
116
|
+
const loc = fn.node.loc;
|
|
117
|
+
const locStr = loc ? `${relative(process.cwd(), filePath)}:${loc.start.line}:${loc.start.column}` : relative(process.cwd(), filePath);
|
|
118
|
+
const header = options.showLoc ? `=== ${fn.name} (${locStr}) ===` : `=== ${fn.name} ===`;
|
|
119
|
+
console.log(`${header}\n`);
|
|
120
|
+
|
|
121
|
+
applyMocks(fn.directives, globalEnv, filePath);
|
|
122
|
+
|
|
123
|
+
const isPure = fn.directives.some((d) => d.kind === "pure");
|
|
124
|
+
const skipDirective = fn.directives.find((d) => d.kind === "skip");
|
|
125
|
+
const returnsDirective = fn.directives.find((d) => d.kind === "returns");
|
|
126
|
+
|
|
127
|
+
if (skipDirective && skipDirective.kind === "skip") {
|
|
128
|
+
if (skipDirective.returns) {
|
|
129
|
+
console.log(`Skipped (declared): ${typeValueToString(skipDirective.returns)}`);
|
|
130
|
+
if (options.dts) {
|
|
131
|
+
dtsLines.push(`export declare function ${fn.name}(...args: unknown[]): ${typeValueToTSType(skipDirective.returns)};`);
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
console.log("Skipped (no return type declared)");
|
|
135
|
+
}
|
|
136
|
+
console.log();
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (isPure) {
|
|
141
|
+
const fnVal = globalEnv.has(fn.name) ? globalEnv.lookup(fn.name) : null;
|
|
142
|
+
if (fnVal && fnVal.kind === "function") {
|
|
143
|
+
(fnVal as any)._memoize = fn.name;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const caseDirectives = fn.directives.filter((d) => d.kind === "case");
|
|
148
|
+
const caseResults: { name: string; args: TypeValue[]; argsStr: string; result: string; resultTV: TypeValue; throws?: string }[] = [];
|
|
149
|
+
|
|
150
|
+
for (const directive of caseDirectives) {
|
|
151
|
+
const fullResult = evaluateFunctionFull(fn.node, directive.args, globalEnv);
|
|
152
|
+
const argsStr = directive.args.map(typeValueToString).join(", ");
|
|
153
|
+
const resultStr = typeValueToString(fullResult.value);
|
|
154
|
+
const throwsStr = fullResult.throws.kind !== "never" ? typeValueToString(fullResult.throws) : undefined;
|
|
155
|
+
caseResults.push({
|
|
156
|
+
name: directive.name,
|
|
157
|
+
args: directive.args,
|
|
158
|
+
argsStr,
|
|
159
|
+
result: resultStr,
|
|
160
|
+
resultTV: fullResult.value,
|
|
161
|
+
throws: throwsStr,
|
|
162
|
+
});
|
|
163
|
+
let line = `Case "${directive.name}": (${argsStr}) => ${resultStr}`;
|
|
164
|
+
if (throwsStr) line += ` throws ${throwsStr}`;
|
|
165
|
+
console.log(line);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (caseResults.length > 1) {
|
|
169
|
+
const allResults = caseDirectives.map((d) =>
|
|
170
|
+
evaluateFunctionFull(fn.node, d.args, globalEnv),
|
|
171
|
+
);
|
|
172
|
+
const combined = simplifyUnion(allResults.map((r) => r.value));
|
|
173
|
+
console.log(`\nCombined: ${typeValueToString(combined)}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (returnsDirective && returnsDirective.kind === "returns") {
|
|
177
|
+
for (const directive of caseDirectives) {
|
|
178
|
+
const result = evaluateFunction(fn.node, directive.args, globalEnv);
|
|
179
|
+
const matches = isSubtypeOf(result, returnsDirective.expected);
|
|
180
|
+
if (!matches) {
|
|
181
|
+
console.log(`\n⚠ @nudo:returns assertion failed for case "${directive.name}": expected ${typeValueToString(returnsDirective.expected)}, got ${typeValueToString(result)}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (options.dts) {
|
|
187
|
+
if (caseResults.length === 1) {
|
|
188
|
+
const c = caseResults[0];
|
|
189
|
+
const params = c.args.map((a, i) => `arg${i}: ${typeValueToTSType(a)}`).join(", ");
|
|
190
|
+
dtsLines.push(`export declare function ${fn.name}(${params}): ${typeValueToTSType(c.resultTV)};`);
|
|
191
|
+
} else {
|
|
192
|
+
for (const c of caseResults) {
|
|
193
|
+
const params = c.args.map((a, i) => `arg${i}: ${typeValueToTSType(a)}`).join(", ");
|
|
194
|
+
dtsLines.push(`export declare function ${fn.name}(${params}): ${typeValueToTSType(c.resultTV)};`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (options.dts && dtsLines.length > 0) {
|
|
203
|
+
const dtsPath = filePath.replace(/\.js$/, ".d.ts");
|
|
204
|
+
const dtsContent = dtsLines.join("\n") + "\n";
|
|
205
|
+
writeFileSync(dtsPath, dtsContent, "utf-8");
|
|
206
|
+
console.log(`Generated: ${relative(process.cwd(), dtsPath)}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
setModuleResolver(null);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
program
|
|
213
|
+
.command("infer")
|
|
214
|
+
.description("Infer types from a JS file with @nudo: directives")
|
|
215
|
+
.argument("<file>", "Path to the JS file")
|
|
216
|
+
.option("--dts", "Generate .d.ts file")
|
|
217
|
+
.option("--loc", "Show source locations in output")
|
|
218
|
+
.action((file: string, opts: { dts?: boolean; loc?: boolean }) => {
|
|
219
|
+
runInfer(file, { dts: opts.dts, showLoc: opts.loc });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
program
|
|
223
|
+
.command("watch")
|
|
224
|
+
.description("Watch file(s) for changes and re-run inference")
|
|
225
|
+
.argument("<path>", "File or directory to watch")
|
|
226
|
+
.option("--dts", "Generate .d.ts files on each change")
|
|
227
|
+
.action((watchPath: string, opts: { dts?: boolean }) => {
|
|
228
|
+
const resolved = resolve(watchPath);
|
|
229
|
+
const isDir = existsSync(resolved) && statSync(resolved).isDirectory();
|
|
230
|
+
|
|
231
|
+
const getFiles = (): string[] => {
|
|
232
|
+
if (!isDir) return [resolved];
|
|
233
|
+
return collectNudoFiles(resolved);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const runAll = () => {
|
|
237
|
+
console.clear();
|
|
238
|
+
console.log(`[${new Date().toLocaleTimeString()}] Analyzing...\n`);
|
|
239
|
+
for (const f of getFiles()) {
|
|
240
|
+
try {
|
|
241
|
+
runInfer(f, { dts: opts.dts, showLoc: true });
|
|
242
|
+
} catch (err) {
|
|
243
|
+
console.error(`Error analyzing ${relative(process.cwd(), f)}:`, (err as Error).message);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
console.log(`[${new Date().toLocaleTimeString()}] Watching for changes...`);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
runAll();
|
|
250
|
+
|
|
251
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
252
|
+
const watchTarget = isDir ? resolved : dirname(resolved);
|
|
253
|
+
|
|
254
|
+
watch(watchTarget, { recursive: isDir }, (_event, filename) => {
|
|
255
|
+
if (!filename) return;
|
|
256
|
+
const fullPath = isDir ? join(watchTarget, filename) : resolved;
|
|
257
|
+
if (!fullPath.endsWith(".js")) return;
|
|
258
|
+
|
|
259
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
260
|
+
debounceTimer = setTimeout(runAll, 200);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
function collectNudoFiles(dir: string): string[] {
|
|
265
|
+
const results: string[] = [];
|
|
266
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
267
|
+
const fullPath = join(dir, entry.name);
|
|
268
|
+
if (entry.isDirectory() && entry.name !== "node_modules") {
|
|
269
|
+
results.push(...collectNudoFiles(fullPath));
|
|
270
|
+
} else if (entry.isFile() && entry.name.endsWith(".js")) {
|
|
271
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
272
|
+
if (/@nudo:(case|mock|pure|skip|sample|returns)\b/.test(content)) {
|
|
273
|
+
results.push(fullPath);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return results;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
program.parse();
|
package/src/narrowing.ts
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import type { Node } from "@babel/types";
|
|
2
|
+
import {
|
|
3
|
+
type TypeValue,
|
|
4
|
+
type Environment,
|
|
5
|
+
T,
|
|
6
|
+
narrowType,
|
|
7
|
+
subtractType,
|
|
8
|
+
getPrimitiveTypeOf,
|
|
9
|
+
typeValueEquals,
|
|
10
|
+
} from "@nudojs/core";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Given a test expression and the current environment, produce two
|
|
14
|
+
* environments: one where the test is truthy, one where it is falsy.
|
|
15
|
+
*/
|
|
16
|
+
export function narrow(
|
|
17
|
+
test: Node,
|
|
18
|
+
env: Environment,
|
|
19
|
+
): [trueEnv: Environment, falseEnv: Environment] {
|
|
20
|
+
// typeof x === "string"
|
|
21
|
+
if (
|
|
22
|
+
test.type === "BinaryExpression" &&
|
|
23
|
+
test.operator === "===" &&
|
|
24
|
+
isTypeofExpr(test.left) &&
|
|
25
|
+
test.right.type === "StringLiteral"
|
|
26
|
+
) {
|
|
27
|
+
const varName = getTypeofTarget(test.left);
|
|
28
|
+
if (varName) {
|
|
29
|
+
return narrowByTypeof(varName, test.right.value, env);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// "string" === typeof x
|
|
34
|
+
if (
|
|
35
|
+
test.type === "BinaryExpression" &&
|
|
36
|
+
test.operator === "===" &&
|
|
37
|
+
test.left.type === "StringLiteral" &&
|
|
38
|
+
isTypeofExpr(test.right)
|
|
39
|
+
) {
|
|
40
|
+
const varName = getTypeofTarget(test.right);
|
|
41
|
+
if (varName) {
|
|
42
|
+
return narrowByTypeof(varName, test.left.value, env);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// typeof x !== "string"
|
|
47
|
+
if (
|
|
48
|
+
test.type === "BinaryExpression" &&
|
|
49
|
+
test.operator === "!==" &&
|
|
50
|
+
isTypeofExpr(test.left) &&
|
|
51
|
+
test.right.type === "StringLiteral"
|
|
52
|
+
) {
|
|
53
|
+
const varName = getTypeofTarget(test.left);
|
|
54
|
+
if (varName) {
|
|
55
|
+
const [trueEnv, falseEnv] = narrowByTypeof(varName, test.right.value, env);
|
|
56
|
+
return [falseEnv, trueEnv];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// "string" !== typeof x
|
|
61
|
+
if (
|
|
62
|
+
test.type === "BinaryExpression" &&
|
|
63
|
+
test.operator === "!==" &&
|
|
64
|
+
test.left.type === "StringLiteral" &&
|
|
65
|
+
isTypeofExpr(test.right)
|
|
66
|
+
) {
|
|
67
|
+
const varName = getTypeofTarget(test.right);
|
|
68
|
+
if (varName) {
|
|
69
|
+
const [trueEnv, falseEnv] = narrowByTypeof(varName, test.left.value, env);
|
|
70
|
+
return [falseEnv, trueEnv];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// x === literal
|
|
75
|
+
if (
|
|
76
|
+
test.type === "BinaryExpression" &&
|
|
77
|
+
test.operator === "===" &&
|
|
78
|
+
test.left.type === "Identifier" &&
|
|
79
|
+
isLiteralNode(test.right)
|
|
80
|
+
) {
|
|
81
|
+
return narrowByStrictEqual(test.left.name, getLiteralValue(test.right), env);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// literal === x
|
|
85
|
+
if (
|
|
86
|
+
test.type === "BinaryExpression" &&
|
|
87
|
+
test.operator === "===" &&
|
|
88
|
+
isLiteralNode(test.left) &&
|
|
89
|
+
test.right.type === "Identifier"
|
|
90
|
+
) {
|
|
91
|
+
return narrowByStrictEqual(test.right.name, getLiteralValue(test.left), env);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// x !== literal
|
|
95
|
+
if (
|
|
96
|
+
test.type === "BinaryExpression" &&
|
|
97
|
+
test.operator === "!==" &&
|
|
98
|
+
test.left.type === "Identifier" &&
|
|
99
|
+
isLiteralNode(test.right)
|
|
100
|
+
) {
|
|
101
|
+
const [trueEnv, falseEnv] = narrowByStrictEqual(test.left.name, getLiteralValue(test.right), env);
|
|
102
|
+
return [falseEnv, trueEnv];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// literal !== x
|
|
106
|
+
if (
|
|
107
|
+
test.type === "BinaryExpression" &&
|
|
108
|
+
test.operator === "!==" &&
|
|
109
|
+
isLiteralNode(test.left) &&
|
|
110
|
+
test.right.type === "Identifier"
|
|
111
|
+
) {
|
|
112
|
+
const [trueEnv, falseEnv] = narrowByStrictEqual(test.right.name, getLiteralValue(test.left), env);
|
|
113
|
+
return [falseEnv, trueEnv];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// x instanceof C
|
|
117
|
+
if (
|
|
118
|
+
test.type === "BinaryExpression" &&
|
|
119
|
+
test.operator === "instanceof" &&
|
|
120
|
+
test.left.type === "Identifier" &&
|
|
121
|
+
test.right.type === "Identifier"
|
|
122
|
+
) {
|
|
123
|
+
return narrowByInstanceof(test.left.name, test.right.name, env);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// !expr (negate)
|
|
127
|
+
if (test.type === "UnaryExpression" && test.operator === "!") {
|
|
128
|
+
const [trueEnv, falseEnv] = narrow(test.argument, env);
|
|
129
|
+
return [falseEnv, trueEnv];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return [env, env];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isTypeofExpr(node: Node): boolean {
|
|
136
|
+
return node.type === "UnaryExpression" && node.operator === "typeof";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getTypeofTarget(node: Node): string | null {
|
|
140
|
+
if (
|
|
141
|
+
node.type === "UnaryExpression" &&
|
|
142
|
+
node.operator === "typeof" &&
|
|
143
|
+
node.argument.type === "Identifier"
|
|
144
|
+
) {
|
|
145
|
+
return node.argument.name;
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isLiteralNode(node: Node): boolean {
|
|
151
|
+
return (
|
|
152
|
+
node.type === "NumericLiteral" ||
|
|
153
|
+
node.type === "StringLiteral" ||
|
|
154
|
+
node.type === "BooleanLiteral" ||
|
|
155
|
+
node.type === "NullLiteral" ||
|
|
156
|
+
(node.type === "Identifier" && node.name === "undefined")
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function getLiteralValue(node: Node): TypeValue {
|
|
161
|
+
if (node.type === "NumericLiteral") return T.literal(node.value);
|
|
162
|
+
if (node.type === "StringLiteral") return T.literal(node.value);
|
|
163
|
+
if (node.type === "BooleanLiteral") return T.literal(node.value);
|
|
164
|
+
if (node.type === "NullLiteral") return T.null;
|
|
165
|
+
if (node.type === "Identifier" && node.name === "undefined") return T.undefined;
|
|
166
|
+
return T.unknown;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const typeofToPrimitive: Record<string, TypeValue> = {
|
|
170
|
+
number: T.number,
|
|
171
|
+
string: T.string,
|
|
172
|
+
boolean: T.boolean,
|
|
173
|
+
bigint: T.bigint,
|
|
174
|
+
symbol: T.symbol,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
function narrowByTypeof(
|
|
178
|
+
varName: string,
|
|
179
|
+
typeStr: string,
|
|
180
|
+
env: Environment,
|
|
181
|
+
): [Environment, Environment] {
|
|
182
|
+
const current = env.lookup(varName);
|
|
183
|
+
const targetPrimitive = typeofToPrimitive[typeStr];
|
|
184
|
+
|
|
185
|
+
const matchesPrimitive = (m: TypeValue): boolean => {
|
|
186
|
+
const pt = getPrimitiveTypeOf(m);
|
|
187
|
+
return pt === typeStr;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
if (targetPrimitive) {
|
|
191
|
+
const narrowed = narrowType(current, matchesPrimitive);
|
|
192
|
+
const excluded = subtractType(current, matchesPrimitive);
|
|
193
|
+
|
|
194
|
+
const trueEnv = env.extend({});
|
|
195
|
+
trueEnv.bind(varName, narrowed.kind === "never" ? targetPrimitive : narrowed);
|
|
196
|
+
const falseEnv = env.extend({});
|
|
197
|
+
falseEnv.bind(varName, excluded.kind === "never" ? current : excluded);
|
|
198
|
+
return [trueEnv, falseEnv];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (typeStr === "object") {
|
|
202
|
+
const narrowed = narrowType(current, (m) => {
|
|
203
|
+
const pt = getPrimitiveTypeOf(m);
|
|
204
|
+
return pt === "object";
|
|
205
|
+
});
|
|
206
|
+
const excluded = subtractType(current, (m) => {
|
|
207
|
+
const pt = getPrimitiveTypeOf(m);
|
|
208
|
+
return pt === "object";
|
|
209
|
+
});
|
|
210
|
+
const trueEnv = env.extend({});
|
|
211
|
+
trueEnv.bind(varName, narrowed.kind === "never" ? current : narrowed);
|
|
212
|
+
const falseEnv = env.extend({});
|
|
213
|
+
falseEnv.bind(varName, excluded.kind === "never" ? current : excluded);
|
|
214
|
+
return [trueEnv, falseEnv];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (typeStr === "function") {
|
|
218
|
+
const narrowed = narrowType(current, (m) => m.kind === "function");
|
|
219
|
+
const excluded = subtractType(current, (m) => m.kind === "function");
|
|
220
|
+
const trueEnv = env.extend({});
|
|
221
|
+
trueEnv.bind(varName, narrowed.kind === "never" ? current : narrowed);
|
|
222
|
+
const falseEnv = env.extend({});
|
|
223
|
+
falseEnv.bind(varName, excluded.kind === "never" ? current : excluded);
|
|
224
|
+
return [trueEnv, falseEnv];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return [env, env];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function narrowByInstanceof(
|
|
231
|
+
varName: string,
|
|
232
|
+
className: string,
|
|
233
|
+
env: Environment,
|
|
234
|
+
): [Environment, Environment] {
|
|
235
|
+
const current = env.lookup(varName);
|
|
236
|
+
|
|
237
|
+
const narrowed = narrowType(current, (m) =>
|
|
238
|
+
m.kind === "instance" && m.className === className,
|
|
239
|
+
);
|
|
240
|
+
const excluded = subtractType(current, (m) =>
|
|
241
|
+
m.kind === "instance" && m.className === className,
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const trueEnv = env.extend({});
|
|
245
|
+
trueEnv.bind(varName, narrowed.kind === "never" ? T.instanceOf(className) : narrowed);
|
|
246
|
+
const falseEnv = env.extend({});
|
|
247
|
+
falseEnv.bind(varName, excluded.kind === "never" ? current : excluded);
|
|
248
|
+
return [trueEnv, falseEnv];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function narrowByStrictEqual(
|
|
252
|
+
varName: string,
|
|
253
|
+
literalTV: TypeValue,
|
|
254
|
+
env: Environment,
|
|
255
|
+
): [Environment, Environment] {
|
|
256
|
+
const current = env.lookup(varName);
|
|
257
|
+
|
|
258
|
+
const narrowed = narrowType(current, (m) => typeValueEquals(m, literalTV));
|
|
259
|
+
const excluded = subtractType(current, (m) => typeValueEquals(m, literalTV));
|
|
260
|
+
|
|
261
|
+
const trueEnv = env.extend({});
|
|
262
|
+
trueEnv.bind(varName, narrowed.kind === "never" ? literalTV : narrowed);
|
|
263
|
+
const falseEnv = env.extend({});
|
|
264
|
+
falseEnv.bind(varName, excluded.kind === "never" ? current : excluded);
|
|
265
|
+
return [trueEnv, falseEnv];
|
|
266
|
+
}
|