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.
- package/CLAUDE.md +101 -26
- package/bin/docs.js +4 -1
- package/demo/docs.json +46 -12
- package/demo/src/examples.test.ts +1 -0
- package/demo/src/imports.test.ts +16 -4
- package/demo/src/imports.ts +60 -15
- package/demo/src/playground-shared.ts +9 -8
- package/demo/src/tfs-worker.js +205 -147
- package/demo/src/tjs-playground.ts +34 -10
- package/demo/src/ts-playground.ts +24 -8
- package/dist/index.js +140 -119
- package/dist/index.js.map +4 -4
- package/dist/src/lang/bool-coercion.d.ts +50 -0
- package/dist/src/lang/docs.d.ts +31 -6
- package/dist/src/lang/linter.d.ts +8 -0
- package/dist/src/lang/parser-transforms.d.ts +18 -0
- package/dist/src/lang/parser-types.d.ts +2 -0
- package/dist/src/lang/parser.d.ts +9 -0
- package/dist/src/lang/runtime.d.ts +34 -0
- package/dist/src/lang/types.d.ts +9 -1
- package/dist/src/rbac/index.d.ts +1 -1
- package/dist/src/vm/runtime.d.ts +1 -1
- package/dist/tjs-eval.js +44 -39
- package/dist/tjs-eval.js.map +4 -4
- package/dist/tjs-from-ts.js +20 -20
- package/dist/tjs-from-ts.js.map +3 -3
- package/dist/tjs-lang.js +86 -80
- package/dist/tjs-lang.js.map +4 -4
- package/dist/tjs-vm.js +50 -45
- package/dist/tjs-vm.js.map +4 -4
- package/llms.txt +79 -0
- package/package.json +3 -2
- package/src/cli/commands/convert.test.ts +16 -21
- package/src/lang/bool-coercion.test.ts +203 -0
- package/src/lang/bool-coercion.ts +314 -0
- package/src/lang/codegen.test.ts +177 -0
- package/src/lang/docs.test.ts +328 -1
- package/src/lang/docs.ts +424 -24
- package/src/lang/emitters/ast.ts +11 -12
- package/src/lang/emitters/dts.test.ts +41 -0
- package/src/lang/emitters/dts.ts +9 -0
- package/src/lang/emitters/js-tests.ts +16 -4
- package/src/lang/emitters/js.ts +208 -2
- package/src/lang/features.test.ts +22 -0
- package/src/lang/inference.ts +54 -0
- package/src/lang/linter.test.ts +104 -1
- package/src/lang/linter.ts +124 -1
- package/src/lang/parser-params.ts +31 -0
- package/src/lang/parser-transforms.ts +539 -6
- package/src/lang/parser-types.ts +2 -0
- package/src/lang/parser.test.ts +73 -1
- package/src/lang/parser.ts +85 -1
- package/src/lang/runtime.ts +98 -0
- package/src/lang/tests.ts +21 -8
- package/src/lang/types.ts +6 -0
- package/src/rbac/index.ts +2 -2
- package/src/rbac/rules.tjs.d.ts +9 -0
- package/src/vm/atoms/batteries.ts +2 -2
- package/src/vm/runtime.ts +10 -3
- package/dist/src/rbac/rules.d.ts +0 -184
- package/src/rbac/rules.js +0 -338
package/src/lang/linter.ts
CHANGED
|
@@ -10,7 +10,14 @@
|
|
|
10
10
|
* POC: Focus on variable usage first, then type checking.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type {
|
|
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
|
|