tjs-lang 0.7.6 → 0.7.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.
Files changed (61) hide show
  1. package/CLAUDE.md +101 -26
  2. package/bin/docs.js +4 -1
  3. package/demo/docs.json +46 -12
  4. package/demo/src/examples.test.ts +1 -0
  5. package/demo/src/imports.test.ts +16 -4
  6. package/demo/src/imports.ts +60 -15
  7. package/demo/src/playground-shared.ts +9 -8
  8. package/demo/src/tfs-worker.js +205 -147
  9. package/demo/src/tjs-playground.ts +34 -10
  10. package/demo/src/ts-playground.ts +24 -8
  11. package/dist/index.js +140 -119
  12. package/dist/index.js.map +4 -4
  13. package/dist/src/lang/bool-coercion.d.ts +50 -0
  14. package/dist/src/lang/docs.d.ts +31 -6
  15. package/dist/src/lang/linter.d.ts +8 -0
  16. package/dist/src/lang/parser-transforms.d.ts +18 -0
  17. package/dist/src/lang/parser-types.d.ts +2 -0
  18. package/dist/src/lang/parser.d.ts +9 -0
  19. package/dist/src/lang/runtime.d.ts +34 -0
  20. package/dist/src/lang/types.d.ts +9 -1
  21. package/dist/src/rbac/index.d.ts +1 -1
  22. package/dist/src/vm/runtime.d.ts +1 -1
  23. package/dist/tjs-eval.js +44 -39
  24. package/dist/tjs-eval.js.map +4 -4
  25. package/dist/tjs-from-ts.js +20 -20
  26. package/dist/tjs-from-ts.js.map +3 -3
  27. package/dist/tjs-lang.js +86 -80
  28. package/dist/tjs-lang.js.map +4 -4
  29. package/dist/tjs-vm.js +50 -45
  30. package/dist/tjs-vm.js.map +4 -4
  31. package/llms.txt +79 -0
  32. package/package.json +3 -2
  33. package/src/cli/commands/convert.test.ts +16 -21
  34. package/src/lang/bool-coercion.test.ts +203 -0
  35. package/src/lang/bool-coercion.ts +314 -0
  36. package/src/lang/codegen.test.ts +177 -0
  37. package/src/lang/docs.test.ts +328 -1
  38. package/src/lang/docs.ts +424 -24
  39. package/src/lang/emitters/ast.ts +11 -12
  40. package/src/lang/emitters/dts.test.ts +41 -0
  41. package/src/lang/emitters/dts.ts +9 -0
  42. package/src/lang/emitters/js-tests.ts +16 -4
  43. package/src/lang/emitters/js.ts +208 -2
  44. package/src/lang/features.test.ts +22 -0
  45. package/src/lang/inference.ts +54 -0
  46. package/src/lang/linter.test.ts +104 -1
  47. package/src/lang/linter.ts +124 -1
  48. package/src/lang/parser-params.ts +31 -0
  49. package/src/lang/parser-transforms.ts +539 -6
  50. package/src/lang/parser-types.ts +2 -0
  51. package/src/lang/parser.test.ts +73 -1
  52. package/src/lang/parser.ts +85 -1
  53. package/src/lang/runtime.ts +98 -0
  54. package/src/lang/tests.ts +21 -8
  55. package/src/lang/types.ts +6 -0
  56. package/src/rbac/index.ts +2 -2
  57. package/src/rbac/rules.tjs.d.ts +9 -0
  58. package/src/vm/atoms/batteries.ts +2 -2
  59. package/src/vm/runtime.ts +10 -3
  60. package/dist/src/rbac/rules.d.ts +0 -184
  61. package/src/rbac/rules.js +0 -338
@@ -10,7 +10,14 @@
10
10
  * POC: Focus on variable usage first, then type checking.
11
11
  */
12
12
 
13
- import type { Program, Node, Identifier, VariableDeclaration } from 'acorn'
13
+ import type {
14
+ Program,
15
+ Node,
16
+ Identifier,
17
+ VariableDeclaration,
18
+ AssignmentExpression,
19
+ Expression,
20
+ } from 'acorn'
14
21
  import { parse } from './parser'
15
22
  import * as walk from 'acorn-walk'
16
23
 
@@ -36,8 +43,16 @@ export interface LintOptions {
36
43
  unreachableCode?: boolean
37
44
  /** Warn about explicit `new` keyword usage (TJS makes classes callable without new) */
38
45
  noExplicitNew?: boolean
46
+ /**
47
+ * Check `let` declarations for missing type information and forbid literal
48
+ * undefined/null assignments to typed lets. If undefined, the parser's
49
+ * `TjsSafeAssign` mode controls whether the rule runs.
50
+ */
51
+ safeAssign?: boolean
39
52
  /** Filename for error messages */
40
53
  filename?: string
54
+ /** Treat safeAssign violations as errors instead of warnings (TjsStrict semantics) */
55
+ strict?: boolean
41
56
  }
42
57
 
43
58
  const DEFAULT_OPTIONS: LintOptions = {
@@ -56,12 +71,16 @@ export function lint(source: string, options: LintOptions = {}): LintResult {
56
71
 
57
72
  // Parse the source
58
73
  let program: Program
74
+ let letAnnotations: Map<string, string> = new Map()
75
+ let safeAssignMode = false
59
76
  try {
60
77
  const result = parse(source, {
61
78
  filename: opts.filename,
62
79
  colonShorthand: true,
63
80
  })
64
81
  program = result.ast
82
+ letAnnotations = result.letAnnotations
83
+ safeAssignMode = result.tjsModes.tjsSafeAssign
65
84
  } catch (error: any) {
66
85
  return {
67
86
  diagnostics: [
@@ -76,6 +95,11 @@ export function lint(source: string, options: LintOptions = {}): LintResult {
76
95
  valid: false,
77
96
  }
78
97
  }
98
+ const safeAssignEnabled =
99
+ opts.safeAssign !== undefined ? opts.safeAssign : safeAssignMode
100
+ const safeAssignSeverity: LintDiagnostic['severity'] = opts.strict
101
+ ? 'error'
102
+ : 'warning'
79
103
 
80
104
  // Track variable declarations and usages per scope
81
105
  const scopes: Scope[] = [createScope()] // Global scope
@@ -177,6 +201,80 @@ export function lint(source: string, options: LintOptions = {}): LintResult {
177
201
  })
178
202
  }
179
203
 
204
+ // TjsSafeAssign: lets need an initializer or `: <example>` annotation, and
205
+ // typed lets must not be (re)assigned literal undefined/null/void 0.
206
+ if (safeAssignEnabled) {
207
+ // First pass: track which lets are "typed" (annotated OR have a non-nullish initializer)
208
+ const typedLets = new Set<string>()
209
+ walk.simple(program, {
210
+ VariableDeclaration(node: VariableDeclaration) {
211
+ if (node.kind !== 'let') return
212
+ for (const d of node.declarations) {
213
+ if (d.id.type !== 'Identifier') continue
214
+ const name = d.id.name
215
+ const annotated = letAnnotations.has(name)
216
+ const init = d.init
217
+ if (annotated) {
218
+ typedLets.add(name)
219
+ } else if (init && !isLiteralNullish(init)) {
220
+ typedLets.add(name)
221
+ }
222
+ }
223
+ },
224
+ })
225
+
226
+ // Declaration-site rule: missing type information
227
+ walk.simple(program, {
228
+ VariableDeclaration(node: VariableDeclaration) {
229
+ if (node.kind !== 'let') return
230
+ for (const d of node.declarations) {
231
+ if (d.id.type !== 'Identifier') continue
232
+ const name = d.id.name
233
+ if (letAnnotations.has(name)) continue
234
+ if (!d.init) {
235
+ diagnostics.push({
236
+ severity: safeAssignSeverity,
237
+ message: `'let ${name}' has no initializer or type annotation. Add an initializer (let ${name} = ...) or annotate (let ${name}: <example>).`,
238
+ line: (d as any).loc?.start?.line,
239
+ column: (d as any).loc?.start?.column,
240
+ rule: 'safe-assign-let-needs-type',
241
+ })
242
+ } else if (isLiteralNullish(d.init)) {
243
+ diagnostics.push({
244
+ severity: safeAssignSeverity,
245
+ message: `'let ${name}' is initialized to ${describeNullish(
246
+ d.init
247
+ )} with no type annotation. Annotate (let ${name}: <example>) to record the intended type.`,
248
+ line: (d as any).loc?.start?.line,
249
+ column: (d as any).loc?.start?.column,
250
+ rule: 'safe-assign-let-needs-type',
251
+ })
252
+ }
253
+ }
254
+ },
255
+ })
256
+
257
+ // Use-site rule: literal undefined/null assigned to a typed let
258
+ walk.simple(program, {
259
+ AssignmentExpression(node: AssignmentExpression) {
260
+ if (node.operator !== '=') return
261
+ if (node.left.type !== 'Identifier') return
262
+ const name = (node.left as Identifier).name
263
+ if (!typedLets.has(name)) return
264
+ if (!isLiteralNullish(node.right)) return
265
+ diagnostics.push({
266
+ severity: safeAssignSeverity,
267
+ message: `Cannot assign ${describeNullish(
268
+ node.right
269
+ )} to typed let '${name}'.`,
270
+ line: (node as any).loc?.start?.line,
271
+ column: (node as any).loc?.start?.column,
272
+ rule: 'safe-assign-no-nullish',
273
+ })
274
+ },
275
+ })
276
+ }
277
+
180
278
  // Check for explicit `new` keyword usage
181
279
  // In TJS, classes are callable without `new`, so using `new` is unnecessary
182
280
  if (opts.noExplicitNew) {
@@ -226,6 +324,31 @@ function createScope(): Scope {
226
324
  return { declarations: new Map() }
227
325
  }
228
326
 
327
+ /**
328
+ * Is the given expression a literal that evaluates to undefined or null?
329
+ * Catches: `undefined`, `null`, `void 0`, `void <any-literal>`.
330
+ */
331
+ function isLiteralNullish(node: Expression | null | undefined): boolean {
332
+ if (!node) return false
333
+ if (node.type === 'Identifier' && (node as Identifier).name === 'undefined') {
334
+ return true
335
+ }
336
+ if (node.type === 'Literal' && (node as any).value === null) return true
337
+ if (node.type === 'UnaryExpression' && (node as any).operator === 'void') {
338
+ return true
339
+ }
340
+ return false
341
+ }
342
+
343
+ function describeNullish(node: Expression): string {
344
+ if (node.type === 'Identifier') return 'undefined'
345
+ if (node.type === 'Literal' && (node as any).value === null) return 'null'
346
+ if (node.type === 'UnaryExpression' && (node as any).operator === 'void') {
347
+ return 'void <expr> (undefined)'
348
+ }
349
+ return 'a nullish value'
350
+ }
351
+
229
352
  function addDeclaration(scope: Scope, node: Node, kind: Declaration['kind']) {
230
353
  if (node.type === 'Identifier') {
231
354
  scope.declarations.set((node as Identifier).name, {
@@ -12,6 +12,7 @@ import type {
12
12
  ContextFrame,
13
13
  TjsModes,
14
14
  } from './parser-types'
15
+ import { locAt } from './parser-transforms'
15
16
 
16
17
  export function transformParenExpressions(
17
18
  source: string,
@@ -331,6 +332,23 @@ export function transformParenExpressions(
331
332
  i = typeResult.endPos
332
333
  }
333
334
  }
335
+
336
+ // Catch a common mistake: writing `=> {` after a function declaration's
337
+ // return type (or after `)`), as if it were an arrow function. Without
338
+ // this check, the `=>` would pass through to Acorn, which complains
339
+ // with a generic "Unexpected token" at a misleading position.
340
+ let arrowCheck = i
341
+ while (arrowCheck < source.length && /\s/.test(source[arrowCheck]))
342
+ arrowCheck++
343
+ if (source[arrowCheck] === '=' && source[arrowCheck + 1] === '>') {
344
+ throw new SyntaxError(
345
+ "Unexpected '=>' after function declaration. " +
346
+ 'Function declarations use `function name(params) { body }`, ' +
347
+ 'not arrow syntax. Remove the `=>`.',
348
+ locAt(ctx.originalSource, arrowCheck),
349
+ ctx.originalSource
350
+ )
351
+ }
334
352
  continue
335
353
  }
336
354
 
@@ -410,6 +428,19 @@ export function transformParenExpressions(
410
428
  }
411
429
  }
412
430
 
431
+ // Same `=>` check for class methods.
432
+ let k = i
433
+ while (k < source.length && /\s/.test(source[k])) k++
434
+ if (source[k] === '=' && source[k + 1] === '>') {
435
+ throw new SyntaxError(
436
+ "Unexpected '=>' after method declaration. " +
437
+ 'Methods use `name(params) { body }`, not arrow syntax. ' +
438
+ 'Remove the `=>`.',
439
+ locAt(ctx.originalSource, k),
440
+ ctx.originalSource
441
+ )
442
+ }
443
+
413
444
  continue
414
445
  }
415
446