@razinshafayet/typedjs 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/.vscode/settings.json +15 -0
- package/README.md +98 -0
- package/eslint.config.cjs +23 -0
- package/example.js +14 -0
- package/package.json +41 -0
- package/src/analyzer/analyzer.js +159 -0
- package/src/cli.js +42 -0
- package/src/generator/generator.js +261 -0
- package/src/parser/parser.js +136 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"eslint.options": {
|
|
3
|
+
"overrideConfig": {
|
|
4
|
+
"languageOptions": {
|
|
5
|
+
"parser": "/home/razin/.antigravity/extensions/razinshafayet.typedjs-vscode-0.0.2-universal/server/parser.js"
|
|
6
|
+
},
|
|
7
|
+
"rules": {
|
|
8
|
+
"no-op": "error"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"rulePaths": [
|
|
12
|
+
"/home/razin/.antigravity/extensions/razinshafayet.typedjs-vscode-0.0.2-universal/server/rules"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# TypedJS
|
|
2
|
+
|
|
3
|
+
A lightweight, runtime-checkable type system for JavaScript. It brings the safety of TypeScript with the simplicity of running directly in Node.js (via a transparent runtime).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Runtime Type Checking**: Catches type errors as your code runs (Development Mode).
|
|
8
|
+
- **Fast Production Mode**: Strips checks for raw JavaScript speed (matching TypeScript performance).
|
|
9
|
+
- **No Build Step**: Run `.js` files directly with the `typedjs` CLI.
|
|
10
|
+
- **Support for Modern Types**: Includes Maps, Sets, Tuples, and Unions.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g typedjs
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
### 1. Development Mode (Safe)
|
|
21
|
+
Runs your code with full runtime type checking. Great for debugging.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
typedjs app.js
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 2. Production Mode (Fast)
|
|
28
|
+
Strips all types and runtime checks. Executes as fast as raw JavaScript. Use this for deployment.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
typedjs app.js --prod
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Supported Type Annotations
|
|
35
|
+
|
|
36
|
+
TypedJS supports a subset of TypeScript syntax, focused on runtime-validatable types.
|
|
37
|
+
|
|
38
|
+
### Primitives
|
|
39
|
+
```javascript
|
|
40
|
+
let name: string = "Razin";
|
|
41
|
+
let age: number = 25;
|
|
42
|
+
let isActive: boolean = true;
|
|
43
|
+
let empty: null = null;
|
|
44
|
+
let notDefined: undefined = undefined;
|
|
45
|
+
let big: bigint = 9007199254740991n;
|
|
46
|
+
let sym: symbol = Symbol("id");
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Interfaces & Objects
|
|
50
|
+
```javascript
|
|
51
|
+
interface User {
|
|
52
|
+
id: number;
|
|
53
|
+
name: string;
|
|
54
|
+
email?: string; // Optional property
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let user: User = {
|
|
58
|
+
id: 1,
|
|
59
|
+
name: "Razin"
|
|
60
|
+
};
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Type Aliases & Unions
|
|
64
|
+
```javascript
|
|
65
|
+
type ID = string | number;
|
|
66
|
+
type Status = "active" | "inactive" | "pending"; // Literal Unions
|
|
67
|
+
|
|
68
|
+
let myId: ID = 123;
|
|
69
|
+
let status: Status = "active";
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Arrays & Tuples
|
|
73
|
+
```javascript
|
|
74
|
+
let scores: Array<number> = [10, 20, 30];
|
|
75
|
+
let point: [number, number] = [10, 20]; // Tuple
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Maps & Sets
|
|
79
|
+
```javascript
|
|
80
|
+
let map: Map<string, number> = new Map();
|
|
81
|
+
let set: Set<string> = new Set();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Functions
|
|
85
|
+
```javascript
|
|
86
|
+
function add(a: number, b: number): number {
|
|
87
|
+
return a + b;
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## How It Works
|
|
92
|
+
|
|
93
|
+
TypedJS includes a smart parser and runtime generator:
|
|
94
|
+
1. **Parser**: Reads your TypedJS syntax (using `acorn-typescript`).
|
|
95
|
+
2. **Analyzer**: Performs static analysis to catch obvious errors early.
|
|
96
|
+
3. **Generator**:
|
|
97
|
+
* **Dev Mode**: Injects `__checkType__` calls around every variable assignment and function call.
|
|
98
|
+
* **Prod Mode**: Removes all types and checks, outputting pure optimized JavaScript.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const parser = require("/home/razin/.vscode/extensions/razinshafayet.typedjs-vscode-0.0.1/server/parser.js");
|
|
2
|
+
const noOp = require("/home/razin/.vscode/extensions/razinshafayet.typedjs-vscode-0.0.1/server/rules/no-op.js");
|
|
3
|
+
|
|
4
|
+
const typedjsPlugin = {
|
|
5
|
+
rules: {
|
|
6
|
+
"no-op": noOp
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
module.exports = [
|
|
11
|
+
{
|
|
12
|
+
files: ["**/*.js"],
|
|
13
|
+
languageOptions: {
|
|
14
|
+
parser: parser
|
|
15
|
+
},
|
|
16
|
+
plugins: {
|
|
17
|
+
typedjs: typedjsPlugin
|
|
18
|
+
},
|
|
19
|
+
rules: {
|
|
20
|
+
"typedjs/no-op": "error"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
];
|
package/example.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@razinshafayet/typedjs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Runtime type checking for JavaScript",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"typedjs": "./src/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"typescript",
|
|
12
|
+
"javascript",
|
|
13
|
+
"types",
|
|
14
|
+
"runtime",
|
|
15
|
+
"validation",
|
|
16
|
+
"type-checking",
|
|
17
|
+
"type-system"
|
|
18
|
+
],
|
|
19
|
+
"author": "Razin Shafayet <curiostymaster77@gmail.com>",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/RazinShafayet2007/typedjs.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/RazinShafayet2007/typedjs/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/RazinShafayet2007/typedjs#readme",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=14.0.0"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"acorn": "^8.15.0",
|
|
34
|
+
"acorn-typescript": "^1.4.13",
|
|
35
|
+
"escodegen": "^2.1.0",
|
|
36
|
+
"estree-walker": "^3.0.3"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"eslint": "^9.39.2"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Updated src/analyzer/analyzer.js - Fixed ESM import + proper walk for enclosing function (no this.parent)
|
|
2
|
+
|
|
3
|
+
import { walk } from 'estree-walker';
|
|
4
|
+
|
|
5
|
+
export function staticAnalyze(typeRegistry, ast) {
|
|
6
|
+
const errors = [];
|
|
7
|
+
|
|
8
|
+
// Helper to check if value matches type (for literals/constants)
|
|
9
|
+
function matchesType(value, type) {
|
|
10
|
+
if (Array.isArray(type)) { // Union
|
|
11
|
+
return type.some(member => matchesType(value, member));
|
|
12
|
+
}
|
|
13
|
+
if (type.kind === 'literal') {
|
|
14
|
+
return value === type.value;
|
|
15
|
+
}
|
|
16
|
+
if (typeof type === 'string') {
|
|
17
|
+
if (type === 'any' || type === 'unknown') return true;
|
|
18
|
+
if (type === 'string') return typeof value === 'string';
|
|
19
|
+
if (type === 'number') return typeof value === 'number';
|
|
20
|
+
if (type === 'boolean') return typeof value === 'boolean';
|
|
21
|
+
if (type === 'null') return value === null;
|
|
22
|
+
if (type === 'undefined') return value === undefined;
|
|
23
|
+
if (type === 'bigint') return typeof value === 'bigint';
|
|
24
|
+
if (type === 'symbol') return typeof value === 'symbol';
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
// Structured types: skip static check for basic version
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function checkType(name, valueNode, type, errors) {
|
|
32
|
+
// 1. Literal Check
|
|
33
|
+
if (valueNode.type === 'Literal' || valueNode.type === 'BigIntLiteral') {
|
|
34
|
+
const value = valueNode.value ?? valueNode.bigint;
|
|
35
|
+
if (!matchesType(value, type)) {
|
|
36
|
+
errors.push(`[Static type error] Property '${name}' got ${JSON.stringify(value)}, expected ${typeToString(type)}`);
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 2. Object Check
|
|
42
|
+
if (valueNode.type === 'ObjectExpression' && typeof type === 'object' && type.kind !== 'literal') {
|
|
43
|
+
checkObject(name, valueNode, type, errors);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 3. Array Check (Simple)
|
|
48
|
+
if (valueNode.type === 'ArrayExpression' && type.kind === 'array') {
|
|
49
|
+
valueNode.elements.forEach((elem, i) => {
|
|
50
|
+
if (elem) checkType(`${name}[${i}]`, elem, type.elementType, errors);
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function checkObject(name, objectExpr, type, errors) {
|
|
57
|
+
if (typeof type !== 'object' || type === null) return; // Can't check primitive vs object here easily without more logic
|
|
58
|
+
|
|
59
|
+
// Convert AST ObjectExpression props to a map for easy lookup
|
|
60
|
+
const props = {};
|
|
61
|
+
objectExpr.properties.forEach(p => {
|
|
62
|
+
if (p.type === 'Property' && p.key.type === 'Identifier') {
|
|
63
|
+
props[p.key.name] = p.value;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Check against interface definition (type is the shape object)
|
|
68
|
+
for (const [key, expectedType] of Object.entries(type)) {
|
|
69
|
+
const isOptional = expectedType.kind === 'optional';
|
|
70
|
+
const actualType = isOptional ? expectedType.type : expectedType;
|
|
71
|
+
|
|
72
|
+
if (!(key in props)) {
|
|
73
|
+
if (!isOptional) {
|
|
74
|
+
errors.push(`[Static type error] Property '${key}' is missing in object '${name}' (expected type ${typeToString(type)})`);
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const valueNode = props[key];
|
|
80
|
+
checkType(`${name}.${key}`, valueNode, actualType, errors);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Map variable names to types
|
|
85
|
+
const varTypes = {};
|
|
86
|
+
typeRegistry.forEach(entry => {
|
|
87
|
+
if (entry.kind === 'variable') {
|
|
88
|
+
varTypes[entry.name] = entry.type;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Map function names to return types
|
|
93
|
+
const funcReturnTypes = {};
|
|
94
|
+
typeRegistry.forEach(entry => {
|
|
95
|
+
if (entry.kind === 'function' && entry.returnType !== 'any') {
|
|
96
|
+
funcReturnTypes[entry.name] = entry.returnType;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
let currentFuncName = null;
|
|
101
|
+
|
|
102
|
+
walk(ast, {
|
|
103
|
+
enter(node) {
|
|
104
|
+
// Track current function
|
|
105
|
+
if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
|
|
106
|
+
currentFuncName = node.id?.name || null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check variable initializer (only simple literals)
|
|
110
|
+
if (node.type === 'VariableDeclarator' && node.init && varTypes[node.id.name]) {
|
|
111
|
+
if (node.init.type === 'Literal' || node.init.type === 'BigIntLiteral') {
|
|
112
|
+
const value = node.init.value ?? node.init.bigint;
|
|
113
|
+
const declaredType = varTypes[node.id.name];
|
|
114
|
+
if (!matchesType(value, declaredType)) {
|
|
115
|
+
errors.push(`[Static type error] Variable '${node.id.name}' initializer ${JSON.stringify(value)} does not match declared type ${typeToString(declaredType)}`);
|
|
116
|
+
}
|
|
117
|
+
} else if (node.init.type === 'ObjectExpression') {
|
|
118
|
+
const declaredType = varTypes[node.id.name];
|
|
119
|
+
checkObject(node.id.name, node.init, declaredType, errors);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check literal return
|
|
124
|
+
if (node.type === 'ReturnStatement' && node.argument && (node.argument.type === 'Literal' || node.argument.type === 'BigIntLiteral') && currentFuncName && funcReturnTypes[currentFuncName]) {
|
|
125
|
+
const value = node.argument.value ?? node.argument.bigint;
|
|
126
|
+
const returnType = funcReturnTypes[currentFuncName];
|
|
127
|
+
if (!matchesType(value, returnType)) {
|
|
128
|
+
errors.push(`[Static type error] Function '${currentFuncName}' returns ${JSON.stringify(value)} which does not match return type ${typeToString(returnType)}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
leave(node) {
|
|
133
|
+
// Clear current function on leave
|
|
134
|
+
if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
|
|
135
|
+
currentFuncName = null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Helper for nice type strings in errors (reuse from generator or simple)
|
|
141
|
+
function typeToString(t) {
|
|
142
|
+
if (Array.isArray(t)) return t.map(typeToString).join(' | ');
|
|
143
|
+
if (typeof t === 'object' && t?.kind === 'literal') return JSON.stringify(t.value);
|
|
144
|
+
return t;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Print static errors
|
|
148
|
+
if (errors.length > 0) {
|
|
149
|
+
console.log('\n--- Static Type Errors ---');
|
|
150
|
+
errors.forEach(err => console.log(err));
|
|
151
|
+
console.log('--- End Static Errors ---\n');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return errors.length === 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function analyze(typeRegistry) {
|
|
158
|
+
return typeRegistry;
|
|
159
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { parseCode } from "./parser/parser.js";
|
|
5
|
+
import { staticAnalyze, analyze } from "./analyzer/analyzer.js"; // Add staticAnalyze
|
|
6
|
+
import { generate } from "./generator/generator.js";
|
|
7
|
+
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
|
|
10
|
+
const isProd = args.includes('--prod');
|
|
11
|
+
const fileArg = args.find(a => !a.startsWith('--'));
|
|
12
|
+
|
|
13
|
+
if (!fileArg) {
|
|
14
|
+
console.error("Usage: typedjs <file.js> [--prod]");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const filePath = path.resolve(fileArg);
|
|
19
|
+
const source = fs.readFileSync(filePath, "utf-8");
|
|
20
|
+
|
|
21
|
+
const { ast, typeRegistry } = parseCode(source);
|
|
22
|
+
|
|
23
|
+
// Static analysis (Always run it, but in prod it's CRITICAL)
|
|
24
|
+
const staticErrors = staticAnalyze(typeRegistry, ast);
|
|
25
|
+
if (!staticErrors && isProd) { // staticAnalyze returns false if errors found (wait, logic check)
|
|
26
|
+
// staticAnalyze returns boolean? Let's check.
|
|
27
|
+
// It returns `errors.length === 0`. So true means OK.
|
|
28
|
+
console.error("Build failed due to static type errors.");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Old minimal analyze (deprecated but keeping for now)
|
|
33
|
+
analyze(typeRegistry);
|
|
34
|
+
|
|
35
|
+
const output = generate(ast, typeRegistry, isProd ? 'production' : 'development');
|
|
36
|
+
|
|
37
|
+
const tmpFile = path.resolve("./typedjs_temp.js");
|
|
38
|
+
fs.writeFileSync(tmpFile, output);
|
|
39
|
+
|
|
40
|
+
await import(tmpFile);
|
|
41
|
+
|
|
42
|
+
fs.unlinkSync(tmpFile);
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// Updated src/generator/generator.js - Added runtime checks for null, undefined, bigint, symbol
|
|
2
|
+
|
|
3
|
+
import escodegen from 'escodegen';
|
|
4
|
+
import { walk } from 'estree-walker';
|
|
5
|
+
|
|
6
|
+
function transformAst(ast, typeRegistry, mode) {
|
|
7
|
+
// Remove compile-time TS nodes
|
|
8
|
+
if (ast.body) {
|
|
9
|
+
ast.body = ast.body.filter(node =>
|
|
10
|
+
node.type !== 'TSInterfaceDeclaration' &&
|
|
11
|
+
node.type !== 'TSTypeAliasDeclaration'
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// If production mode, stop here! (No runtime checks)
|
|
16
|
+
if (mode === 'production') {
|
|
17
|
+
return ast;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Helpers for check injection
|
|
21
|
+
function createCheckCall(name, valueExpr, type) {
|
|
22
|
+
return {
|
|
23
|
+
type: 'CallExpression',
|
|
24
|
+
callee: { type: 'Identifier', name: '__checkType__' },
|
|
25
|
+
arguments: [
|
|
26
|
+
{ type: 'Literal', value: name },
|
|
27
|
+
valueExpr,
|
|
28
|
+
{ type: 'Literal', value: JSON.stringify(type) }
|
|
29
|
+
]
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createReturnCheckCall(valueExpr, type) {
|
|
34
|
+
return {
|
|
35
|
+
type: 'CallExpression',
|
|
36
|
+
callee: { type: 'Identifier', name: '__checkReturnType__' },
|
|
37
|
+
arguments: [
|
|
38
|
+
valueExpr,
|
|
39
|
+
{ type: 'Literal', value: JSON.stringify(type) }
|
|
40
|
+
]
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const funcTypes = {};
|
|
45
|
+
typeRegistry.forEach(entry => {
|
|
46
|
+
if (entry.kind === 'function') {
|
|
47
|
+
funcTypes[entry.name] = entry;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
walk(ast, {
|
|
52
|
+
enter(node) {
|
|
53
|
+
if (node.typeAnnotation) node.typeAnnotation = null;
|
|
54
|
+
if (node.returnType) node.returnType = null;
|
|
55
|
+
|
|
56
|
+
if (node.type === 'FunctionDeclaration' && node.id && funcTypes[node.id.name]?.params?.length > 0) {
|
|
57
|
+
const params = funcTypes[node.id.name].params;
|
|
58
|
+
const checkStmts = params.map(p => ({
|
|
59
|
+
type: 'VariableDeclaration',
|
|
60
|
+
kind: 'const',
|
|
61
|
+
declarations: [{
|
|
62
|
+
type: 'VariableDeclarator',
|
|
63
|
+
id: { type: 'Identifier', name: p.name },
|
|
64
|
+
init: createCheckCall(p.name, { type: 'Identifier', name: `__arg_${p.name}` }, p.type)
|
|
65
|
+
}]
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
node.params = node.params.map((param, i) => {
|
|
69
|
+
if (params[i]) param.name = `__arg_${param.name}`;
|
|
70
|
+
return param;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
node.body.body = [...checkStmts, ...node.body.body];
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
leave(node, parent) {
|
|
77
|
+
if (node.type === 'ReturnStatement' && parent?.type === 'BlockStatement' && parent.parent?.type === 'FunctionDeclaration' && parent.parent.id) {
|
|
78
|
+
const funcName = parent.parent.id.name;
|
|
79
|
+
const returnType = funcTypes[funcName]?.returnType;
|
|
80
|
+
if (returnType && returnType !== 'any' && node.argument) {
|
|
81
|
+
node.argument = createReturnCheckCall(node.argument, returnType);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return ast;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function generate(ast, typeRegistry, mode = 'development') {
|
|
91
|
+
const transformedAst = transformAst(ast, typeRegistry, mode);
|
|
92
|
+
|
|
93
|
+
let transformed = escodegen.generate(transformedAst, {
|
|
94
|
+
format: { indent: { style: ' ' } },
|
|
95
|
+
comment: true,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (mode === 'production') {
|
|
99
|
+
return transformed;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const helpers = `
|
|
103
|
+
function typeToString(t) {
|
|
104
|
+
if (Array.isArray(t)) {
|
|
105
|
+
return t.map(typeToString).join(' | ');
|
|
106
|
+
}
|
|
107
|
+
if (typeof t === 'object' && t !== null) {
|
|
108
|
+
if (t.kind === 'literal') return JSON.stringify(t.value);
|
|
109
|
+
if (t.kind === 'array') return 'Array<' + typeToString(t.elementType) + '>';
|
|
110
|
+
if (t.kind === 'map') return 'Map<' + typeToString(t.keyType) + ', ' + typeToString(t.valueType) + '>';
|
|
111
|
+
if (t.kind === 'set') return 'Set<' + typeToString(t.elementType) + '>';
|
|
112
|
+
if (t.kind === 'tuple') return '[' + t.elements.map(typeToString).join(', ') + ']';
|
|
113
|
+
if (t.kind === 'optional') return typeToString(t.type) + '?';
|
|
114
|
+
return '{' + Object.keys(t).map(k => k + ': ' + typeToString(t[k])).join(', ') + '}';
|
|
115
|
+
}
|
|
116
|
+
return t;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function __checkType__(name, value, typeJson) {
|
|
120
|
+
const type = JSON.parse(typeJson);
|
|
121
|
+
const typeStr = typeToString(type);
|
|
122
|
+
|
|
123
|
+
// Union
|
|
124
|
+
if (Array.isArray(type)) {
|
|
125
|
+
const ok = type.some(member => {
|
|
126
|
+
if (typeof member === 'string') {
|
|
127
|
+
if (member === 'string') return typeof value === 'string';
|
|
128
|
+
if (member === 'number') return typeof value === 'number';
|
|
129
|
+
if (member === 'boolean') return typeof value === 'boolean';
|
|
130
|
+
if (member === 'null') return value === null;
|
|
131
|
+
if (member === 'undefined') return value === undefined;
|
|
132
|
+
if (member === 'bigint') return typeof value === 'bigint';
|
|
133
|
+
if (member === 'symbol') return typeof value === 'symbol';
|
|
134
|
+
if (member === 'unknown') return true;
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
if (member.kind === 'literal') {
|
|
138
|
+
return value === member.value;
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
});
|
|
142
|
+
if (!ok) {
|
|
143
|
+
console.warn('[Type warning] ' + name + ' expected ' + typeStr + ', got ' + JSON.stringify(value));
|
|
144
|
+
}
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Single literal
|
|
149
|
+
if (type.kind === 'literal') {
|
|
150
|
+
if (value !== type.value) {
|
|
151
|
+
console.warn('[Type warning] ' + name + ' expected ' + typeStr + ', got ' + JSON.stringify(value));
|
|
152
|
+
}
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Primitive (including new ones)
|
|
157
|
+
if (typeof type === 'string') {
|
|
158
|
+
if (type === 'any') return value;
|
|
159
|
+
const allowed = type.split('|').map(s => s.trim());
|
|
160
|
+
const ok = allowed.some(a => {
|
|
161
|
+
if (a === 'string') return typeof value === 'string';
|
|
162
|
+
if (a === 'number') return typeof value === 'number';
|
|
163
|
+
if (a === 'boolean') return typeof value === 'boolean';
|
|
164
|
+
if (a === 'null') return value === null;
|
|
165
|
+
if (a === 'undefined') return value === undefined;
|
|
166
|
+
if (a === 'bigint') return typeof value === 'bigint';
|
|
167
|
+
if (a === 'symbol') return typeof value === 'symbol';
|
|
168
|
+
if (a === 'unknown') return true;
|
|
169
|
+
return false;
|
|
170
|
+
});
|
|
171
|
+
if (!ok) console.warn('[Type warning] ' + name + ' expected ' + typeStr + ', got ' + typeof value);
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Structured types (unchanged)
|
|
176
|
+
if (typeof type === 'object' && type !== null) {
|
|
177
|
+
if (type.kind === 'array') {
|
|
178
|
+
if (!Array.isArray(value)) {
|
|
179
|
+
console.warn('[Type warning] ' + name + ' expected ' + typeStr + ', got ' + typeof value);
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
value.forEach((item, i) => {
|
|
183
|
+
__checkType__(name + '[' + i + ']', item, JSON.stringify(type.elementType));
|
|
184
|
+
});
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (type.kind === 'map') {
|
|
189
|
+
if (!(value instanceof Map)) {
|
|
190
|
+
console.warn('[Type warning] ' + name + ' expected ' + typeStr + ', got ' + typeof value);
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
for (const [k, v] of value.entries()) {
|
|
194
|
+
__checkType__(name + '.key', k, JSON.stringify(type.keyType));
|
|
195
|
+
__checkType__(name + '.value', v, JSON.stringify(type.valueType));
|
|
196
|
+
}
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (type.kind === 'set') {
|
|
201
|
+
if (!(value instanceof Set)) {
|
|
202
|
+
console.warn('[Type warning] ' + name + ' expected ' + typeStr + ', got ' + typeof value);
|
|
203
|
+
return value;
|
|
204
|
+
}
|
|
205
|
+
let i = 0;
|
|
206
|
+
for (const item of value) {
|
|
207
|
+
__checkType__(name + '[#' + i + ']', item, JSON.stringify(type.elementType));
|
|
208
|
+
i++;
|
|
209
|
+
}
|
|
210
|
+
return value;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (type.kind === 'tuple') {
|
|
214
|
+
if (!Array.isArray(value)) {
|
|
215
|
+
console.warn('[Type warning] ' + name + ' expected ' + typeStr + ', got ' + typeof value);
|
|
216
|
+
return value;
|
|
217
|
+
}
|
|
218
|
+
if (value.length !== type.elements.length) {
|
|
219
|
+
console.warn('[Type warning] ' + name + ' expected tuple length ' + type.elements.length + ', got ' + value.length);
|
|
220
|
+
}
|
|
221
|
+
type.elements.forEach((et, i) => {
|
|
222
|
+
if (i < value.length) {
|
|
223
|
+
__checkType__(name + '[' + i + ']', value[i], JSON.stringify(et));
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
return value;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (type.kind === 'optional') {
|
|
230
|
+
if (value === undefined) return value;
|
|
231
|
+
return __checkType__(name, value, JSON.stringify(type.type));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Object shape
|
|
235
|
+
if (typeof value !== 'object' || value === null) {
|
|
236
|
+
console.warn('[Type warning] ' + name + ' expected ' + typeStr + ', got ' + typeof value);
|
|
237
|
+
return value;
|
|
238
|
+
}
|
|
239
|
+
for (const [prop, pt] of Object.entries(type)) {
|
|
240
|
+
const actualType = pt.kind === 'optional' ? pt.type : pt;
|
|
241
|
+
if (pt.kind === 'optional' && value[prop] === undefined) continue;
|
|
242
|
+
__checkType__(name + '.' + prop, value[prop], JSON.stringify(actualType));
|
|
243
|
+
}
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return value;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function __checkReturnType__(value, typeJson) {
|
|
251
|
+
return __checkType__('return', value, typeJson);
|
|
252
|
+
}
|
|
253
|
+
`;
|
|
254
|
+
|
|
255
|
+
const varChecks = typeRegistry
|
|
256
|
+
.filter(e => e.kind === 'variable')
|
|
257
|
+
.map(({ name, type }) => `__checkType__('${name}', ${name}, ${JSON.stringify(JSON.stringify(type))});`)
|
|
258
|
+
.join('\n');
|
|
259
|
+
|
|
260
|
+
return `${helpers}\n${transformed}\n${varChecks}`;
|
|
261
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Updated src/parser/parser.js - Added support for null, undefined, bigint, symbol
|
|
2
|
+
|
|
3
|
+
import { Parser } from "acorn";
|
|
4
|
+
import ts from "acorn-typescript";
|
|
5
|
+
import { walk } from 'estree-walker';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Convert a TypeScript AST type node into structured representation
|
|
9
|
+
*/
|
|
10
|
+
function tsTypeToString(typeNode, registry = []) {
|
|
11
|
+
switch (typeNode.type) {
|
|
12
|
+
case "TSNullKeyword":
|
|
13
|
+
return "null";
|
|
14
|
+
case "TSUndefinedKeyword":
|
|
15
|
+
return "undefined";
|
|
16
|
+
case "TSBigIntKeyword":
|
|
17
|
+
return "bigint";
|
|
18
|
+
case "TSSymbolKeyword":
|
|
19
|
+
return "symbol";
|
|
20
|
+
case "TSStringKeyword":
|
|
21
|
+
return "string";
|
|
22
|
+
case "TSNumberKeyword":
|
|
23
|
+
return "number";
|
|
24
|
+
case "TSBooleanKeyword":
|
|
25
|
+
return "boolean";
|
|
26
|
+
case "TSLiteralType":
|
|
27
|
+
return { kind: 'literal', value: typeNode.literal.value };
|
|
28
|
+
case "TSTypeReference":
|
|
29
|
+
const refName = typeNode.typeName.name;
|
|
30
|
+
if (refName === 'Array' && typeNode.typeParameters?.params?.length > 0) {
|
|
31
|
+
return { kind: 'array', elementType: tsTypeToString(typeNode.typeParameters.params[0], registry) };
|
|
32
|
+
}
|
|
33
|
+
if (refName === 'Map' && typeNode.typeParameters?.params?.length === 2) {
|
|
34
|
+
return { kind: 'map', keyType: tsTypeToString(typeNode.typeParameters.params[0], registry), valueType: tsTypeToString(typeNode.typeParameters.params[1], registry) };
|
|
35
|
+
}
|
|
36
|
+
if (refName === 'Set' && typeNode.typeParameters?.params?.length > 0) {
|
|
37
|
+
return { kind: 'set', elementType: tsTypeToString(typeNode.typeParameters.params[0], registry) };
|
|
38
|
+
}
|
|
39
|
+
const iface = registry.find(e => e.kind === 'interface' && e.name === refName);
|
|
40
|
+
return iface ? { ...iface.shape } : refName;
|
|
41
|
+
case "TSUnionType":
|
|
42
|
+
return typeNode.types.map(t => tsTypeToString(t, registry));
|
|
43
|
+
case "TSTypeLiteral":
|
|
44
|
+
const shape = {};
|
|
45
|
+
typeNode.members.forEach(m => {
|
|
46
|
+
if (m.type === 'TSPropertySignature') {
|
|
47
|
+
const name = m.key.name;
|
|
48
|
+
const optional = !!m.optional;
|
|
49
|
+
const propType = tsTypeToString(m.typeAnnotation.typeAnnotation, registry);
|
|
50
|
+
shape[name] = optional ? { kind: 'optional', type: propType } : propType;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return shape;
|
|
54
|
+
case "TSTupleType":
|
|
55
|
+
return { kind: 'tuple', elements: typeNode.elementTypes.map(et => tsTypeToString(et, registry)) };
|
|
56
|
+
default:
|
|
57
|
+
return "unknown";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parse code and collect type registry
|
|
63
|
+
*/
|
|
64
|
+
export function parseCode(source) {
|
|
65
|
+
const parser = Parser.extend(ts());
|
|
66
|
+
const ast = parser.parse(source, { ecmaVersion: 2024, sourceType: "module" });
|
|
67
|
+
|
|
68
|
+
const typeRegistry = [];
|
|
69
|
+
|
|
70
|
+
// Pass 1: Interfaces + type aliases
|
|
71
|
+
walk(ast, {
|
|
72
|
+
enter(node) {
|
|
73
|
+
if (node.type === "TSInterfaceDeclaration") {
|
|
74
|
+
const shape = {};
|
|
75
|
+
node.body.body.forEach(m => {
|
|
76
|
+
if (m.type === 'TSPropertySignature') {
|
|
77
|
+
const name = m.key.name;
|
|
78
|
+
const optional = !!m.optional;
|
|
79
|
+
const propType = tsTypeToString(m.typeAnnotation.typeAnnotation, typeRegistry);
|
|
80
|
+
shape[name] = optional ? { kind: 'optional', type: propType } : propType;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
typeRegistry.push({ kind: "interface", name: node.id.name, shape });
|
|
84
|
+
} else if (node.type === "TSTypeAliasDeclaration") {
|
|
85
|
+
const aliasType = tsTypeToString(node.typeAnnotation, typeRegistry);
|
|
86
|
+
typeRegistry.push({ kind: "typeAlias", name: node.id.name, type: aliasType });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Pass 2: Variables + functions (resolve aliases)
|
|
92
|
+
walk(ast, {
|
|
93
|
+
enter(node) {
|
|
94
|
+
if (node.type === "VariableDeclaration") {
|
|
95
|
+
for (const decl of node.declarations) {
|
|
96
|
+
if (decl.id.typeAnnotation) {
|
|
97
|
+
let type = tsTypeToString(decl.id.typeAnnotation.typeAnnotation, typeRegistry);
|
|
98
|
+
if (typeof type === 'string') {
|
|
99
|
+
const alias = typeRegistry.find(e => e.kind === 'typeAlias' && e.name === type);
|
|
100
|
+
if (alias) type = alias.type;
|
|
101
|
+
}
|
|
102
|
+
typeRegistry.push({ kind: "variable", name: decl.id.name, type });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} else if (node.type === "FunctionDeclaration") {
|
|
106
|
+
const funcName = node.id.name;
|
|
107
|
+
const params = node.params.map(p => {
|
|
108
|
+
if (p.typeAnnotation) {
|
|
109
|
+
let type = tsTypeToString(p.typeAnnotation.typeAnnotation, typeRegistry);
|
|
110
|
+
if (typeof type === 'string') {
|
|
111
|
+
const alias = typeRegistry.find(e => e.kind === 'typeAlias' && e.name === type);
|
|
112
|
+
if (alias) type = alias.type;
|
|
113
|
+
}
|
|
114
|
+
return { name: p.name, type };
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}).filter(Boolean);
|
|
118
|
+
|
|
119
|
+
let returnType = "any";
|
|
120
|
+
if (node.returnType) {
|
|
121
|
+
returnType = tsTypeToString(node.returnType.typeAnnotation, typeRegistry);
|
|
122
|
+
if (typeof returnType === 'string') {
|
|
123
|
+
const alias = typeRegistry.find(e => e.kind === 'typeAlias' && e.name === returnType);
|
|
124
|
+
if (alias) returnType = alias.type;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (params.length > 0 || returnType !== "any") {
|
|
129
|
+
typeRegistry.push({ kind: "function", name: funcName, params, returnType });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return { ast, typeRegistry };
|
|
136
|
+
}
|