tjs-lang 0.6.7 → 0.6.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tjs-lang",
3
- "version": "0.6.7",
3
+ "version": "0.6.8",
4
4
  "description": "Type-safe JavaScript dialect with runtime validation, sandboxed VM execution, and AI agent orchestration. Transpiles TypeScript to validated JS with fuel-metered execution for untrusted code.",
5
5
  "keywords": [
6
6
  "typescript",
package/src/cli/tjs.ts CHANGED
@@ -20,7 +20,7 @@ import { emit } from './commands/emit'
20
20
  import { convert } from './commands/convert'
21
21
  import { test } from './commands/test'
22
22
 
23
- const VERSION = '0.6.7'
23
+ const VERSION = '0.6.8'
24
24
 
25
25
  const HELP = `
26
26
  tjs - Typed JavaScript CLI
@@ -2043,3 +2043,47 @@ describe('TS overloads → TJS → JS full pipeline', () => {
2043
2043
  }
2044
2044
  })
2045
2045
  })
2046
+
2047
+ describe('rest parameter metadata', () => {
2048
+ it('should capture typed rest param in metadata', () => {
2049
+ const result = tjs(`function sum(...nums: [0]) -> 0 { return 0 }`, {
2050
+ runTests: false,
2051
+ })
2052
+ const info = result.types.sum
2053
+ expect(info.params.nums).toBeDefined()
2054
+ expect(info.params.nums.type.kind).toBe('array')
2055
+ expect(info.params.nums.type.items?.kind).toBe('integer')
2056
+ expect(info.params.nums.required).toBe(false)
2057
+ })
2058
+
2059
+ it('should capture rest param with float array type', () => {
2060
+ const result = tjs(
2061
+ `function mean(...values: [1.0, 2.0]) -> 0.0 { return 0 }`,
2062
+ { runTests: false }
2063
+ )
2064
+ const info = result.types.mean
2065
+ expect(info.params.values).toBeDefined()
2066
+ expect(info.params.values.type.kind).toBe('array')
2067
+ expect(info.params.values.type.items?.kind).toBe('number')
2068
+ })
2069
+
2070
+ it('should capture heterogeneous rest param as union', () => {
2071
+ const result = tjs(
2072
+ `function log(...args: ['hello', 42, true]) -> 0 { return 0 }`,
2073
+ { runTests: false }
2074
+ )
2075
+ const info = result.types.log
2076
+ expect(info.params.args.type.kind).toBe('array')
2077
+ expect(info.params.args.type.items?.kind).toBe('union')
2078
+ expect(info.params.args.type.items?.members).toHaveLength(3)
2079
+ })
2080
+
2081
+ it('should capture untyped rest param as bare array', () => {
2082
+ const result = tjs(`function collect(...args) { return args }`, {
2083
+ runTests: false,
2084
+ })
2085
+ const info = result.types.collect
2086
+ expect(info.params.args).toBeDefined()
2087
+ expect(info.params.args.type.kind).toBe('array')
2088
+ })
2089
+ })
@@ -1105,9 +1105,30 @@ function extractParamExamples(paramsStr: string): string[] {
1105
1105
  const params = splitParams(paramsStr)
1106
1106
 
1107
1107
  for (const param of params) {
1108
+ const trimmed = param.trim()
1109
+
1110
+ // Rest parameter: ...name: [examples] — spread the array elements as individual args
1111
+ const restMatch = trimmed.match(/^\.\.\.(\w+)\s*[:=]\s*(\[.+\])$/)
1112
+ if (restMatch) {
1113
+ try {
1114
+ const arr = new Function(`return ${restMatch[2]}`)()
1115
+ if (Array.isArray(arr)) {
1116
+ for (const el of arr) {
1117
+ examples.push(JSON.stringify(el))
1118
+ }
1119
+ }
1120
+ } catch {
1121
+ // Can't parse — skip rest param examples
1122
+ }
1123
+ continue
1124
+ }
1125
+
1126
+ // Bare rest param without type: ...name — skip (no example to use)
1127
+ if (trimmed.startsWith('...')) continue
1128
+
1108
1129
  // Match: name: example or name = example (with optional safety markers)
1109
1130
  // Handle: (? name: example) or (! name: example)
1110
- const match = param.match(/(?:\(\s*[?!]\s*)?(\w+)\s*[:=]\s*(.+?)(?:\))?$/)
1131
+ const match = trimmed.match(/(?:\(\s*[?!]\s*)?(\w+)\s*[:=]\s*(.+?)(?:\))?$/)
1111
1132
  if (match) {
1112
1133
  examples.push(match[2].trim())
1113
1134
  } else {
@@ -257,6 +257,47 @@ function extractFunctionTypeInfo(
257
257
  }
258
258
  }
259
259
  }
260
+ } else if (
261
+ param.type === 'RestElement' &&
262
+ param.argument?.type === 'Identifier'
263
+ ) {
264
+ // Handle rest parameters: ...args: [0]
265
+ // The type annotation was stripped by preprocessing (JS forbids
266
+ // defaults on rest params), so extract it from the original source
267
+ const restName = param.argument.name
268
+ const restTypeMatch = originalSource.match(
269
+ new RegExp(`\\.\\.\\.${restName}\\s*:\\s*([^)]+?)\\s*\\)`)
270
+ )
271
+ if (restTypeMatch) {
272
+ try {
273
+ const typeExpr = parseExpressionAt(restTypeMatch[1].trim(), 0, {
274
+ ecmaVersion: 2022,
275
+ })
276
+ const restItemType = inferTypeFromValue(typeExpr as any)
277
+ params[restName] = {
278
+ name: restName,
279
+ type: restItemType,
280
+ required: false,
281
+ description: tdoc.params[restName],
282
+ }
283
+ } catch {
284
+ // If we can't parse the type, emit as any array
285
+ params[restName] = {
286
+ name: restName,
287
+ type: { kind: 'array' },
288
+ required: false,
289
+ description: tdoc.params[restName],
290
+ }
291
+ }
292
+ } else {
293
+ // No type annotation — bare rest param
294
+ params[restName] = {
295
+ name: restName,
296
+ type: { kind: 'array' },
297
+ required: false,
298
+ description: tdoc.params[restName],
299
+ }
300
+ }
260
301
  }
261
302
  }
262
303
  }
@@ -48,9 +48,26 @@ export function inferTypeFromValue(node: Expression): TypeDescriptor {
48
48
  if (elements.length === 0) {
49
49
  return { kind: 'array', items: { kind: 'any' } }
50
50
  }
51
- // Use first element as the item type
52
- const itemType = inferTypeFromValue(elements[0])
53
- return { kind: 'array', items: itemType }
51
+ // Infer type from all elements if homogeneous, use that type;
52
+ // if heterogeneous, produce a union of distinct kinds
53
+ const itemTypes = elements
54
+ .filter((el) => el != null)
55
+ .map((el) => inferTypeFromValue(el))
56
+ if (itemTypes.length === 0) {
57
+ return { kind: 'array', items: { kind: 'any' } }
58
+ }
59
+ // Deduplicate by structure
60
+ const seen = new Map<string, TypeDescriptor>()
61
+ for (const t of itemTypes) {
62
+ const key = JSON.stringify(t)
63
+ if (!seen.has(key)) seen.set(key, t)
64
+ }
65
+ const unique = [...seen.values()]
66
+ const items =
67
+ unique.length === 1
68
+ ? unique[0]
69
+ : { kind: 'union' as const, members: unique }
70
+ return { kind: 'array', items }
54
71
  }
55
72
 
56
73
  case 'ObjectExpression': {
@@ -933,6 +933,17 @@ function processParamString(
933
933
  return `[ ${processedInner} ]`
934
934
  }
935
935
 
936
+ // Handle rest parameters: ...args: [0] -> ...args (strip type, JS forbids defaults on rest)
937
+ // The type annotation is still captured by extractFunctionTypeInfo for __tjs metadata
938
+ if (trimmed.startsWith('...')) {
939
+ const restColonPos = findTopLevelColon(trimmed)
940
+ if (restColonPos !== -1) {
941
+ const restName = trimmed.slice(0, restColonPos).trim()
942
+ return restName
943
+ }
944
+ return param
945
+ }
946
+
936
947
  // Handle optional param syntax: x?: type -> x = type (not required)
937
948
  const optionalMatch = trimmed.match(/^(\w+)\s*\?\s*:\s*(.+)$/)
938
949
  if (optionalMatch) {
@@ -35,6 +35,21 @@ describe('Transpiler', () => {
35
35
  )
36
36
  })
37
37
 
38
+ it('should strip type annotation from rest params', () => {
39
+ // JS forbids default values on rest params, so : annotation must be stripped
40
+ const result = preprocess(`function sum(...nums: [0]) { }`)
41
+ expect(result.source).toContain('...nums)')
42
+ expect(result.source).not.toContain('...nums =')
43
+ })
44
+
45
+ it('should handle rest param with array type example', () => {
46
+ const result = preprocess(
47
+ `function mean(...values: [1.0, 2.0, 3.0]) -> 2.0 { return 0 }`
48
+ )
49
+ expect(result.source).toContain('...values)')
50
+ expect(result.source).not.toContain('[1.0')
51
+ })
52
+
38
53
  it('should allow required params after optional params', () => {
39
54
  // TypeScript permits this pattern, and fromTS can produce it when
40
55
  // earlier params degrade to any. TJS should accept it.