tjs-lang 0.6.13 → 0.6.15
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/CLAUDE.md +40 -17
- package/demo/docs.json +1 -1
- package/dist/index.js +119 -116
- package/dist/index.js.map +8 -8
- package/dist/src/lang/parser-transforms.d.ts +15 -0
- package/dist/src/lang/runtime.d.ts +4 -2
- package/dist/src/types/Type.d.ts +41 -0
- package/dist/src/types/index.d.ts +1 -1
- package/dist/tjs-full.js +119 -116
- package/dist/tjs-full.js.map +8 -8
- package/dist/tjs-vm.js +37 -37
- package/dist/tjs-vm.js.map +4 -4
- package/docs/function-predicate-design.md +180 -0
- package/package.json +1 -1
- package/src/cli/tjs.ts +1 -1
- package/src/lang/emitters/dts.test.ts +58 -0
- package/src/lang/emitters/dts.ts +84 -0
- package/src/lang/emitters/from-ts.ts +217 -6
- package/src/lang/function-predicate.test.ts +188 -0
- package/src/lang/parser-transforms.ts +103 -0
- package/src/lang/parser.ts +2 -0
- package/src/lang/runtime.ts +4 -0
- package/src/lang/typescript-syntax.test.ts +154 -5
- package/src/types/Type.ts +148 -0
- package/src/types/index.ts +5 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test'
|
|
2
|
+
import { FunctionPredicate } from '../types/Type'
|
|
3
|
+
import { preprocess } from './parser'
|
|
4
|
+
import { tjs } from './index'
|
|
5
|
+
import { fromTS } from './emitters/from-ts'
|
|
6
|
+
|
|
7
|
+
describe('FunctionPredicate runtime', () => {
|
|
8
|
+
it('should create a type that accepts functions', () => {
|
|
9
|
+
const Callback = FunctionPredicate('Callback', {
|
|
10
|
+
params: { x: 0 },
|
|
11
|
+
returns: '',
|
|
12
|
+
})
|
|
13
|
+
expect(Callback.check(() => {})).toBe(true)
|
|
14
|
+
expect(Callback.check((x: number) => String(x))).toBe(true)
|
|
15
|
+
expect(Callback.check(42)).toBe(false)
|
|
16
|
+
expect(Callback.check('not a function')).toBe(false)
|
|
17
|
+
expect(Callback.check(null)).toBe(false)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('should create from existing typed function', () => {
|
|
21
|
+
const fn = (a: number, b: number) => a + b
|
|
22
|
+
;(fn as any).__tjs = {
|
|
23
|
+
params: {
|
|
24
|
+
a: { type: { kind: 'integer' }, example: 0 },
|
|
25
|
+
b: { type: { kind: 'integer' }, example: 0 },
|
|
26
|
+
},
|
|
27
|
+
returns: { type: { kind: 'integer' }, example: 0 },
|
|
28
|
+
}
|
|
29
|
+
const Adder = FunctionPredicate('Adder', fn)
|
|
30
|
+
expect(Adder.params).toHaveProperty('a')
|
|
31
|
+
expect(Adder.params).toHaveProperty('b')
|
|
32
|
+
expect(Adder.description).toBe('Adder')
|
|
33
|
+
expect(Adder.__runtimeType).toBe(true)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should have correct return contract', () => {
|
|
37
|
+
const assert = FunctionPredicate('f', {
|
|
38
|
+
returnContract: 'assertReturns',
|
|
39
|
+
})
|
|
40
|
+
const returns = FunctionPredicate('f', { returnContract: 'returns' })
|
|
41
|
+
const checked = FunctionPredicate('f', {
|
|
42
|
+
returnContract: 'checkedReturns',
|
|
43
|
+
})
|
|
44
|
+
expect(assert.returnContract).toBe('assertReturns')
|
|
45
|
+
expect(returns.returnContract).toBe('returns')
|
|
46
|
+
expect(checked.returnContract).toBe('checkedReturns')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should default to assertReturns contract', () => {
|
|
50
|
+
const fp = FunctionPredicate('f', {})
|
|
51
|
+
expect(fp.returnContract).toBe('assertReturns')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should reject function with wrong arity via __tjs metadata', () => {
|
|
55
|
+
const Binop = FunctionPredicate('Binop', {
|
|
56
|
+
params: { a: 0, b: 0 },
|
|
57
|
+
returns: 0,
|
|
58
|
+
})
|
|
59
|
+
// Function with matching arity + types → pass
|
|
60
|
+
const goodFn = (a: number, b: number) => a + b
|
|
61
|
+
;(goodFn as any).__tjs = {
|
|
62
|
+
params: {
|
|
63
|
+
a: { type: { kind: 'integer' }, example: 0 },
|
|
64
|
+
b: { type: { kind: 'integer' }, example: 0 },
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
expect(Binop.check(goodFn)).toBe(true)
|
|
68
|
+
|
|
69
|
+
// Function with wrong arity → fail
|
|
70
|
+
const wrongArity = (x: number) => x
|
|
71
|
+
;(wrongArity as any).__tjs = {
|
|
72
|
+
params: {
|
|
73
|
+
x: { type: { kind: 'integer' }, example: 0 },
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
expect(Binop.check(wrongArity)).toBe(false)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should reject function with wrong param types via __tjs metadata', () => {
|
|
80
|
+
const StrFn = FunctionPredicate('StrFn', {
|
|
81
|
+
params: { name: '' },
|
|
82
|
+
returns: '',
|
|
83
|
+
})
|
|
84
|
+
// Function with string param → pass
|
|
85
|
+
const goodFn = (s: string) => s
|
|
86
|
+
;(goodFn as any).__tjs = {
|
|
87
|
+
params: { s: { type: { kind: 'string' }, example: '' } },
|
|
88
|
+
}
|
|
89
|
+
expect(StrFn.check(goodFn)).toBe(true)
|
|
90
|
+
|
|
91
|
+
// Function with number param → fail
|
|
92
|
+
const badFn = (n: number) => String(n)
|
|
93
|
+
;(badFn as any).__tjs = {
|
|
94
|
+
params: { n: { type: { kind: 'integer' }, example: 0 } },
|
|
95
|
+
}
|
|
96
|
+
expect(StrFn.check(badFn)).toBe(false)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('should accept function with any-typed params', () => {
|
|
100
|
+
const Binop = FunctionPredicate('Binop', {
|
|
101
|
+
params: { a: 0, b: 0 },
|
|
102
|
+
})
|
|
103
|
+
const fn = (a: any, b: any) => a + b
|
|
104
|
+
;(fn as any).__tjs = {
|
|
105
|
+
params: {
|
|
106
|
+
a: { type: { kind: 'any' }, example: null },
|
|
107
|
+
b: { type: { kind: 'any' }, example: null },
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
expect(Binop.check(fn)).toBe(true)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('should accept plain functions without __tjs metadata', () => {
|
|
114
|
+
const Callback = FunctionPredicate('Callback', {
|
|
115
|
+
params: { x: 0 },
|
|
116
|
+
})
|
|
117
|
+
// Plain function with no metadata — should still pass (can't validate)
|
|
118
|
+
expect(Callback.check(() => {})).toBe(true)
|
|
119
|
+
expect(Callback.check(Math.abs)).toBe(true)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe('FunctionPredicate parser transform', () => {
|
|
124
|
+
it('should transform block form', () => {
|
|
125
|
+
const result = preprocess(
|
|
126
|
+
"FunctionPredicate Callback {\n params: { x: 0 }\n returns: ''\n}"
|
|
127
|
+
)
|
|
128
|
+
expect(result.source).toContain("FunctionPredicate('Callback'")
|
|
129
|
+
expect(result.source).toContain('params: { x: 0 }')
|
|
130
|
+
expect(result.source).toContain("returns: ''")
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('should transform function form', () => {
|
|
134
|
+
const result = preprocess(
|
|
135
|
+
"FunctionPredicate Handler(myFn, 'event handler')"
|
|
136
|
+
)
|
|
137
|
+
expect(result.source).toContain("FunctionPredicate('event handler'")
|
|
138
|
+
expect(result.source).toContain('myFn')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('should transpile block form through full pipeline', () => {
|
|
142
|
+
const result = tjs(
|
|
143
|
+
'FunctionPredicate Callback {\n params: { x: 0 }\n returns: false\n}',
|
|
144
|
+
{ runTests: false }
|
|
145
|
+
)
|
|
146
|
+
expect(result.code).toContain('FunctionPredicate')
|
|
147
|
+
expect(result.code).toContain('Callback')
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
describe('FunctionPredicate in fromTS', () => {
|
|
152
|
+
it('should convert function type alias to FunctionPredicate', () => {
|
|
153
|
+
const result = fromTS('type Callback = (x: number) => void', {
|
|
154
|
+
emitTJS: true,
|
|
155
|
+
})
|
|
156
|
+
expect(result.code).toContain('FunctionPredicate Callback')
|
|
157
|
+
expect(result.code).toContain('params: { x: 0.0 }')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('should convert function type with return to FunctionPredicate', () => {
|
|
161
|
+
const result = fromTS('type Mapper = (value: string) => number', {
|
|
162
|
+
emitTJS: true,
|
|
163
|
+
})
|
|
164
|
+
expect(result.code).toContain('FunctionPredicate Mapper')
|
|
165
|
+
expect(result.code).toContain("params: { value: '' }")
|
|
166
|
+
expect(result.code).toContain('returns: 0.0')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('should handle inline function params', () => {
|
|
170
|
+
const result = fromTS(
|
|
171
|
+
'function process(cb: (x: number) => string): void {}',
|
|
172
|
+
{ emitTJS: true }
|
|
173
|
+
)
|
|
174
|
+
expect(result.code).toContain("FunctionPredicate('function'")
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('should handle void function type', () => {
|
|
178
|
+
const result = fromTS('type VoidFn = () => void', { emitTJS: true })
|
|
179
|
+
expect(result.code).toContain('FunctionPredicate VoidFn')
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('should preserve export on function type alias', () => {
|
|
183
|
+
const result = fromTS('export type Handler = (event: Event) => boolean', {
|
|
184
|
+
emitTJS: true,
|
|
185
|
+
})
|
|
186
|
+
expect(result.code).toContain('export FunctionPredicate Handler')
|
|
187
|
+
})
|
|
188
|
+
})
|
|
@@ -1188,6 +1188,109 @@ export function transformTypeDeclarations(source: string): string {
|
|
|
1188
1188
|
return result
|
|
1189
1189
|
}
|
|
1190
1190
|
|
|
1191
|
+
/**
|
|
1192
|
+
* Transform FunctionPredicate declarations
|
|
1193
|
+
*
|
|
1194
|
+
* Block form:
|
|
1195
|
+
* FunctionPredicate Callback {
|
|
1196
|
+
* params: { x: 0, y: '' }
|
|
1197
|
+
* returns: false
|
|
1198
|
+
* }
|
|
1199
|
+
* → const Callback = FunctionPredicate('Callback', { params: { x: 0, y: '' }, returns: false })
|
|
1200
|
+
*
|
|
1201
|
+
* Function form:
|
|
1202
|
+
* FunctionPredicate Handler(existingFn, 'description')
|
|
1203
|
+
* → const Handler = FunctionPredicate('description', existingFn)
|
|
1204
|
+
*/
|
|
1205
|
+
export function transformFunctionPredicateDeclarations(source: string): string {
|
|
1206
|
+
let result = ''
|
|
1207
|
+
let i = 0
|
|
1208
|
+
|
|
1209
|
+
while (i < source.length) {
|
|
1210
|
+
const fpMatch = source
|
|
1211
|
+
.slice(i)
|
|
1212
|
+
.match(/^\bFunctionPredicate\s+([A-Z_][a-zA-Z0-9_]*)\s*/)
|
|
1213
|
+
if (fpMatch) {
|
|
1214
|
+
const fpName = fpMatch[1]
|
|
1215
|
+
const j = i + fpMatch[0].length
|
|
1216
|
+
|
|
1217
|
+
// Check for block form: FunctionPredicate Name { ... }
|
|
1218
|
+
if (source[j] === '{') {
|
|
1219
|
+
// Find matching closing brace
|
|
1220
|
+
let depth = 1
|
|
1221
|
+
let k = j + 1
|
|
1222
|
+
while (k < source.length && depth > 0) {
|
|
1223
|
+
if (source[k] === '{') depth++
|
|
1224
|
+
else if (source[k] === '}') depth--
|
|
1225
|
+
k++
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
if (depth === 0) {
|
|
1229
|
+
const blockBody = source.slice(j + 1, k - 1).trim()
|
|
1230
|
+
|
|
1231
|
+
// Extract params: { ... }
|
|
1232
|
+
const paramsMatch = blockBody.match(/params\s*:\s*(\{[^}]*\})/)
|
|
1233
|
+
// Extract returns value
|
|
1234
|
+
const returnsMatch = blockBody.match(/returns\s*:\s*(.+?)(?:\n|$)/)
|
|
1235
|
+
// Extract returnContract
|
|
1236
|
+
const contractMatch = blockBody.match(
|
|
1237
|
+
/returnContract\s*:\s*['"](\w+)['"]/
|
|
1238
|
+
)
|
|
1239
|
+
// Extract description
|
|
1240
|
+
const descMatch = blockBody.match(/description\s*:\s*(['"])([^]*?)\1/)
|
|
1241
|
+
|
|
1242
|
+
const spec: string[] = []
|
|
1243
|
+
if (paramsMatch) spec.push(`params: ${paramsMatch[1]}`)
|
|
1244
|
+
if (returnsMatch) spec.push(`returns: ${returnsMatch[1].trim()}`)
|
|
1245
|
+
if (contractMatch) {
|
|
1246
|
+
spec.push(`returnContract: '${contractMatch[1]}'`)
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
const desc = descMatch ? descMatch[2] : fpName
|
|
1250
|
+
result += `const ${fpName} = FunctionPredicate('${desc}', { ${spec.join(
|
|
1251
|
+
', '
|
|
1252
|
+
)} })`
|
|
1253
|
+
i = k
|
|
1254
|
+
continue
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Check for function form: FunctionPredicate Name(fn, 'desc')
|
|
1259
|
+
if (source[j] === '(') {
|
|
1260
|
+
// Find matching closing paren
|
|
1261
|
+
let depth = 1
|
|
1262
|
+
let k = j + 1
|
|
1263
|
+
while (k < source.length && depth > 0) {
|
|
1264
|
+
if (source[k] === '(') depth++
|
|
1265
|
+
else if (source[k] === ')') depth--
|
|
1266
|
+
k++
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
if (depth === 0) {
|
|
1270
|
+
const args = source.slice(j + 1, k - 1).trim()
|
|
1271
|
+
// Split on comma: fn, 'description'
|
|
1272
|
+
const commaIdx = args.indexOf(',')
|
|
1273
|
+
if (commaIdx !== -1) {
|
|
1274
|
+
const fnRef = args.slice(0, commaIdx).trim()
|
|
1275
|
+
const desc = args.slice(commaIdx + 1).trim()
|
|
1276
|
+
result += `const ${fpName} = FunctionPredicate(${desc}, ${fnRef})`
|
|
1277
|
+
} else {
|
|
1278
|
+
// Just a function reference, name as description
|
|
1279
|
+
result += `const ${fpName} = FunctionPredicate('${fpName}', ${args})`
|
|
1280
|
+
}
|
|
1281
|
+
i = k
|
|
1282
|
+
continue
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
result += source[i]
|
|
1288
|
+
i++
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
return result
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1191
1294
|
/**
|
|
1192
1295
|
* Transform Generic block declarations
|
|
1193
1296
|
*
|
package/src/lang/parser.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
transformEqualityToStructural,
|
|
37
37
|
transformTypeDeclarations,
|
|
38
38
|
transformGenericDeclarations,
|
|
39
|
+
transformFunctionPredicateDeclarations,
|
|
39
40
|
transformUnionDeclarations,
|
|
40
41
|
transformEnumDeclarations,
|
|
41
42
|
transformExtendDeclarations,
|
|
@@ -167,6 +168,7 @@ export function preprocess(
|
|
|
167
168
|
// Enum Status { Pending, Active, Done } -> const Status = Enum(...)
|
|
168
169
|
source = transformTypeDeclarations(source)
|
|
169
170
|
source = transformGenericDeclarations(source)
|
|
171
|
+
source = transformFunctionPredicateDeclarations(source)
|
|
170
172
|
source = transformUnionDeclarations(source)
|
|
171
173
|
source = transformEnumDeclarations(source)
|
|
172
174
|
|
package/src/lang/runtime.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
Union,
|
|
18
18
|
Generic,
|
|
19
19
|
Enum,
|
|
20
|
+
FunctionPredicate,
|
|
20
21
|
Nullable,
|
|
21
22
|
Optional,
|
|
22
23
|
TArray,
|
|
@@ -48,6 +49,7 @@ export {
|
|
|
48
49
|
Union,
|
|
49
50
|
Generic,
|
|
50
51
|
Enum,
|
|
52
|
+
FunctionPredicate,
|
|
51
53
|
Nullable,
|
|
52
54
|
Optional,
|
|
53
55
|
TArray,
|
|
@@ -1179,6 +1181,7 @@ export function createRuntime() {
|
|
|
1179
1181
|
Union,
|
|
1180
1182
|
Generic,
|
|
1181
1183
|
Enum,
|
|
1184
|
+
FunctionPredicate,
|
|
1182
1185
|
Nullable,
|
|
1183
1186
|
Optional,
|
|
1184
1187
|
TArray,
|
|
@@ -1251,6 +1254,7 @@ export const runtime = {
|
|
|
1251
1254
|
Union,
|
|
1252
1255
|
Generic,
|
|
1253
1256
|
Enum,
|
|
1257
|
+
FunctionPredicate,
|
|
1254
1258
|
Nullable,
|
|
1255
1259
|
Optional,
|
|
1256
1260
|
TArray,
|
|
@@ -1312,10 +1312,9 @@ describe('Constrained generics use constraint as example', () => {
|
|
|
1312
1312
|
// =============================================================================
|
|
1313
1313
|
|
|
1314
1314
|
describe('fromTS — tosijs conversion edge cases', () => {
|
|
1315
|
-
test('
|
|
1316
|
-
//
|
|
1317
|
-
//
|
|
1318
|
-
// "required param after optional" error.
|
|
1315
|
+
test('DOM type params stay annotated (not degraded to bare name)', () => {
|
|
1316
|
+
// Element is a DOM interface type — should map to {} (opaque object)
|
|
1317
|
+
// so the param stays annotated and required, not degraded to bare name
|
|
1319
1318
|
const result = fromTS(
|
|
1320
1319
|
`export function touchElement(element: Element, changedPath?: string): void {
|
|
1321
1320
|
console.log(element, changedPath)
|
|
@@ -1323,7 +1322,8 @@ describe('fromTS — tosijs conversion edge cases', () => {
|
|
|
1323
1322
|
{ emitTJS: true }
|
|
1324
1323
|
)
|
|
1325
1324
|
expect(result.code).toContain('function touchElement(')
|
|
1326
|
-
//
|
|
1325
|
+
// Element should be annotated with {} (opaque object), not bare name
|
|
1326
|
+
expect(result.code).toContain('element: {}')
|
|
1327
1327
|
expect(result.code).toBeDefined()
|
|
1328
1328
|
})
|
|
1329
1329
|
|
|
@@ -1594,6 +1594,155 @@ describe('fromTS — tosijs conversion edge cases', () => {
|
|
|
1594
1594
|
})
|
|
1595
1595
|
})
|
|
1596
1596
|
|
|
1597
|
+
// =============================================================================
|
|
1598
|
+
// DOM TYPES
|
|
1599
|
+
// =============================================================================
|
|
1600
|
+
|
|
1601
|
+
describe('DOM Types', () => {
|
|
1602
|
+
test('Event param maps to opaque object', () => {
|
|
1603
|
+
const result = fromTS(`function handle(e: Event): void {}`, {
|
|
1604
|
+
emitTJS: true,
|
|
1605
|
+
})
|
|
1606
|
+
expect(result.code).toContain('e: {}')
|
|
1607
|
+
// Should NOT have degraded comment
|
|
1608
|
+
expect(result.code).not.toContain('TODO: TS types degraded')
|
|
1609
|
+
})
|
|
1610
|
+
|
|
1611
|
+
test('HTMLElement param maps to opaque object', () => {
|
|
1612
|
+
const result = fromTS(`function render(el: HTMLElement): void {}`, {
|
|
1613
|
+
emitTJS: true,
|
|
1614
|
+
})
|
|
1615
|
+
expect(result.code).toContain('el: {}')
|
|
1616
|
+
})
|
|
1617
|
+
|
|
1618
|
+
test('specific HTML element types map to opaque object', () => {
|
|
1619
|
+
const result = fromTS(
|
|
1620
|
+
`function setup(input: HTMLInputElement, form: HTMLFormElement): void {}`,
|
|
1621
|
+
{ emitTJS: true }
|
|
1622
|
+
)
|
|
1623
|
+
expect(result.code).toContain('input: {}')
|
|
1624
|
+
expect(result.code).toContain('form: {}')
|
|
1625
|
+
})
|
|
1626
|
+
|
|
1627
|
+
test('MouseEvent param maps to opaque object', () => {
|
|
1628
|
+
const result = fromTS(
|
|
1629
|
+
`function onClick(ev: MouseEvent): boolean { return true }`,
|
|
1630
|
+
{ emitTJS: true }
|
|
1631
|
+
)
|
|
1632
|
+
expect(result.code).toContain('ev: {}')
|
|
1633
|
+
})
|
|
1634
|
+
|
|
1635
|
+
test('Document and Node params map to opaque object', () => {
|
|
1636
|
+
const result = fromTS(
|
|
1637
|
+
`function traverse(doc: Document, node: Node): void {}`,
|
|
1638
|
+
{ emitTJS: true }
|
|
1639
|
+
)
|
|
1640
|
+
expect(result.code).toContain('doc: {}')
|
|
1641
|
+
expect(result.code).toContain('node: {}')
|
|
1642
|
+
})
|
|
1643
|
+
|
|
1644
|
+
test('DOM types as return types', () => {
|
|
1645
|
+
const result = fromTS(
|
|
1646
|
+
`function getRoot(): HTMLElement { return document.body }`,
|
|
1647
|
+
{ emitTJS: true }
|
|
1648
|
+
)
|
|
1649
|
+
// Return type should be {} not degraded
|
|
1650
|
+
expect(result.code).toContain('-! {}')
|
|
1651
|
+
})
|
|
1652
|
+
|
|
1653
|
+
test('DOM callback type preserves annotation', () => {
|
|
1654
|
+
const result = fromTS(`type ClickHandler = (ev: MouseEvent) => void`, {
|
|
1655
|
+
emitTJS: true,
|
|
1656
|
+
})
|
|
1657
|
+
expect(result.code).toContain('FunctionPredicate ClickHandler')
|
|
1658
|
+
})
|
|
1659
|
+
|
|
1660
|
+
test('mixed DOM and primitive params', () => {
|
|
1661
|
+
const result = fromTS(
|
|
1662
|
+
`function bind(el: Element, attr: string, value: number): void {}`,
|
|
1663
|
+
{ emitTJS: true }
|
|
1664
|
+
)
|
|
1665
|
+
expect(result.code).toContain('el: {}')
|
|
1666
|
+
expect(result.code).toContain("attr: ''")
|
|
1667
|
+
expect(result.code).toContain('value: 0.0')
|
|
1668
|
+
})
|
|
1669
|
+
|
|
1670
|
+
test('DOM types in metadata mode', () => {
|
|
1671
|
+
const { types } = fromTS(`function handle(e: Event): void {}`)
|
|
1672
|
+
// Should be 'object' not 'any'
|
|
1673
|
+
expect(types?.handle.params.e.type.kind).toBe('object')
|
|
1674
|
+
})
|
|
1675
|
+
})
|
|
1676
|
+
|
|
1677
|
+
// =============================================================================
|
|
1678
|
+
// FUNCTION TYPES → FunctionPredicate
|
|
1679
|
+
// =============================================================================
|
|
1680
|
+
|
|
1681
|
+
describe('Function Types (FunctionPredicate)', () => {
|
|
1682
|
+
test('function type alias emits FunctionPredicate', () => {
|
|
1683
|
+
const result = fromTS(`type Callback = (x: number, y: string) => boolean`, {
|
|
1684
|
+
emitTJS: true,
|
|
1685
|
+
})
|
|
1686
|
+
expect(result.code).toContain('FunctionPredicate Callback')
|
|
1687
|
+
expect(result.code).toContain('params: { x: 0.0')
|
|
1688
|
+
expect(result.code).toContain("y: ''")
|
|
1689
|
+
expect(result.code).toContain('returns: false')
|
|
1690
|
+
})
|
|
1691
|
+
|
|
1692
|
+
test('void function type alias', () => {
|
|
1693
|
+
const result = fromTS(`type Logger = (msg: string) => void`, {
|
|
1694
|
+
emitTJS: true,
|
|
1695
|
+
})
|
|
1696
|
+
expect(result.code).toContain('FunctionPredicate Logger')
|
|
1697
|
+
expect(result.code).toContain("params: { msg: '' }")
|
|
1698
|
+
// void return should not emit a returns line
|
|
1699
|
+
expect(result.code).not.toMatch(/returns:/)
|
|
1700
|
+
})
|
|
1701
|
+
|
|
1702
|
+
test('exported function type alias preserves export', () => {
|
|
1703
|
+
const result = fromTS(`export type Handler = (event: Event) => boolean`, {
|
|
1704
|
+
emitTJS: true,
|
|
1705
|
+
})
|
|
1706
|
+
expect(result.code).toContain('export FunctionPredicate Handler')
|
|
1707
|
+
})
|
|
1708
|
+
|
|
1709
|
+
test('inline function param emits FunctionPredicate', () => {
|
|
1710
|
+
const result = fromTS(
|
|
1711
|
+
`function process(cb: (item: string) => number): void {}`,
|
|
1712
|
+
{ emitTJS: true }
|
|
1713
|
+
)
|
|
1714
|
+
expect(result.code).toContain("FunctionPredicate('function'")
|
|
1715
|
+
})
|
|
1716
|
+
|
|
1717
|
+
test('no-arg function type', () => {
|
|
1718
|
+
const result = fromTS(`type Thunk = () => number`, { emitTJS: true })
|
|
1719
|
+
expect(result.code).toContain('FunctionPredicate Thunk')
|
|
1720
|
+
expect(result.code).toContain('returns: 0.0')
|
|
1721
|
+
})
|
|
1722
|
+
|
|
1723
|
+
test('function type with multiple params', () => {
|
|
1724
|
+
const result = fromTS(
|
|
1725
|
+
`type Reducer = (acc: number, item: string, index: number) => number`,
|
|
1726
|
+
{ emitTJS: true }
|
|
1727
|
+
)
|
|
1728
|
+
expect(result.code).toContain('FunctionPredicate Reducer')
|
|
1729
|
+
expect(result.code).toContain('acc: 0.0')
|
|
1730
|
+
expect(result.code).toContain("item: ''")
|
|
1731
|
+
expect(result.code).toContain('index: 0.0')
|
|
1732
|
+
expect(result.code).toContain('returns: 0.0')
|
|
1733
|
+
})
|
|
1734
|
+
|
|
1735
|
+
test('function type transpiles through full TJS pipeline', () => {
|
|
1736
|
+
const tsResult = fromTS(
|
|
1737
|
+
`type Compare = (a: number, b: number) => boolean`,
|
|
1738
|
+
{ emitTJS: true }
|
|
1739
|
+
)
|
|
1740
|
+
const jsResult = tjs(tsResult.code, { runTests: false })
|
|
1741
|
+
expect(jsResult.code).toContain('FunctionPredicate')
|
|
1742
|
+
expect(jsResult.code).toContain('Compare')
|
|
1743
|
+
})
|
|
1744
|
+
})
|
|
1745
|
+
|
|
1597
1746
|
// =============================================================================
|
|
1598
1747
|
// SUMMARY: Features to implement (in priority order)
|
|
1599
1748
|
// =============================================================================
|
package/src/types/Type.ts
CHANGED
|
@@ -608,3 +608,151 @@ export function Enum<T extends Record<string, string | number>>(
|
|
|
608
608
|
|
|
609
609
|
return enumType
|
|
610
610
|
}
|
|
611
|
+
|
|
612
|
+
// =============================================================================
|
|
613
|
+
// FunctionPredicate - Runtime type for function signatures
|
|
614
|
+
// =============================================================================
|
|
615
|
+
|
|
616
|
+
/** Return contract levels in order of strictness */
|
|
617
|
+
export type ReturnContract = 'assertReturns' | 'returns' | 'checkedReturns'
|
|
618
|
+
|
|
619
|
+
/** Specification for a FunctionPredicate */
|
|
620
|
+
export interface FunctionPredicateSpec {
|
|
621
|
+
/** Parameter types as example values */
|
|
622
|
+
params?: Record<string, any>
|
|
623
|
+
/** Return type as example value */
|
|
624
|
+
returns?: any
|
|
625
|
+
/** Return contract level */
|
|
626
|
+
returnContract?: ReturnContract
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** A runtime type that validates function signatures */
|
|
630
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
631
|
+
export interface FunctionPredicateType extends RuntimeType<Function> {
|
|
632
|
+
/** Parameter specification */
|
|
633
|
+
readonly params: Record<string, any>
|
|
634
|
+
/** Return type specification */
|
|
635
|
+
readonly returns?: any
|
|
636
|
+
/** Return contract level */
|
|
637
|
+
readonly returnContract: ReturnContract
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/** Infer a TypeDescriptor kind from an example value */
|
|
641
|
+
function kindOfExample(example: unknown): string | null {
|
|
642
|
+
if (example === null) return 'null'
|
|
643
|
+
if (example === undefined) return 'undefined'
|
|
644
|
+
switch (typeof example) {
|
|
645
|
+
case 'string':
|
|
646
|
+
return 'string'
|
|
647
|
+
case 'boolean':
|
|
648
|
+
return 'boolean'
|
|
649
|
+
case 'number':
|
|
650
|
+
return Number.isInteger(example) ? 'integer' : 'number'
|
|
651
|
+
case 'object':
|
|
652
|
+
return Array.isArray(example) ? 'array' : 'object'
|
|
653
|
+
default:
|
|
654
|
+
return null
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Create a runtime type for function signatures.
|
|
660
|
+
*
|
|
661
|
+
* Forms:
|
|
662
|
+
* FunctionPredicate(name, spec) - from a specification object
|
|
663
|
+
* FunctionPredicate(name, fn) - from an existing typed function
|
|
664
|
+
*
|
|
665
|
+
* @example
|
|
666
|
+
* const Callback = FunctionPredicate('Callback', {
|
|
667
|
+
* params: { x: 0, y: 0 },
|
|
668
|
+
* returns: 0,
|
|
669
|
+
* })
|
|
670
|
+
* Callback.check((a, b) => a + b) // true (typeof === 'function')
|
|
671
|
+
* Callback.check(42) // false
|
|
672
|
+
*
|
|
673
|
+
* @example
|
|
674
|
+
* function add(a: 0, b: 0) -> 0 { return a + b }
|
|
675
|
+
* const Adder = FunctionPredicate('Adder', add)
|
|
676
|
+
* // Extracts params/returns from add.__tjs
|
|
677
|
+
*/
|
|
678
|
+
export function FunctionPredicate(
|
|
679
|
+
name: string,
|
|
680
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
681
|
+
specOrFn: FunctionPredicateSpec | Function
|
|
682
|
+
): FunctionPredicateType {
|
|
683
|
+
let params: Record<string, any> = {}
|
|
684
|
+
let returns: any = undefined
|
|
685
|
+
let returnContract: ReturnContract = 'assertReturns'
|
|
686
|
+
|
|
687
|
+
if (typeof specOrFn === 'function') {
|
|
688
|
+
// Extract from function's __tjs metadata
|
|
689
|
+
const meta = (specOrFn as any).__tjs
|
|
690
|
+
if (meta) {
|
|
691
|
+
// Build params from __tjs.params
|
|
692
|
+
if (meta.params) {
|
|
693
|
+
for (const [key, info] of Object.entries(meta.params)) {
|
|
694
|
+
params[key] = (info as any)?.example ?? null
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
// Extract return type
|
|
698
|
+
if (meta.returns) {
|
|
699
|
+
returns = (meta.returns as any)?.example ?? null
|
|
700
|
+
}
|
|
701
|
+
// Extract return contract from safety markers
|
|
702
|
+
if (meta.safeReturn) returnContract = 'checkedReturns'
|
|
703
|
+
else if (meta.unsafe) returnContract = 'assertReturns'
|
|
704
|
+
else returnContract = 'returns'
|
|
705
|
+
}
|
|
706
|
+
} else {
|
|
707
|
+
params = specOrFn.params ?? {}
|
|
708
|
+
returns = specOrFn.returns
|
|
709
|
+
returnContract = specOrFn.returnContract ?? 'assertReturns'
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const fpType: FunctionPredicateType = {
|
|
713
|
+
description: name,
|
|
714
|
+
params,
|
|
715
|
+
returns,
|
|
716
|
+
returnContract,
|
|
717
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
718
|
+
check: (value: unknown): value is Function => {
|
|
719
|
+
if (typeof value !== 'function') return false
|
|
720
|
+
|
|
721
|
+
// Structural validation: check arity and __tjs metadata
|
|
722
|
+
const expectedArity = Object.keys(params).length
|
|
723
|
+
if (expectedArity > 0) {
|
|
724
|
+
// Check function.length (number of params before first default)
|
|
725
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
726
|
+
const fn = value as Function
|
|
727
|
+
const meta = (fn as any).__tjs
|
|
728
|
+
if (meta?.params) {
|
|
729
|
+
// Has TJS metadata — check param count matches
|
|
730
|
+
const metaParamCount = Object.keys(meta.params).length
|
|
731
|
+
if (metaParamCount !== expectedArity) return false
|
|
732
|
+
|
|
733
|
+
// Check param type kinds match where both sides have type info
|
|
734
|
+
const expectedKeys = Object.keys(params)
|
|
735
|
+
const metaKeys = Object.keys(meta.params)
|
|
736
|
+
for (let i = 0; i < expectedKeys.length; i++) {
|
|
737
|
+
const metaInfo = meta.params[metaKeys[i]]
|
|
738
|
+
const expectedExample = params[expectedKeys[i]]
|
|
739
|
+
if (metaInfo?.type?.kind && expectedExample !== undefined) {
|
|
740
|
+
const expectedKind = kindOfExample(expectedExample)
|
|
741
|
+
if (
|
|
742
|
+
expectedKind &&
|
|
743
|
+
metaInfo.type.kind !== expectedKind &&
|
|
744
|
+
metaInfo.type.kind !== 'any'
|
|
745
|
+
)
|
|
746
|
+
return false
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
return true
|
|
753
|
+
},
|
|
754
|
+
__runtimeType: true as const,
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return fpType
|
|
758
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -38,6 +38,11 @@ export {
|
|
|
38
38
|
TRecord,
|
|
39
39
|
type GenericType,
|
|
40
40
|
type TypeParam,
|
|
41
|
+
// Function predicates
|
|
42
|
+
FunctionPredicate,
|
|
43
|
+
type FunctionPredicateType,
|
|
44
|
+
type FunctionPredicateSpec,
|
|
45
|
+
type ReturnContract,
|
|
41
46
|
} from './Type'
|
|
42
47
|
|
|
43
48
|
// Timestamp and LegalDate utilities (pure functions)
|