rooklang 0.3.3 → 0.4.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.
Files changed (244) hide show
  1. package/README.md +34 -7
  2. package/README.ru.md +34 -7
  3. package/dist/ast/declarations.d.ts +39 -4
  4. package/dist/ast/expressions.d.ts +34 -8
  5. package/dist/ast/markup.d.ts +6 -1
  6. package/dist/ast/statements.d.ts +28 -2
  7. package/dist/auto-import.d.ts +11 -0
  8. package/dist/auto-import.js +81 -0
  9. package/dist/breaks.d.ts +6 -0
  10. package/dist/breaks.js +36 -0
  11. package/dist/build.d.ts +7 -0
  12. package/dist/build.js +23 -2
  13. package/dist/checker/array-literals.d.ts +6 -0
  14. package/dist/checker/array-literals.js +59 -0
  15. package/dist/checker/assignments.d.ts +4 -0
  16. package/dist/checker/assignments.js +23 -3
  17. package/dist/checker/base.d.ts +8 -2
  18. package/dist/checker/base.js +7 -7
  19. package/dist/checker/builtins.js +22 -8
  20. package/dist/checker/bytes.d.ts +2 -2
  21. package/dist/checker/bytes.js +2 -2
  22. package/dist/checker/calls.js +3 -0
  23. package/dist/checker/channel-objects.d.ts +3 -0
  24. package/dist/checker/channel-objects.js +40 -0
  25. package/dist/checker/channels.d.ts +1 -0
  26. package/dist/checker/channels.js +17 -4
  27. package/dist/checker/checker.d.ts +1 -1
  28. package/dist/checker/checker.js +19 -0
  29. package/dist/checker/class-members.d.ts +22 -0
  30. package/dist/checker/class-members.js +105 -0
  31. package/dist/checker/classes.d.ts +5 -4
  32. package/dist/checker/classes.js +48 -13
  33. package/dist/checker/closing.d.ts +21 -0
  34. package/dist/checker/closing.js +164 -0
  35. package/dist/checker/completion.d.ts +3 -3
  36. package/dist/checker/completion.js +4 -3
  37. package/dist/checker/component-bindings.d.ts +7 -0
  38. package/dist/checker/component-bindings.js +45 -0
  39. package/dist/checker/component-values.js +10 -7
  40. package/dist/checker/components.d.ts +2 -2
  41. package/dist/checker/components.js +8 -3
  42. package/dist/checker/context.d.ts +41 -5
  43. package/dist/checker/context.js +23 -0
  44. package/dist/checker/control-flow.d.ts +11 -5
  45. package/dist/checker/control-flow.js +65 -40
  46. package/dist/checker/declarations.d.ts +2 -2
  47. package/dist/checker/declarations.js +4 -2
  48. package/dist/checker/decode.d.ts +8 -0
  49. package/dist/checker/decode.js +32 -0
  50. package/dist/checker/decorator-exports.js +4 -0
  51. package/dist/checker/decorator-includes.d.ts +9 -0
  52. package/dist/checker/decorator-includes.js +71 -0
  53. package/dist/checker/decorator-members.d.ts +8 -10
  54. package/dist/checker/decorator-members.js +28 -31
  55. package/dist/checker/decorator-wrappers.d.ts +1 -1
  56. package/dist/checker/decorator-wrappers.js +3 -2
  57. package/dist/checker/decorators.d.ts +14 -5
  58. package/dist/checker/decorators.js +52 -25
  59. package/dist/checker/expressions.d.ts +6 -2
  60. package/dist/checker/expressions.js +20 -40
  61. package/dist/checker/flow.d.ts +25 -3
  62. package/dist/checker/flow.js +56 -12
  63. package/dist/checker/functions.d.ts +2 -2
  64. package/dist/checker/functions.js +9 -5
  65. package/dist/checker/helpers.d.ts +5 -2
  66. package/dist/checker/helpers.js +16 -4
  67. package/dist/checker/imports.js +5 -2
  68. package/dist/checker/iterables.d.ts +3 -1
  69. package/dist/checker/iterables.js +13 -0
  70. package/dist/checker/iterators.d.ts +4 -1
  71. package/dist/checker/iterators.js +12 -4
  72. package/dist/checker/json.d.ts +5 -0
  73. package/dist/checker/json.js +92 -0
  74. package/dist/checker/loops.d.ts +2 -0
  75. package/dist/checker/loops.js +50 -5
  76. package/dist/checker/markup.js +14 -2
  77. package/dist/checker/members.js +10 -1
  78. package/dist/checker/operators.d.ts +2 -2
  79. package/dist/checker/operators.js +7 -5
  80. package/dist/checker/queues.d.ts +2 -2
  81. package/dist/checker/queues.js +2 -2
  82. package/dist/checker/resolution.js +5 -3
  83. package/dist/checker/shared-state.d.ts +11 -0
  84. package/dist/checker/shared-state.js +37 -0
  85. package/dist/checker/signals.d.ts +8 -0
  86. package/dist/checker/signals.js +48 -0
  87. package/dist/checker/statements.d.ts +5 -2
  88. package/dist/checker/statements.js +51 -22
  89. package/dist/checker/tagged-unions.d.ts +37 -0
  90. package/dist/checker/tagged-unions.js +132 -0
  91. package/dist/checker/topics.d.ts +6 -0
  92. package/dist/checker/topics.js +17 -0
  93. package/dist/checker/transfer.d.ts +3 -1
  94. package/dist/checker/transfer.js +25 -5
  95. package/dist/checker/try.d.ts +6 -0
  96. package/dist/checker/try.js +19 -0
  97. package/dist/checker/types/constructors.d.ts +1 -1
  98. package/dist/checker/types/constructors.js +2 -2
  99. package/dist/checker/types/generics.js +1 -1
  100. package/dist/checker/types/iterables.d.ts +13 -0
  101. package/dist/checker/types/iterables.js +52 -0
  102. package/dist/checker/types/model.d.ts +25 -0
  103. package/dist/checker/types/params.js +22 -3
  104. package/dist/checker/types/printing.js +4 -3
  105. package/dist/checker/types/relations.js +19 -25
  106. package/dist/checker/types/topic.d.ts +6 -0
  107. package/dist/checker/types/topic.js +23 -0
  108. package/dist/checker/types.d.ts +2 -0
  109. package/dist/checker/types.js +2 -0
  110. package/dist/checker/untyped-globals.d.ts +1 -0
  111. package/dist/checker/untyped-globals.js +3 -1
  112. package/dist/cli.js +18 -36
  113. package/dist/codegen/attributes.d.ts +4 -0
  114. package/dist/codegen/attributes.js +24 -2
  115. package/dist/codegen/calls.d.ts +23 -0
  116. package/dist/codegen/calls.js +88 -7
  117. package/dist/codegen/component-helpers.d.ts +6 -1
  118. package/dist/codegen/component-helpers.js +6 -1
  119. package/dist/codegen/component-props.d.ts +11 -2
  120. package/dist/codegen/component-props.js +50 -5
  121. package/dist/codegen/components.d.ts +6 -5
  122. package/dist/codegen/components.js +44 -21
  123. package/dist/codegen/control-flow.d.ts +2 -0
  124. package/dist/codegen/control-flow.js +23 -9
  125. package/dist/codegen/control-statements.d.ts +10 -2
  126. package/dist/codegen/control-statements.js +56 -19
  127. package/dist/codegen/creation.js +5 -2
  128. package/dist/codegen/decode.d.ts +14 -0
  129. package/dist/codegen/decode.js +93 -0
  130. package/dist/codegen/early-returns.d.ts +4 -3
  131. package/dist/codegen/early-returns.js +17 -5
  132. package/dist/codegen/emitter.d.ts +13 -41
  133. package/dist/codegen/emitter.js +27 -8
  134. package/dist/codegen/expressions.d.ts +9 -0
  135. package/dist/codegen/expressions.js +15 -26
  136. package/dist/codegen/functions.d.ts +11 -1
  137. package/dist/codegen/functions.js +31 -3
  138. package/dist/codegen/js.d.ts +2 -2
  139. package/dist/codegen/js.js +5 -2
  140. package/dist/codegen/live.d.ts +60 -0
  141. package/dist/codegen/live.js +29 -0
  142. package/dist/codegen/mutations.d.ts +7 -0
  143. package/dist/codegen/mutations.js +43 -2
  144. package/dist/codegen/options.d.ts +42 -0
  145. package/dist/codegen/options.js +1 -0
  146. package/dist/codegen/reactive.d.ts +4 -59
  147. package/dist/codegen/reactive.js +34 -72
  148. package/dist/codegen/runtime.d.ts +7 -3
  149. package/dist/codegen/runtime.js +33 -14
  150. package/dist/codegen/shared.d.ts +22 -0
  151. package/dist/codegen/shared.js +93 -0
  152. package/dist/codegen/simple-statements.d.ts +11 -5
  153. package/dist/codegen/simple-statements.js +40 -15
  154. package/dist/codegen/statements.d.ts +5 -0
  155. package/dist/codegen/statements.js +99 -4
  156. package/dist/codegen/try.d.ts +5 -0
  157. package/dist/codegen/try.js +40 -0
  158. package/dist/codegen/updates.d.ts +11 -3
  159. package/dist/codegen/updates.js +52 -4
  160. package/dist/codegen/web-components.d.ts +2 -2
  161. package/dist/codegen/web-components.js +2 -2
  162. package/dist/compile.d.ts +9 -0
  163. package/dist/compile.js +25 -8
  164. package/dist/completion.d.ts +4 -2
  165. package/dist/completion.js +10 -5
  166. package/dist/declarations.js +32 -6
  167. package/dist/decorator-copy.d.ts +1 -1
  168. package/dist/decorator-copy.js +6 -2
  169. package/dist/decorator-names.d.ts +14 -0
  170. package/dist/decorator-names.js +56 -0
  171. package/dist/decorators.d.ts +2 -3
  172. package/dist/decorators.js +36 -14
  173. package/dist/goroutines.d.ts +1 -1
  174. package/dist/goroutines.js +5 -4
  175. package/dist/index.d.ts +6 -2
  176. package/dist/index.js +14 -5
  177. package/dist/inline-packages.d.ts +2 -1
  178. package/dist/inline-packages.js +9 -4
  179. package/dist/lexer/base.d.ts +2 -0
  180. package/dist/lexer/base.js +6 -1
  181. package/dist/lexer/lexer.js +3 -0
  182. package/dist/lexer/literals.d.ts +5 -0
  183. package/dist/lexer/literals.js +51 -1
  184. package/dist/lexer/markup.js +3 -4
  185. package/dist/lexer/tables.js +2 -0
  186. package/dist/lexer/token.d.ts +4 -3
  187. package/dist/lexer/token.js +0 -2
  188. package/dist/loader.js +30 -8
  189. package/dist/names.d.ts +4 -0
  190. package/dist/names.js +29 -0
  191. package/dist/parser/base.d.ts +5 -0
  192. package/dist/parser/base.js +2 -1
  193. package/dist/parser/classes.d.ts +2 -0
  194. package/dist/parser/classes.js +88 -23
  195. package/dist/parser/control-flow.d.ts +8 -1
  196. package/dist/parser/control-flow.js +49 -26
  197. package/dist/parser/declarations.d.ts +5 -12
  198. package/dist/parser/declarations.js +14 -79
  199. package/dist/parser/decorators.d.ts +6 -2
  200. package/dist/parser/decorators.js +43 -19
  201. package/dist/parser/expressions.js +27 -3
  202. package/dist/parser/jumps.d.ts +11 -0
  203. package/dist/parser/jumps.js +77 -0
  204. package/dist/parser/literals.d.ts +9 -0
  205. package/dist/parser/literals.js +45 -6
  206. package/dist/parser/markup.js +1 -1
  207. package/dist/parser/params.d.ts +21 -0
  208. package/dist/parser/params.js +143 -0
  209. package/dist/parser/simple-statements.d.ts +2 -1
  210. package/dist/parser/simple-statements.js +38 -15
  211. package/dist/parser/statements.d.ts +5 -5
  212. package/dist/parser/statements.js +39 -35
  213. package/dist/parser/syntax.d.ts +11 -1
  214. package/dist/parser/syntax.js +16 -2
  215. package/dist/parser/types.js +8 -3
  216. package/dist/project-modules.d.ts +12 -0
  217. package/dist/project-modules.js +106 -0
  218. package/dist/run-file.d.ts +6 -0
  219. package/dist/run-file.js +111 -0
  220. package/dist/runtime/sources.js +13 -6
  221. package/dist/source-map.d.ts +17 -0
  222. package/dist/source-map.js +75 -0
  223. package/dist/ts-types.js +4 -1
  224. package/dist/typescript/convert-objects.d.ts +2 -1
  225. package/dist/typescript/convert-objects.js +18 -9
  226. package/dist/vite.js +12 -4
  227. package/dist/walk-types.js +1 -0
  228. package/dist/walk.d.ts +0 -2
  229. package/dist/walk.js +14 -31
  230. package/package.json +2 -2
  231. package/src/runtime/chan.js +15 -5
  232. package/src/runtime/channel.js +28 -0
  233. package/src/runtime/classes.js +32 -0
  234. package/src/runtime/core.js +10 -1
  235. package/src/runtime/decode.js +221 -0
  236. package/src/runtime/go.js +3 -0
  237. package/src/runtime/index.js +7 -0
  238. package/src/runtime/markup.js +27 -17
  239. package/src/runtime/select.js +14 -9
  240. package/src/runtime/shared.js +17 -0
  241. package/src/runtime/signals.js +59 -0
  242. package/src/runtime/threads.js +32 -5
  243. package/src/runtime/topic.js +118 -0
  244. package/src/runtime/try.js +45 -0
@@ -0,0 +1,105 @@
1
+ import { DeclarationChecker } from "./declarations.js";
2
+ import { notCopied } from "./transfer.js";
3
+ import { findClassMember, isAssignable, typeToString, UNKNOWN, } from "./types.js";
4
+ // The members of classes that are more than a field or a method: getters and setters, the
5
+ // properties of constructor parameters, abstract methods; ClassChecker declares and checks them.
6
+ export class ClassMemberChecker extends DeclarationChecker {
7
+ /** The signatures of getters and setters: their members have the type of the value. */
8
+ accessorSignatures = new Map();
9
+ declareMember(info, isStatic, name, member) {
10
+ const members = isStatic ? info.statics : info.members;
11
+ if (members.has(name.name))
12
+ this.error(`duplicate member "${name.name}"`, name);
13
+ members.set(name.name, member);
14
+ }
15
+ /** `get area() number` and `set area(v number)` make one member, of the type of the value. */
16
+ declareAccessor(info, node, accessor) {
17
+ const signature = this.signature(node.params, node.results);
18
+ const type = (accessor === 'get' ? signature.results[0] : signature.params[0]) ?? UNKNOWN;
19
+ const members = node.isStatic ? info.statics : info.members;
20
+ const other = members.get(node.name.name);
21
+ this.accessorSignatures.set(node, signature);
22
+ if (other?.owner === info &&
23
+ (other.accessor === 'get' || other.accessor === 'set') &&
24
+ other.accessor !== accessor) {
25
+ const [getter, setter] = accessor === 'get' ? [type, other.type] : [other.type, type];
26
+ if (!isAssignable(getter, setter) || !isAssignable(setter, getter)) {
27
+ this.error(`the getter and the setter of "${node.name.name}" have different types: ` +
28
+ `${typeToString(getter)} and ${typeToString(setter)}`, node.name);
29
+ }
30
+ members.set(node.name.name, { ...other, type: getter, accessor: 'both' });
31
+ return;
32
+ }
33
+ this.declareMember(info, node.isStatic, node.name, {
34
+ type,
35
+ method: false,
36
+ visibility: node.visibility,
37
+ owner: info,
38
+ accessor,
39
+ ...(node.abstract ? { abstract: true } : {}),
40
+ });
41
+ }
42
+ /** `constructor(private name string)`: the parameters that are properties of the class too. */
43
+ declareParamProperties(node, info, ctor) {
44
+ node.params.forEach((param, i) => {
45
+ if (!param.property)
46
+ return;
47
+ const type = ctor.params[i] ?? UNKNOWN;
48
+ const reason = info.data === true ? notCopied(type) : null;
49
+ if (reason !== null) {
50
+ this.error(`the fields of a data class go to other threads as copies: ${reason}`, param.type ?? param.name);
51
+ }
52
+ this.declareMember(info, false, param.name, {
53
+ type,
54
+ method: false,
55
+ visibility: param.property.visibility,
56
+ owner: info,
57
+ ...(param.property.readonly ? { readonly: true } : {}),
58
+ });
59
+ });
60
+ }
61
+ /**
62
+ * Only an abstract class has abstract methods, and a class that is not abstract implements
63
+ * those of its bases.
64
+ */
65
+ checkAbstract(node, info) {
66
+ if (node.abstract)
67
+ return;
68
+ for (const member of node.members) {
69
+ if (member.kind !== 'MethodDeclaration' || !member.abstract)
70
+ continue;
71
+ this.error(`"${member.name.name}" is abstract, so its class must be too: abstract class ${info.name}`, member.name);
72
+ }
73
+ const missing = new Set();
74
+ for (let base = info.superClass; base; base = base.superClass) {
75
+ for (const [name, member] of base.members) {
76
+ const found = member.abstract ? findClassMember(info, name, false) : undefined;
77
+ if (found?.abstract)
78
+ missing.add(`${found.owner?.name ?? base.name}.${name}`);
79
+ }
80
+ }
81
+ if (missing.size === 0)
82
+ return;
83
+ this.error(`class "${info.name}" does not implement ${[...missing].join(', ')}: implement ` +
84
+ `${missing.size === 1 ? 'it' : 'them'} or make the class abstract, e.g. abstract class ${info.name}`, node.name);
85
+ }
86
+ /**
87
+ * In JS a field is set on each instance and so hides a getter or setter of the prototype: a
88
+ * field and an accessor of the same name in a class and its base would not work together.
89
+ */
90
+ checkAccessorOverride(info, name, isStatic) {
91
+ const own = (isStatic ? info.statics : info.members).get(name.name);
92
+ const base = info.superClass && findClassMember(info.superClass, name.name, isStatic);
93
+ // Only the classes of Rook have `data` set: lib.dom declares many accessors as fields.
94
+ if (!own || !base || own.method || base.method || base.owner?.data === undefined)
95
+ return;
96
+ if (own.accessor && !base.accessor) {
97
+ this.error(`"${name.name}" is a field in ${base.owner.name}, which would hide a getter or setter ` +
98
+ 'here: the field is set on each instance; override it with a field', name);
99
+ }
100
+ else if (!own.accessor && base.accessor) {
101
+ this.error(`"${name.name}" is a getter or setter in ${base.owner.name}, which a field here would ` +
102
+ 'hide: override it with a getter or setter', name);
103
+ }
104
+ }
105
+ }
@@ -1,14 +1,15 @@
1
1
  import type * as ast from '../ast.ts';
2
2
  import { type ModulePackages } from '../inline-packages.ts';
3
3
  import type { ModuleExports } from './context.ts';
4
- import { DeclarationChecker } from './declarations.ts';
5
- import { type ClassInfo, type Member } from './types.ts';
6
- export declare abstract class ClassChecker extends DeclarationChecker {
4
+ import { ClassMemberChecker } from './class-members.ts';
5
+ import { type ClassInfo } from './types.ts';
6
+ export declare abstract class ClassChecker extends ClassMemberChecker {
7
7
  protected ensureClassResolved(info: ClassInfo): void;
8
8
  /** Resolves the base class and the types of all members (but does not check method bodies). */
9
9
  protected resolveClass(node: ast.ClassDeclaration, info: ClassInfo): void;
10
10
  protected resolveClassMembers(node: ast.ClassDeclaration, info: ClassInfo): void;
11
- protected declareMember(info: ClassInfo, isStatic: boolean, name: ast.Identifier, member: Member): void;
11
+ /** A data class extends only a data class: its fields go with it, and its class is restored. */
12
+ private checkDataBase;
12
13
  /** Method bodies and the checks that need every member of the class. */
13
14
  protected checkClassBodies(node: ast.ClassDeclaration, info: ClassInfo): void;
14
15
  protected checkClassMembers(node: ast.ClassDeclaration, info: ClassInfo): void;
@@ -1,9 +1,10 @@
1
1
  import { inlinePackages } from "../inline-packages.js";
2
- import { DeclarationChecker } from "./declarations.js";
2
+ import { ClassMemberChecker } from "./class-members.js";
3
3
  import { isSuperCall, assignsField } from "./helpers.js";
4
+ import { notCopied } from "./transfer.js";
4
5
  import { explainMismatch, findClassMember, hasZeroValue, isAssignable, isSubclass, typeToString, UNKNOWN, } from "./types.js";
5
6
  // Classes: their base class and members, method bodies, overrides and `implements`; and the declarations a module exports.
6
- export class ClassChecker extends DeclarationChecker {
7
+ export class ClassChecker extends ClassMemberChecker {
7
8
  ensureClassResolved(info) {
8
9
  const resolve = this.unresolvedClasses.get(info);
9
10
  if (!resolve || this.resolvingClasses.has(info))
@@ -49,6 +50,9 @@ export class ClassChecker extends DeclarationChecker {
49
50
  this.error(`cannot extend ${typeToString(base)}: it is not a class`, node.superClass);
50
51
  }
51
52
  }
53
+ if (node.data)
54
+ this.checkDataBase(node, info);
55
+ info.abstract = node.abstract === true;
52
56
  for (const member of node.members) {
53
57
  switch (member.kind) {
54
58
  case 'FieldDeclaration': {
@@ -62,20 +66,31 @@ export class ClassChecker extends DeclarationChecker {
62
66
  else
63
67
  type = this.inferredType(valueType, value);
64
68
  }
69
+ // The fields of a data class are what goes to another thread.
70
+ const reason = node.data && !member.isStatic ? notCopied(type) : null;
71
+ if (reason !== null) {
72
+ this.error(`the fields of a data class go to other threads as copies: ${reason}`, member.type ?? member.name);
73
+ }
65
74
  this.declareMember(info, member.isStatic, member.name, {
66
75
  type,
67
76
  method: false,
68
77
  visibility: member.visibility,
69
78
  owner: info,
79
+ ...(member.readonly ? { readonly: true } : {}),
70
80
  });
71
81
  break;
72
82
  }
73
83
  case 'MethodDeclaration':
84
+ if (member.accessor) {
85
+ this.declareAccessor(info, member, member.accessor);
86
+ break;
87
+ }
74
88
  this.declareMember(info, member.isStatic, member.name, {
75
89
  type: this.signature(member.params, member.results, member.async),
76
90
  method: true,
77
91
  visibility: member.visibility,
78
92
  owner: info,
93
+ ...(member.abstract ? { abstract: true } : {}),
79
94
  });
80
95
  break;
81
96
  case 'ConstructorDeclaration':
@@ -83,15 +98,20 @@ export class ClassChecker extends DeclarationChecker {
83
98
  this.error('a class can have only one constructor', member);
84
99
  info.ctor = this.signature(member.params, []);
85
100
  info.ctorVisibility = member.visibility;
101
+ this.declareParamProperties(member, info, info.ctor);
86
102
  break;
87
103
  }
88
104
  }
89
105
  }
90
- declareMember(info, isStatic, name, member) {
91
- const members = isStatic ? info.statics : info.members;
92
- if (members.has(name.name))
93
- this.error(`duplicate member "${name.name}"`, name);
94
- members.set(name.name, member);
106
+ /** A data class extends only a data class: its fields go with it, and its class is restored. */
107
+ checkDataBase(node, info) {
108
+ const base = info.superClass;
109
+ if (info.untypedBase) {
110
+ this.error('a data class cannot extend a class of JS: its copy in another thread could not get the class back', node.superClass);
111
+ }
112
+ else if (base && base.data !== true) {
113
+ this.error(`a data class extends only a data class: mark ${base.name} as one, e.g. data class ${base.name}`, node.superClass);
114
+ }
95
115
  }
96
116
  /** Method bodies and the checks that need every member of the class. */
97
117
  checkClassBodies(node, info) {
@@ -101,13 +121,18 @@ export class ClassChecker extends DeclarationChecker {
101
121
  let constructorNode = null;
102
122
  for (const member of node.members) {
103
123
  if (member.kind === 'MethodDeclaration') {
104
- const type = (member.isStatic ? info.statics : info.members).get(member.name.name)?.type;
124
+ const { body } = member;
125
+ const type = member.accessor
126
+ ? this.accessorSignatures.get(member)
127
+ : (member.isStatic ? info.statics : info.members).get(member.name.name)?.type;
105
128
  if (type?.kind !== 'function')
106
129
  continue;
107
- this.withClass({ info, isStatic: member.isStatic }, () => this.checkFunction(member.params, type, type.results, member.body, {
108
- async: member.async,
109
- }));
130
+ if (body) {
131
+ this.withClass({ info, isStatic: member.isStatic }, () => this.checkFunction(member.params, type, type.results, body, { async: member.async }));
132
+ }
110
133
  this.checkOverride(info, member);
134
+ if (member.accessor)
135
+ this.checkAccessorOverride(info, member.name, member.isStatic);
111
136
  }
112
137
  else if (member.kind === 'ConstructorDeclaration' && info.ctor) {
113
138
  constructorNode = member;
@@ -119,9 +144,13 @@ export class ClassChecker extends DeclarationChecker {
119
144
  }
120
145
  }
121
146
  }
147
+ this.checkAbstract(node, info);
122
148
  // Fields without a zero value must get one at once or in the constructor.
123
149
  for (const member of node.members) {
124
- if (member.kind !== 'FieldDeclaration' || member.isStatic || member.value)
150
+ if (member.kind !== 'FieldDeclaration')
151
+ continue;
152
+ this.checkAccessorOverride(info, member.name, member.isStatic);
153
+ if (member.isStatic || member.value)
125
154
  continue;
126
155
  const type = info.members.get(member.name.name)?.type;
127
156
  if (!type || hasZeroValue(type))
@@ -166,6 +195,7 @@ export class ClassChecker extends DeclarationChecker {
166
195
  types: new Map(),
167
196
  decorators: new Map(),
168
197
  components: new Map(),
198
+ shared: new Set(),
169
199
  };
170
200
  const declarations = { values: new Map(), types: new Map() };
171
201
  const valueOf = (name) => this.moduleScope.values.get(name)?.type ?? UNKNOWN;
@@ -188,6 +218,8 @@ export class ClassChecker extends DeclarationChecker {
188
218
  case 'VariableDeclaration':
189
219
  for (const name of statement.names) {
190
220
  add(statement.exported, 'values', name.name, valueOf(name.name));
221
+ if (statement.exported && statement.keyword === 'state')
222
+ exports.shared.add(name.name);
191
223
  }
192
224
  break;
193
225
  case 'ClassDeclaration':
@@ -201,7 +233,10 @@ export class ClassChecker extends DeclarationChecker {
201
233
  case 'ComponentDeclaration': {
202
234
  const info = this.moduleScope.values.get(statement.name.name)?.component;
203
235
  if (statement.exported && info?.node === statement) {
204
- info.inline = this.modulePackages().components.get(info.node.name.name) ?? null;
236
+ const found = this.modulePackages().components.get(info.node.name.name) ?? null;
237
+ // Code that reads state another module shares listens to it here, so it stays here.
238
+ const shared = found?.moduleNames.some((name) => this.moduleScope.values.get(name)?.shared !== undefined);
239
+ info.inline = shared ? null : found;
205
240
  info.expressionTypes = this.checkedTypes;
206
241
  exports.components.set(statement.name.name, info);
207
242
  }
@@ -0,0 +1,21 @@
1
+ import type * as ast from '../ast.ts';
2
+ import type { Binding, BindingKind } from './context.ts';
3
+ import { IteratorChecker } from './iterators.ts';
4
+ import type { Type } from './types.ts';
5
+ export declare abstract class ClosingChecker extends IteratorChecker {
6
+ /** The identifier that declares a binding of this module. */
7
+ private readonly declarations;
8
+ /** What each name refers to: its declaration, or the name of a predeclared value. */
9
+ private readonly references;
10
+ protected declareValue(id: ast.Identifier, kind: BindingKind, type: Type | null): Binding;
11
+ protected checkIdentifier(node: ast.Identifier): Type;
12
+ /** The functions declared in the module, by their names, which `use` looks arguments up in. */
13
+ private functions;
14
+ protected checkClosing(): void;
15
+ /** `chan<T>()`, `topic<T>()`, `tick(ms)`, `signal(name)` and their `Channel.make` and the like. */
16
+ private made;
17
+ /** `Channel.close` and the like, as a name, when `Channel` is the predeclared one. */
18
+ private predeclaredMember;
19
+ /** What the code around a name of a channel does with it. */
20
+ private use;
21
+ }
@@ -0,0 +1,164 @@
1
+ import { forEachChild } from "../walk.js";
2
+ import { IteratorChecker } from "./iterators.js";
3
+ /** What `Channel.x(ch)` and `Topic.x(t)` do with a channel without keeping it. */
4
+ const OPERATIONS = new Set(['send', 'receive', 'range', 'stream', 'publish', 'listen', 'try']);
5
+ const MESSAGES = {
6
+ chan: (name) => `"${name}" is never closed: close it when nothing more is sent, e.g. close(${name})`,
7
+ topic: (name) => `"${name}" is never closed, and a topic listens until it is closed: close it when it is no longer needed, e.g. close(${name}); a component closes it with defer close(${name}) in mount`,
8
+ tick: (name) => `"${name}" is never closed, and a tick runs until it is closed: e.g. close(${name})`,
9
+ signal: (name) => `"${name}" is never closed, and it keeps the program running until it is: close it once the signal has come, e.g. close(${name})`,
10
+ };
11
+ export class ClosingChecker extends IteratorChecker {
12
+ /** The identifier that declares a binding of this module. */
13
+ declarations = new WeakMap();
14
+ /** What each name refers to: its declaration, or the name of a predeclared value. */
15
+ references = new Map();
16
+ declareValue(id, kind, type) {
17
+ const binding = super.declareValue(id, kind, type);
18
+ this.declarations.set(binding, id);
19
+ return binding;
20
+ }
21
+ checkIdentifier(node) {
22
+ const binding = this.lookupValue(node.name);
23
+ const declaration = binding && this.declarations.get(binding);
24
+ if (declaration)
25
+ this.references.set(node, declaration);
26
+ else if (binding && binding === this.globals.values.get(node.name)) {
27
+ this.references.set(node, node.name);
28
+ }
29
+ return super.checkIdentifier(node);
30
+ }
31
+ /** The functions declared in the module, by their names, which `use` looks arguments up in. */
32
+ functions = new Map();
33
+ checkClosing() {
34
+ const made = [];
35
+ const uses = new Map();
36
+ const collect = (node) => {
37
+ if (node.kind === 'FuncDeclaration')
38
+ this.functions.set(node.name, node);
39
+ forEachChild(node, collect);
40
+ };
41
+ const visit = (node, parent) => {
42
+ const what = node.kind === 'VariableDeclaration' && !node.exported && node.names.length === 1
43
+ ? node.values[0] && this.made(node.values[0])
44
+ : null;
45
+ if (node.kind === 'VariableDeclaration' && what)
46
+ made.push([node.names[0], what]);
47
+ const declaration = node.kind === 'Identifier' ? this.references.get(node) : undefined;
48
+ if (node.kind === 'Identifier' && typeof declaration === 'object' && parent) {
49
+ if (!uses.has(declaration)) {
50
+ uses.set(declaration, { closed: false, escapes: false, passed: [] });
51
+ }
52
+ this.use(node, parent, uses.get(declaration));
53
+ }
54
+ forEachChild(node, (child) => visit(child, node));
55
+ };
56
+ collect(this.program);
57
+ visit(this.program, null);
58
+ // Whether a variable or a parameter is closed, goes where it is not seen, or neither.
59
+ const fate = (id, seen) => {
60
+ const found = uses.get(id);
61
+ if (!found || seen.has(id))
62
+ return 'none';
63
+ if (found.closed)
64
+ return 'closed';
65
+ seen.add(id);
66
+ let result = found.escapes ? 'escapes' : 'none';
67
+ for (const [fn, i] of found.passed) {
68
+ const param = fn.params[i];
69
+ const inner = param && !param.rest ? fate(param.name, seen) : 'escapes';
70
+ if (inner === 'closed')
71
+ return 'closed';
72
+ if (inner === 'escapes')
73
+ result = 'escapes';
74
+ }
75
+ return result;
76
+ };
77
+ for (const [name, what] of made) {
78
+ if (fate(name, new Set()) === 'none')
79
+ this.warn(MESSAGES[what](name.name), name);
80
+ }
81
+ }
82
+ /** `chan<T>()`, `topic<T>()`, `tick(ms)`, `signal(name)` and their `Channel.make` and the like. */
83
+ made(value) {
84
+ if (value.kind === 'ChanExpression')
85
+ return 'chan';
86
+ if (value.kind === 'TopicExpression')
87
+ return 'topic';
88
+ if (value.kind !== 'CallExpression')
89
+ return null;
90
+ const { callee } = value;
91
+ if (callee.kind === 'Identifier') {
92
+ const predeclared = this.references.get(callee);
93
+ return predeclared === 'tick' || predeclared === 'signal' ? predeclared : null;
94
+ }
95
+ const member = this.predeclaredMember(callee);
96
+ if (member === 'Channel.make')
97
+ return 'chan';
98
+ if (member === 'Channel.tick')
99
+ return 'tick';
100
+ if (member === 'Channel.signal')
101
+ return 'signal';
102
+ return member === 'Topic.make' ? 'topic' : null;
103
+ }
104
+ /** `Channel.close` and the like, as a name, when `Channel` is the predeclared one. */
105
+ predeclaredMember(node) {
106
+ if (node.kind !== 'MemberExpression' || node.object.kind !== 'Identifier')
107
+ return null;
108
+ const object = this.references.get(node.object);
109
+ return (object === 'Channel' || object === 'Topic') && node.property.kind === 'Identifier'
110
+ ? `${object}.${node.property.name}`
111
+ : null;
112
+ }
113
+ /** What the code around a name of a channel does with it. */
114
+ use(node, parent, found) {
115
+ switch (parent.kind) {
116
+ case 'CallExpression': {
117
+ const i = parent.arguments.indexOf(node);
118
+ if (i < 0)
119
+ return;
120
+ const { callee } = parent;
121
+ const ref = callee.kind === 'Identifier' ? this.references.get(callee) : undefined;
122
+ const member = this.predeclaredMember(callee)?.split('.')[1];
123
+ if (ref === 'close' || member === 'close')
124
+ found.closed = true;
125
+ else if (ref === 'stream' || (member !== undefined && OPERATIONS.has(member)))
126
+ return;
127
+ else {
128
+ const fn = callee.kind === 'FuncExpression'
129
+ ? callee
130
+ : typeof ref === 'object'
131
+ ? this.functions.get(ref)
132
+ : undefined;
133
+ if (fn)
134
+ found.passed.push([fn, i]);
135
+ else
136
+ found.escapes = true;
137
+ }
138
+ return;
139
+ }
140
+ // Sending to it, receiving from it, a loop over it, comparing and assigning it keep it here;
141
+ // sending it through a channel does not.
142
+ case 'SendStatement':
143
+ if (parent.value === node)
144
+ found.escapes = true;
145
+ return;
146
+ case 'UnaryExpression':
147
+ if (parent.operator !== '<-')
148
+ found.escapes = true;
149
+ return;
150
+ case 'ForInStatement':
151
+ return;
152
+ case 'BinaryExpression':
153
+ if (!['==', '!=', '===', '!=='].includes(parent.operator))
154
+ found.escapes = true;
155
+ return;
156
+ case 'AssignmentStatement':
157
+ if (!parent.targets.includes(node))
158
+ found.escapes = true;
159
+ return;
160
+ default:
161
+ found.escapes = true;
162
+ }
163
+ }
164
+ }
@@ -1,8 +1,8 @@
1
1
  import type * as ast from '../ast.ts';
2
- import { IteratorChecker } from './iterators.ts';
3
- import type { CompletionResult } from './context.ts';
2
+ import { ClosingChecker } from './closing.ts';
3
+ import { type CompletionResult } from './context.ts';
4
4
  import { type Type } from './types.ts';
5
- export declare abstract class CompletionChecker extends IteratorChecker {
5
+ export declare abstract class CompletionChecker extends ClosingChecker {
6
6
  protected completion: CompletionResult | null;
7
7
  private atCursor;
8
8
  /** The first check of the cursor counts: a component's body, for one, is checked for each use. */
@@ -1,5 +1,6 @@
1
- import { IteratorChecker } from "./iterators.js";
1
+ import { ClosingChecker } from "./closing.js";
2
2
  import { ARRAY_MEMBERS, ATTRIBUTE_NAMES, BIGINT_MEMBERS, BOOL_MEMBERS, EVENT_ATTRIBUTES, HTML_TAGS, NUMBER_MEMBERS, STRING_MEMBERS, } from "./completion-names.js";
3
+ import { placeOf, } from "./context.js";
3
4
  import { isSubclass, literalBase, memberNames, memberTypeOf, nonNull, typeToString, union, UNKNOWN, } from "./types.js";
4
5
  // Completion in editors (src/completion.ts): the editor writes the name `options.completion` where
5
6
  // the cursor is, and this layer lists what can be written there when the check reaches that name:
@@ -12,7 +13,7 @@ const PRIMITIVE_MEMBERS = {
12
13
  bigint: BIGINT_MEMBERS,
13
14
  bool: BOOL_MEMBERS,
14
15
  };
15
- export class CompletionChecker extends IteratorChecker {
16
+ export class CompletionChecker extends ClosingChecker {
16
17
  completion = null;
17
18
  atCursor(node) {
18
19
  return node.name === this.options.completion;
@@ -79,7 +80,7 @@ export class CompletionChecker extends IteratorChecker {
79
80
  if (component) {
80
81
  return { name, kind: 'component', detail: typeToString(this.componentValue(component)) };
81
82
  }
82
- const type = binding.type && (this.flow.get(binding) ?? binding.type);
83
+ const type = binding.type && (this.flow.get(placeOf(binding)) ?? binding.type);
83
84
  const detail = type ? { detail: typeToString(type) } : {};
84
85
  if (kind === 'function' || type?.kind === 'function')
85
86
  return { name, kind: 'function', ...detail };
@@ -0,0 +1,7 @@
1
+ import type * as ast from '../ast.ts';
2
+ import type { ComponentInfo } from './context.ts';
3
+ import { ForwardingChecker } from './forwarding.ts';
4
+ export declare abstract class ComponentBindingChecker extends ForwardingChecker {
5
+ /** `bind:active={tab}` on a component; adds the name of the property to `given`. */
6
+ protected checkPropBinding(attribute: ast.JsxAttribute, info: ComponentInfo, component: string, given: Set<string>): void;
7
+ }
@@ -0,0 +1,45 @@
1
+ import { attrsParam } from "../attrs.js";
2
+ import { ForwardingChecker } from "./forwarding.js";
3
+ import { isUntyped } from "./helpers.js";
4
+ import { isAssignable, typeToString } from "./types.js";
5
+ // Bound properties: `comp Tabs(bind active string)` may change `active`, and
6
+ // `<Tabs bind:active={tab} />` writes those changes into the parent's `tab`, as `bind:value` of an
7
+ // input does. The value goes both ways, so the variable and the property have the same type.
8
+ export class ComponentBindingChecker extends ForwardingChecker {
9
+ /** `bind:active={tab}` on a component; adds the name of the property to `given`. */
10
+ checkPropBinding(attribute, info, component, given) {
11
+ const name = attribute.name.name.slice('bind:'.length);
12
+ const prop = info.props.get(name);
13
+ const { value } = attribute;
14
+ if (given.has(name))
15
+ this.error(`duplicate property "${name}"`, attribute.name);
16
+ given.add(name);
17
+ if (!prop) {
18
+ this.error(attrsParam(info.node)
19
+ ? `${attribute.name.name} does not go through the attributes of <${component}>`
20
+ : `<${component}> has no property "${name}" to bind`, attribute.name);
21
+ }
22
+ else if (!prop.bind) {
23
+ this.error(`"${name}" of <${component}> is not bound: the component declares it with bind to change it, e.g. comp ${component}(bind ${name} ...)`, attribute.name);
24
+ }
25
+ if (value === null || value.kind === 'StringLiteral' || value.kind === 'EventHandler') {
26
+ this.error(`"bind:${name}" needs a variable in braces, e.g. bind:${name}={${name}}`, attribute);
27
+ return;
28
+ }
29
+ const bindable = (value.kind === 'Identifier' && value.name !== '_') ||
30
+ ((value.kind === 'MemberExpression' || value.kind === 'IndexExpression') && !value.optional);
31
+ if (!bindable) {
32
+ this.checkValue(value);
33
+ this.error(`"bind:${name}" needs a variable or a field to write to`, value);
34
+ return;
35
+ }
36
+ const type = this.checkTarget(value);
37
+ this.checkedTypes.set(value, type);
38
+ if (!prop?.bind || isUntyped(type) || isUntyped(prop.type))
39
+ return;
40
+ // The parent gives its value, and the component writes its own back.
41
+ if (!isAssignable(type, prop.type) || !isAssignable(prop.type, type)) {
42
+ this.error(`cannot bind ${typeToString(type)} to "${name}" of <${component}>, which is ${typeToString(prop.type)}`, value);
43
+ }
44
+ }
45
+ }
@@ -1,6 +1,5 @@
1
1
  import { ComponentChecker } from "./components.js";
2
2
  import { isUntyped } from "./helpers.js";
3
- const upperFirst = (name) => name.charAt(0).toUpperCase() + name.slice(1);
4
3
  import { isNullable, typeToString, UNKNOWN, } from "./types.js";
5
4
  // Components as values: `content: Intro` has the type `comp()`, and a tag takes the component
6
5
  // from a value, `<section.content />`, or `<Page />` when `Page` is a variable. Such a tag passes
@@ -20,12 +19,8 @@ export class ComponentValueChecker extends ComponentChecker {
20
19
  const name = tag.name;
21
20
  if (isUntyped(type) || type.kind !== 'component') {
22
21
  if (isNullable(type)) {
23
- // Only variables are narrowed, so a path goes into one first.
24
- const variable = /^[A-Z]/.test(name) ? name : upperFirst(name.split('.').at(-1));
25
- const show = `{if ${variable} != null { <${variable} /> }}`;
26
- this.error(name === variable
27
- ? `<${name}> may be null: show it with {if}, e.g. ${show}`
28
- : `<${name}> may be null: put it into a variable and show it with {if}, e.g. const ${variable} = ${name}, then ${show}`, tag);
22
+ // A path of fields is narrowed like a variable: `{if page.other != null { <page.other /> }}`.
23
+ this.error(`<${name}> may be null: show it with {if}, e.g. {if ${name} != null { <${name} /> }}`, tag);
29
24
  }
30
25
  else if (!isUntyped(type)) {
31
26
  this.error(`<${name}> is ${typeToString(type)}, not a component: give it a type like comp(title string)`, tag);
@@ -58,6 +53,14 @@ export class ComponentValueChecker extends ComponentChecker {
58
53
  this.error(`ref cannot point to <${name}>: its component is a value, so its element is not known in advance`, attribute);
59
54
  continue;
60
55
  }
56
+ // The type of the value does not say which properties are bound.
57
+ if (attributeName.startsWith('bind:')) {
58
+ const { value } = attribute;
59
+ if (value && value.kind !== 'EventHandler')
60
+ this.checkValue(value);
61
+ this.error(`<${name}> takes its component from a value, so it cannot bind its properties; pass a function, e.g. onChange={...}`, attribute.name);
62
+ continue;
63
+ }
61
64
  if (given.has(attributeName)) {
62
65
  this.error(`duplicate property "${attributeName}"`, attribute.name);
63
66
  }
@@ -1,8 +1,8 @@
1
1
  import type * as ast from '../ast.ts';
2
2
  import type { ComponentInfo } from './context.ts';
3
- import { ForwardingChecker } from './forwarding.ts';
3
+ import { ComponentBindingChecker } from './component-bindings.ts';
4
4
  import { type Type } from './types.ts';
5
- export declare abstract class ComponentChecker extends ForwardingChecker {
5
+ export declare abstract class ComponentChecker extends ComponentBindingChecker {
6
6
  protected resolveProps(info: ComponentInfo): void;
7
7
  /** The body runs once where the component is used and must end with `return <markup>`. */
8
8
  protected checkComponentBody(info: ComponentInfo): void;
@@ -3,10 +3,11 @@ import { freeNames } from "../names.js";
3
3
  import { endlessRecursion } from "../recursion.js";
4
4
  import { CONTENT } from "./dom.js";
5
5
  import { lookupIn, ownStatements, isUntyped, callSignature } from "./helpers.js";
6
- import { ATTRIBUTES, ForwardingChecker } from "./forwarding.js";
6
+ import { ComponentBindingChecker } from "./component-bindings.js";
7
+ import { ATTRIBUTES } from "./forwarding.js";
7
8
  import { BOOL, func, hasZeroValue, isAssignable, isNullable, nonNull, spreadFields, typesEqual, typeToString, UNKNOWN, } from "./types.js";
8
9
  // Components: their bodies and uses, properties and spread, names they use from the module, recursion.
9
- export class ComponentChecker extends ForwardingChecker {
10
+ export class ComponentChecker extends ComponentBindingChecker {
10
11
  resolveProps(info) {
11
12
  for (const param of info.node.params) {
12
13
  // `...attrs` takes the attributes that are not properties.
@@ -20,7 +21,7 @@ export class ComponentChecker extends ForwardingChecker {
20
21
  if (info.props.has(name))
21
22
  this.error(`duplicate property "${name}"`, param.name);
22
23
  const optional = param.defaultValue !== null || isNullable(type) || name === 'children';
23
- info.props.set(name, { type, optional });
24
+ info.props.set(name, { type, optional, ...(param.bind ? { bind: true } : {}) });
24
25
  }
25
26
  }
26
27
  /** The body runs once where the component is used and must end with `return <markup>`. */
@@ -96,6 +97,10 @@ export class ComponentChecker extends ForwardingChecker {
96
97
  this.checkRefAttribute(attribute, node);
97
98
  continue;
98
99
  }
100
+ if (name.startsWith('bind:')) {
101
+ this.checkPropBinding(attribute, info, tag.name, given);
102
+ continue;
103
+ }
99
104
  if (given.has(name))
100
105
  this.error(`duplicate property "${name}"`, attribute.name);
101
106
  given.add(name);