rooklang 0.3.4 → 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.
- package/README.md +2 -0
- package/README.ru.md +2 -0
- package/dist/ast/declarations.d.ts +19 -3
- package/dist/ast/expressions.d.ts +4 -8
- package/dist/ast/statements.d.ts +3 -1
- package/dist/checker/assignments.d.ts +2 -0
- package/dist/checker/assignments.js +9 -3
- package/dist/checker/base.d.ts +8 -2
- package/dist/checker/base.js +3 -3
- package/dist/checker/builtins.js +7 -3
- package/dist/checker/channels.js +1 -2
- package/dist/checker/classes.js +7 -1
- package/dist/checker/closing.js +1 -1
- package/dist/checker/completion.d.ts +1 -1
- package/dist/checker/completion.js +2 -1
- package/dist/checker/component-bindings.d.ts +7 -0
- package/dist/checker/component-bindings.js +45 -0
- package/dist/checker/component-values.js +10 -7
- package/dist/checker/components.d.ts +2 -2
- package/dist/checker/components.js +8 -3
- package/dist/checker/context.d.ts +28 -5
- package/dist/checker/context.js +23 -0
- package/dist/checker/control-flow.d.ts +6 -5
- package/dist/checker/control-flow.js +48 -40
- package/dist/checker/declarations.d.ts +2 -2
- package/dist/checker/declarations.js +3 -2
- package/dist/checker/decorator-exports.js +4 -0
- package/dist/checker/decorator-includes.d.ts +9 -0
- package/dist/checker/decorator-includes.js +71 -0
- package/dist/checker/decorator-members.d.ts +8 -10
- package/dist/checker/decorator-members.js +28 -31
- package/dist/checker/decorator-wrappers.d.ts +1 -1
- package/dist/checker/decorator-wrappers.js +3 -2
- package/dist/checker/decorators.d.ts +14 -5
- package/dist/checker/decorators.js +52 -25
- package/dist/checker/expressions.d.ts +1 -1
- package/dist/checker/expressions.js +7 -6
- package/dist/checker/flow.d.ts +25 -3
- package/dist/checker/flow.js +48 -8
- package/dist/checker/functions.d.ts +2 -2
- package/dist/checker/functions.js +9 -5
- package/dist/checker/helpers.d.ts +4 -1
- package/dist/checker/helpers.js +7 -2
- package/dist/checker/imports.js +5 -2
- package/dist/checker/loops.d.ts +2 -0
- package/dist/checker/loops.js +27 -2
- package/dist/checker/markup.js +14 -2
- package/dist/checker/members.js +3 -1
- package/dist/checker/shared-state.d.ts +11 -0
- package/dist/checker/shared-state.js +37 -0
- package/dist/checker/statements.d.ts +5 -2
- package/dist/checker/statements.js +51 -22
- package/dist/checker/tagged-unions.d.ts +37 -0
- package/dist/checker/tagged-unions.js +132 -0
- package/dist/checker/untyped-globals.d.ts +1 -0
- package/dist/checker/untyped-globals.js +3 -1
- package/dist/codegen/attributes.d.ts +4 -0
- package/dist/codegen/attributes.js +24 -2
- package/dist/codegen/component-helpers.d.ts +6 -1
- package/dist/codegen/component-helpers.js +6 -1
- package/dist/codegen/component-props.d.ts +11 -2
- package/dist/codegen/component-props.js +50 -5
- package/dist/codegen/components.d.ts +6 -5
- package/dist/codegen/components.js +44 -21
- package/dist/codegen/control-flow.d.ts +2 -0
- package/dist/codegen/control-flow.js +23 -9
- package/dist/codegen/creation.js +5 -2
- package/dist/codegen/early-returns.d.ts +4 -3
- package/dist/codegen/early-returns.js +17 -5
- package/dist/codegen/emitter.d.ts +3 -0
- package/dist/codegen/emitter.js +18 -4
- package/dist/codegen/expressions.js +0 -2
- package/dist/codegen/functions.d.ts +11 -1
- package/dist/codegen/functions.js +31 -3
- package/dist/codegen/live.d.ts +60 -0
- package/dist/codegen/live.js +29 -0
- package/dist/codegen/mutations.d.ts +7 -0
- package/dist/codegen/mutations.js +43 -2
- package/dist/codegen/options.d.ts +2 -0
- package/dist/codegen/reactive.d.ts +4 -59
- package/dist/codegen/reactive.js +34 -72
- package/dist/codegen/runtime.js +6 -1
- package/dist/codegen/shared.d.ts +22 -0
- package/dist/codegen/shared.js +93 -0
- package/dist/codegen/simple-statements.d.ts +1 -1
- package/dist/codegen/statements.d.ts +5 -0
- package/dist/codegen/statements.js +35 -0
- package/dist/codegen/updates.d.ts +11 -3
- package/dist/codegen/updates.js +52 -4
- package/dist/codegen/web-components.d.ts +2 -2
- package/dist/codegen/web-components.js +2 -2
- package/dist/compile.js +6 -2
- package/dist/decorator-copy.d.ts +1 -1
- package/dist/decorator-copy.js +1 -0
- package/dist/decorator-names.d.ts +14 -0
- package/dist/decorator-names.js +56 -0
- package/dist/decorators.d.ts +2 -3
- package/dist/decorators.js +36 -14
- package/dist/inline-packages.d.ts +2 -1
- package/dist/inline-packages.js +9 -4
- package/dist/names.d.ts +4 -0
- package/dist/names.js +29 -0
- package/dist/parser/base.d.ts +1 -0
- package/dist/parser/base.js +1 -1
- package/dist/parser/control-flow.d.ts +5 -0
- package/dist/parser/control-flow.js +28 -2
- package/dist/parser/declarations.d.ts +5 -15
- package/dist/parser/declarations.js +15 -119
- package/dist/parser/decorators.d.ts +6 -2
- package/dist/parser/decorators.js +43 -19
- package/dist/parser/params.d.ts +21 -0
- package/dist/parser/params.js +143 -0
- package/dist/parser/simple-statements.d.ts +1 -0
- package/dist/parser/simple-statements.js +22 -6
- package/dist/parser/statements.d.ts +5 -2
- package/dist/parser/statements.js +18 -11
- package/dist/runtime/sources.js +3 -2
- package/dist/walk.js +3 -3
- package/package.json +2 -2
- package/src/runtime/decode.js +41 -10
- package/src/runtime/index.js +1 -0
- package/src/runtime/markup.js +27 -17
- package/src/runtime/shared.js +17 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { freeNames } from "../names.js";
|
|
2
2
|
import { forEachChild } from "../walk.js";
|
|
3
|
+
import { withIncluded, } from "./context.js";
|
|
3
4
|
import { DecoratorExportChecker } from "./decorator-exports.js";
|
|
4
5
|
import { lookupIn, ownStatements } from "./helpers.js";
|
|
5
6
|
import { func, UNKNOWN } from "./types.js";
|
|
@@ -42,7 +43,9 @@ export class DecoratorChecker extends DecoratorExportChecker {
|
|
|
42
43
|
needed.add(name);
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Decorators after the ones they need or include. A cycle is an error: none can be first.
|
|
48
|
+
*/
|
|
46
49
|
orderByNeeds(decorators) {
|
|
47
50
|
const order = [];
|
|
48
51
|
const path = [];
|
|
@@ -51,13 +54,17 @@ export class DecoratorChecker extends DecoratorExportChecker {
|
|
|
51
54
|
return;
|
|
52
55
|
const at = path.indexOf(info);
|
|
53
56
|
if (at >= 0) {
|
|
54
|
-
const cycle = path.slice(at)
|
|
55
|
-
const steps = cycle.map((
|
|
56
|
-
|
|
57
|
+
const cycle = path.slice(at);
|
|
58
|
+
const steps = cycle.map((each, i) => {
|
|
59
|
+
const next = cycle[(i + 1) % cycle.length];
|
|
60
|
+
const how = each.included.includes(next) ? 'includes' : 'needs';
|
|
61
|
+
return `@${each.node.name.name} ${how} @${next.node.name.name}`;
|
|
62
|
+
});
|
|
63
|
+
this.error(`decorators cannot need or include each other: ${steps.join(', ')}; remove one of them`, info.node.name);
|
|
57
64
|
return;
|
|
58
65
|
}
|
|
59
66
|
path.push(info);
|
|
60
|
-
for (const needed of info.needed)
|
|
67
|
+
for (const needed of [...info.needed, ...info.included])
|
|
61
68
|
visit(needed);
|
|
62
69
|
path.pop();
|
|
63
70
|
order.push(info);
|
|
@@ -112,8 +119,7 @@ export class DecoratorChecker extends DecoratorExportChecker {
|
|
|
112
119
|
}
|
|
113
120
|
else if (statement.kind === 'VariableDeclaration' &&
|
|
114
121
|
statement.isPublic &&
|
|
115
|
-
statement.keyword !== 'let'
|
|
116
|
-
statement.keyword !== 'ref') {
|
|
122
|
+
statement.keyword !== 'let') {
|
|
117
123
|
for (const name of statement.names)
|
|
118
124
|
add(name, statement.keyword);
|
|
119
125
|
}
|
|
@@ -121,10 +127,16 @@ export class DecoratorChecker extends DecoratorExportChecker {
|
|
|
121
127
|
}
|
|
122
128
|
/**
|
|
123
129
|
* The decorators before `comp`: they exist, are applied once and below the ones they need, get
|
|
124
|
-
* valid arguments, and their wrappers get what they can take.
|
|
130
|
+
* valid arguments, and their wrappers get what they can take. Those that a decorator includes
|
|
131
|
+
* are applied with it, above it.
|
|
125
132
|
*/
|
|
126
133
|
checkDecoratorUses(info) {
|
|
127
|
-
|
|
134
|
+
// The decorator that each one is applied with, for those that another includes.
|
|
135
|
+
const applied = new Map();
|
|
136
|
+
const props = info.node.params.map((param) => [
|
|
137
|
+
param.name,
|
|
138
|
+
info.props.get(param.name.name)?.type ?? UNKNOWN,
|
|
139
|
+
]);
|
|
128
140
|
for (const use of info.node.decorators) {
|
|
129
141
|
const name = use.name.name;
|
|
130
142
|
const decorator = this.decorators.get(name);
|
|
@@ -132,23 +144,39 @@ export class DecoratorChecker extends DecoratorExportChecker {
|
|
|
132
144
|
this.error(`unknown decorator @${name}`, use.name);
|
|
133
145
|
continue;
|
|
134
146
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
continue;
|
|
141
|
-
const other = needed.node.name.name;
|
|
142
|
-
this.error(`@${name} needs @${other} above it: @${other} @${name} comp ...`, use);
|
|
147
|
+
for (const each of withIncluded([decorator])) {
|
|
148
|
+
const via = each === decorator ? null : name;
|
|
149
|
+
this.checkApplied(each, via, applied, use);
|
|
150
|
+
applied.set(each, via);
|
|
151
|
+
this.checkInlinedNames(use, each, info);
|
|
143
152
|
}
|
|
144
|
-
|
|
145
|
-
this.checkDecoratorArguments(use, decorator, info);
|
|
146
|
-
this.checkInlinedNames(use, decorator, info);
|
|
153
|
+
this.checkDecoratorArguments(use, decorator, props);
|
|
147
154
|
}
|
|
148
155
|
this.checkWrappedTypes(info);
|
|
149
156
|
}
|
|
150
|
-
/**
|
|
151
|
-
|
|
157
|
+
/** Applied once, below the ones it needs; `via` is the decorator that includes it. */
|
|
158
|
+
checkApplied(decorator, via, applied, use) {
|
|
159
|
+
const name = decorator.node.name.name;
|
|
160
|
+
const where = (other) => (other === null ? '' : ` (it is in @${other})`);
|
|
161
|
+
if (applied.has(decorator)) {
|
|
162
|
+
const before = applied.get(decorator) ?? null;
|
|
163
|
+
this.error(before === null && via === null
|
|
164
|
+
? `@${name} is applied twice: apply it once`
|
|
165
|
+
: `@${name} is applied twice${where(via ?? before)}: apply it once`, use);
|
|
166
|
+
}
|
|
167
|
+
// The body of a decorator may read the state of those it needs right away.
|
|
168
|
+
for (const needed of decorator.needed) {
|
|
169
|
+
if (applied.has(needed))
|
|
170
|
+
continue;
|
|
171
|
+
const other = needed.node.name.name;
|
|
172
|
+
this.error(`@${name}${where(via)} needs @${other} above it: @${other} @${via ?? name} comp ...`, use);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Arguments are positional and see `params`: the properties of the component, as its body does,
|
|
177
|
+
* or the parameters of the decorator that includes this one.
|
|
178
|
+
*/
|
|
179
|
+
checkDecoratorArguments(use, decorator, scope) {
|
|
152
180
|
const name = use.name.name;
|
|
153
181
|
const { params } = decorator.node;
|
|
154
182
|
const extra = use.arguments[params.length];
|
|
@@ -157,9 +185,8 @@ export class DecoratorChecker extends DecoratorExportChecker {
|
|
|
157
185
|
this.error(params.length === 0 ? `@${name} takes no arguments` : `@${name} takes ${count}`, extra);
|
|
158
186
|
}
|
|
159
187
|
this.withScope(() => {
|
|
160
|
-
for (const param of
|
|
161
|
-
this.declareValue(param
|
|
162
|
-
}
|
|
188
|
+
for (const [param, type] of scope)
|
|
189
|
+
this.declareValue(param, 'prop', type);
|
|
163
190
|
params.forEach((param, i) => {
|
|
164
191
|
const type = decorator.params[i] ?? UNKNOWN;
|
|
165
192
|
const arg = use.arguments[i];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type * as ast from '../ast.ts';
|
|
2
|
-
import type
|
|
2
|
+
import { type ComponentInfo } from './context.ts';
|
|
3
3
|
import { FunctionChecker } from './functions.ts';
|
|
4
4
|
import { type ComponentType, type Type } from './types.ts';
|
|
5
5
|
export declare abstract class ExpressionChecker extends FunctionChecker {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { placeOf } from "./context.js";
|
|
1
2
|
import { FunctionChecker } from "./functions.js";
|
|
2
3
|
import { expectsLiteral, callName, isUntyped } from "./helpers.js";
|
|
3
4
|
import { ANY, BIGINT, BOOL, hasUntypedBase, instanceMembers, isAssignable, literal, nonNull, NULL, nullable, NUMBER, STRING, typeToString, UNKNOWN, } from "./types.js";
|
|
@@ -90,8 +91,6 @@ export class ExpressionChecker extends FunctionChecker {
|
|
|
90
91
|
return this.checkElement(node);
|
|
91
92
|
case 'DecoratorMember':
|
|
92
93
|
return this.checkDecoratorMember(node);
|
|
93
|
-
case 'DecoratorGet':
|
|
94
|
-
return this.checkDecoratorGet(node);
|
|
95
94
|
case 'MemberExpression':
|
|
96
95
|
case 'IndexExpression':
|
|
97
96
|
case 'CallExpression': {
|
|
@@ -130,7 +129,7 @@ export class ExpressionChecker extends FunctionChecker {
|
|
|
130
129
|
this.error(`"${node.name}" is used before its declaration`, node);
|
|
131
130
|
return UNKNOWN;
|
|
132
131
|
}
|
|
133
|
-
return this.flow.get(binding) ?? binding.type;
|
|
132
|
+
return this.flow.get(placeOf(binding)) ?? binding.type;
|
|
134
133
|
}
|
|
135
134
|
thisType(node) {
|
|
136
135
|
if (!this.cls) {
|
|
@@ -155,10 +154,11 @@ export class ExpressionChecker extends FunctionChecker {
|
|
|
155
154
|
}
|
|
156
155
|
checkObjectLiteral(node, expected) {
|
|
157
156
|
let target = expected ? nonNull(expected) : null;
|
|
158
|
-
// `?(ScrollIntoViewOptions | bool)`: an object literal can only be the object type in it
|
|
157
|
+
// `?(ScrollIntoViewOptions | bool)`: an object literal can only be the object type in it, or
|
|
158
|
+
// the one that its tag chooses, `{ kind: "pickup" }` of `Pickup | Courier`.
|
|
159
159
|
if (target?.kind === 'union') {
|
|
160
160
|
const objects = target.types.filter((type) => type.kind === 'object' || type.kind === 'class');
|
|
161
|
-
target = objects.length === 1 ? objects[0] :
|
|
161
|
+
target = objects.length === 1 ? objects[0] : this.taggedMember(node, objects, target);
|
|
162
162
|
}
|
|
163
163
|
const index = target?.kind === 'object' ? target.index : undefined;
|
|
164
164
|
const targetMembers = target?.kind === 'object'
|
|
@@ -168,7 +168,8 @@ export class ExpressionChecker extends FunctionChecker {
|
|
|
168
168
|
: null;
|
|
169
169
|
const members = new Map();
|
|
170
170
|
const written = new Set();
|
|
171
|
-
|
|
171
|
+
// A tag of no member is reported once, not again where the literal is used.
|
|
172
|
+
let untyped = target?.kind === 'unknown';
|
|
172
173
|
for (const property of node.properties) {
|
|
173
174
|
if (property.kind === 'SpreadElement') {
|
|
174
175
|
const spread = this.checkValue(property.argument);
|
package/dist/checker/flow.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type * as ast from '../ast.ts';
|
|
2
|
-
import type { Flow, Narrowing } from './context.ts';
|
|
3
|
-
import { type Type } from './types.ts';
|
|
2
|
+
import type { Binding, Flow, Narrowing, Place } from './context.ts';
|
|
3
|
+
import { type LiteralType, type Type } from './types.ts';
|
|
4
4
|
/** What `typeof` gives for every value of a type, or `null` when it may give several things. */
|
|
5
5
|
export declare function typeofTag(type: Type): string | null;
|
|
6
6
|
/** The type after `typeof x == tag` is found true (`matches`) or false. */
|
|
@@ -9,8 +9,30 @@ export declare function narrowByTypeof(type: Type, tag: string, matches: boolean
|
|
|
9
9
|
export declare function literalValue(node: ast.Expression): string | number | boolean | undefined;
|
|
10
10
|
/** A type without one literal value: what is left after `x != "all"`. */
|
|
11
11
|
export declare function withoutLiteral(type: Type, value: string | number | boolean): Type;
|
|
12
|
+
/**
|
|
13
|
+
* A union after a comparison of its tag, a field with literal types in each member: `tags` has the
|
|
14
|
+
* values the tag may have in each member, `values` those it is compared with. When `equal`, the
|
|
15
|
+
* members whose tag may be one of the values remain; otherwise those whose tag may be another one.
|
|
16
|
+
*/
|
|
17
|
+
export declare function narrowByTag(type: Type, tags: readonly (readonly LiteralType['value'][])[], values: readonly LiteralType['value'][], equal: boolean): Type;
|
|
18
|
+
/**
|
|
19
|
+
* Whether `field` is a tag of what `place` was declared as: then an assignment to it may change
|
|
20
|
+
* the member of the union that the place was narrowed to.
|
|
21
|
+
*/
|
|
22
|
+
export type IsTag = (place: Place, field: string) => boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Whether an assignment to the fields `written` of a variable ends the narrowing of `place`, a
|
|
25
|
+
* path of the same variable or the variable itself (the empty path): it writes the path, its
|
|
26
|
+
* start or a tag of it. `"delivery.kind"` ends the narrowing of `"delivery"`, which may come from
|
|
27
|
+
* that tag; `"delivery.floor"` does not.
|
|
28
|
+
*/
|
|
29
|
+
export declare function overwrites(written: string, place: Place, isTag: IsTag): boolean;
|
|
30
|
+
/** Ends the narrowing that an assignment to `binding` or to its fields `written` changes. */
|
|
31
|
+
export declare function forgetWritten(flow: Flow, binding: Binding, written: string, isTag: IsTag): void;
|
|
32
|
+
/** The flow without what assignments to the variables `names` and the paths `paths` change. */
|
|
33
|
+
export declare function withoutAssigned(flow: Flow, names: ReadonlySet<string>, paths: ReadonlySet<string>, isTag: IsTag): Flow;
|
|
12
34
|
export declare function withNarrowing(flow: Flow, narrowing: Narrowing): Flow;
|
|
13
|
-
/** Narrowing that holds after both branches: the
|
|
35
|
+
/** Narrowing that holds after both branches: the places narrowed the same way in both. */
|
|
14
36
|
export declare function mergeFlows(a: Flow, b: Flow): Flow;
|
|
15
37
|
/** Go's terminating statements: execution never continues after them. */
|
|
16
38
|
export declare function isTerminating(node: ast.Statement): boolean;
|
package/dist/checker/flow.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { containsBreak } from "../breaks.js";
|
|
2
2
|
import { BIGINT, BOOL, isNullable, nonNull, NULL, NUMBER, STRING, typesEqual, union, unionMembers, } from "./types.js";
|
|
3
|
-
// Control flow for the checker: how conditions narrow the types of variables, and which
|
|
4
|
-
// execution never continues after.
|
|
3
|
+
// Control flow for the checker: how conditions narrow the types of variables and paths, and which
|
|
4
|
+
// statements execution never continues after.
|
|
5
5
|
/** What a value of unknown type is after `typeof x == tag`. */
|
|
6
6
|
const PRIMITIVE_TAGS = new Map([
|
|
7
7
|
['string', STRING],
|
|
@@ -67,19 +67,59 @@ export function withoutLiteral(type, value) {
|
|
|
67
67
|
const kept = unionMembers(nonNull(type)).filter((member) => !(member.kind === 'literal' && member.value === value));
|
|
68
68
|
return union(isNullable(type) ? [...kept, NULL] : kept);
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* A union after a comparison of its tag, a field with literal types in each member: `tags` has the
|
|
72
|
+
* values the tag may have in each member, `values` those it is compared with. When `equal`, the
|
|
73
|
+
* members whose tag may be one of the values remain; otherwise those whose tag may be another one.
|
|
74
|
+
*/
|
|
75
|
+
export function narrowByTag(type, tags, values, equal) {
|
|
76
|
+
const kept = unionMembers(nonNull(type)).filter((_, i) => {
|
|
77
|
+
const tag = tags[i] ?? [];
|
|
78
|
+
return equal
|
|
79
|
+
? tag.some((value) => values.includes(value))
|
|
80
|
+
: !tag.every((value) => values.includes(value));
|
|
81
|
+
});
|
|
82
|
+
return union(!equal && isNullable(type) ? [...kept, NULL] : kept);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Whether an assignment to the fields `written` of a variable ends the narrowing of `place`, a
|
|
86
|
+
* path of the same variable or the variable itself (the empty path): it writes the path, its
|
|
87
|
+
* start or a tag of it. `"delivery.kind"` ends the narrowing of `"delivery"`, which may come from
|
|
88
|
+
* that tag; `"delivery.floor"` does not.
|
|
89
|
+
*/
|
|
90
|
+
export function overwrites(written, place, isTag) {
|
|
91
|
+
const { path } = place;
|
|
92
|
+
if (written === '' || written === path || path.startsWith(`${written}.`))
|
|
93
|
+
return true;
|
|
94
|
+
const field = path === '' ? written : written.startsWith(`${path}.`) ? written.slice(path.length + 1) : null;
|
|
95
|
+
return field !== null && !field.includes('.') && isTag(place, field);
|
|
96
|
+
}
|
|
97
|
+
/** Ends the narrowing that an assignment to `binding` or to its fields `written` changes. */
|
|
98
|
+
export function forgetWritten(flow, binding, written, isTag) {
|
|
99
|
+
for (const place of flow.keys()) {
|
|
100
|
+
if (place.binding === binding && overwrites(written, place, isTag))
|
|
101
|
+
flow.delete(place);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** The flow without what assignments to the variables `names` and the paths `paths` change. */
|
|
105
|
+
export function withoutAssigned(flow, names, paths, isTag) {
|
|
106
|
+
const written = [...paths].map((path) => path.split('.'));
|
|
107
|
+
return new Map([...flow].filter(([place]) => !names.has(place.binding.name) &&
|
|
108
|
+
!written.some(([root, ...fields]) => root === place.binding.name && overwrites(fields.join('.'), place, isTag))));
|
|
109
|
+
}
|
|
70
110
|
export function withNarrowing(flow, narrowing) {
|
|
71
111
|
const result = new Map(flow);
|
|
72
|
-
for (const [
|
|
73
|
-
result.set(
|
|
112
|
+
for (const [place, type] of narrowing)
|
|
113
|
+
result.set(place, type);
|
|
74
114
|
return result;
|
|
75
115
|
}
|
|
76
|
-
/** Narrowing that holds after both branches: the
|
|
116
|
+
/** Narrowing that holds after both branches: the places narrowed the same way in both. */
|
|
77
117
|
export function mergeFlows(a, b) {
|
|
78
118
|
const result = new Map();
|
|
79
|
-
for (const [
|
|
80
|
-
const other = b.get(
|
|
119
|
+
for (const [place, type] of a) {
|
|
120
|
+
const other = b.get(place);
|
|
81
121
|
if (other && typesEqual(type, other))
|
|
82
|
-
result.set(
|
|
122
|
+
result.set(place, type);
|
|
83
123
|
}
|
|
84
124
|
return result;
|
|
85
125
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type * as ast from '../ast.ts';
|
|
2
2
|
import { type BindingKind, Scope, type Flow } from './context.ts';
|
|
3
|
-
import {
|
|
3
|
+
import { TaggedUnionChecker } from './tagged-unions.ts';
|
|
4
4
|
import { type FunctionType, type Type } from './types.ts';
|
|
5
|
-
export declare abstract class FunctionChecker extends
|
|
5
|
+
export declare abstract class FunctionChecker extends TaggedUnionChecker {
|
|
6
6
|
/**
|
|
7
7
|
* Checks a function body and returns its result types: the declared ones, or those inferred from
|
|
8
8
|
* the `return` statements when `results` is `null`.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Scope } from "./context.js";
|
|
2
|
-
import { ControlFlowChecker } from "./control-flow.js";
|
|
3
2
|
import { isTerminating } from "./flow.js";
|
|
4
3
|
import { isUntyped, callSignature } from "./helpers.js";
|
|
5
4
|
import { awaitedResults, isPromise } from "./promise.js";
|
|
5
|
+
import { TaggedUnionChecker } from "./tagged-unions.js";
|
|
6
6
|
import { commonType, containsTypeParam, func, nonNull, UNKNOWN, } from "./types.js";
|
|
7
7
|
// Bodies of functions, arrow functions and func literals, and the results they return.
|
|
8
|
-
export class FunctionChecker extends
|
|
8
|
+
export class FunctionChecker extends TaggedUnionChecker {
|
|
9
9
|
/**
|
|
10
10
|
* Checks a function body and returns its result types: the declared ones, or those inferred from
|
|
11
11
|
* the `return` statements when `results` is `null`.
|
|
@@ -30,7 +30,9 @@ export class FunctionChecker extends ControlFlowChecker {
|
|
|
30
30
|
// A generic function: its type parameters are types in its body.
|
|
31
31
|
for (const typeParam of type.typeParams)
|
|
32
32
|
this.scope.types.set(typeParam.name, typeParam);
|
|
33
|
-
params.forEach((param, i) =>
|
|
33
|
+
params.forEach((param, i) =>
|
|
34
|
+
// A bound property of a component, `bind active string`, changes as its state does.
|
|
35
|
+
this.declareValue(param.name, param.bind ? 'state' : (options.paramKind ?? 'param'), type.params[i] ?? UNKNOWN));
|
|
34
36
|
if (body.kind === 'BlockStatement') {
|
|
35
37
|
this.checkStatementList(body.body, false);
|
|
36
38
|
options.inspect?.(this.scope);
|
|
@@ -87,9 +89,11 @@ export class FunctionChecker extends ControlFlowChecker {
|
|
|
87
89
|
}
|
|
88
90
|
stableFlow() {
|
|
89
91
|
const flow = new Map();
|
|
90
|
-
|
|
92
|
+
// A path keeps its narrowing when its variable does, like `user.boss` of a constant `user`.
|
|
93
|
+
for (const [place, type] of this.flow) {
|
|
94
|
+
const { binding } = place;
|
|
91
95
|
if (binding.kind === 'const' || !this.assigned.has(binding.name))
|
|
92
|
-
flow.set(
|
|
96
|
+
flow.set(place, type);
|
|
93
97
|
}
|
|
94
98
|
return flow;
|
|
95
99
|
}
|
|
@@ -11,7 +11,10 @@ export declare function ownStatements(body: ast.BlockStatement): ast.Statement[]
|
|
|
11
11
|
* the type of a DOM node), lists of them, or null.
|
|
12
12
|
*/
|
|
13
13
|
export declare function isContent(given: Type, node: Type): boolean;
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Values that setAttribute accepts: text, numbers, bools, or null to leave it out. A literal is
|
|
16
|
+
* one of them, and so is a union of such, `type Status "shown" | "aborted"`.
|
|
17
|
+
*/
|
|
15
18
|
export declare function isAttributeValue(type: Type): boolean;
|
|
16
19
|
export declare function isUntyped(type: Type): boolean;
|
|
17
20
|
export declare function isNumeric(type: Type): boolean;
|
package/dist/checker/helpers.js
CHANGED
|
@@ -61,7 +61,10 @@ export function isContent(given, node) {
|
|
|
61
61
|
return false;
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Values that setAttribute accepts: text, numbers, bools, or null to leave it out. A literal is
|
|
66
|
+
* one of them, and so is a union of such, `type Status "shown" | "aborted"`.
|
|
67
|
+
*/
|
|
65
68
|
export function isAttributeValue(type) {
|
|
66
69
|
const value = nonNull(type);
|
|
67
70
|
return (isUntyped(value) ||
|
|
@@ -69,8 +72,10 @@ export function isAttributeValue(type) {
|
|
|
69
72
|
value.kind === 'number' ||
|
|
70
73
|
value.kind === 'bigint' ||
|
|
71
74
|
value.kind === 'bool' ||
|
|
75
|
+
value.kind === 'literal' ||
|
|
72
76
|
value.kind === 'null' ||
|
|
73
|
-
value.kind === 'never'
|
|
77
|
+
value.kind === 'never' ||
|
|
78
|
+
(value.kind === 'union' && value.types.every(isAttributeValue)));
|
|
74
79
|
}
|
|
75
80
|
export function isUntyped(type) {
|
|
76
81
|
return type.kind === 'any' || type.kind === 'unknown';
|
package/dist/checker/imports.js
CHANGED
|
@@ -54,8 +54,11 @@ export class ImportChecker extends CheckerBase {
|
|
|
54
54
|
if (!found) {
|
|
55
55
|
this.error(`"${name}" is not exported by "${specifier}"`, specifierNode.imported);
|
|
56
56
|
}
|
|
57
|
-
if (value !== undefined)
|
|
58
|
-
this.declareValue(specifierNode.local, 'import', value);
|
|
57
|
+
if (value !== undefined) {
|
|
58
|
+
const binding = this.declareValue(specifierNode.local, 'import', value);
|
|
59
|
+
if (exports.shared?.has(name))
|
|
60
|
+
binding.shared = specifier;
|
|
61
|
+
}
|
|
59
62
|
if (type !== undefined)
|
|
60
63
|
this.declareType(specifierNode.local, type);
|
|
61
64
|
if (decorator !== undefined)
|
package/dist/checker/loops.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type * as ast from '../ast.ts';
|
|
|
2
2
|
import { BytesChecker } from './bytes.ts';
|
|
3
3
|
export declare abstract class LoopChecker extends BytesChecker {
|
|
4
4
|
protected checkForIn(node: ast.ForInStatement): void;
|
|
5
|
+
/** `with key todo.id`: a string or a number, by which the blocks of the list are found again. */
|
|
6
|
+
private checkListKey;
|
|
5
7
|
/**
|
|
6
8
|
* The key and the value of a loop over an iterator: what its `yield` takes. Of two, one variable
|
|
7
9
|
* gets the second, as with a Map; of one, there is no key.
|
package/dist/checker/loops.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { assignedNames } from "../names.js";
|
|
2
1
|
import { BytesChecker } from "./bytes.js";
|
|
3
2
|
import { chanDirection, elementOf, isChan } from "./channel.js";
|
|
4
3
|
import { asyncIterated, isMap, loopEntry } from "./iterables.js";
|
|
@@ -78,13 +77,22 @@ export class LoopChecker extends BytesChecker {
|
|
|
78
77
|
if (node.key)
|
|
79
78
|
this.declareValue(node.key, 'loop', key);
|
|
80
79
|
this.declareValue(node.value, 'loop', value);
|
|
81
|
-
|
|
80
|
+
if (node.withKey)
|
|
81
|
+
this.checkListKey(node.withKey);
|
|
82
|
+
this.dropNarrowing(node.body);
|
|
82
83
|
const entry = this.flow;
|
|
83
84
|
this.flow = new Map(entry);
|
|
84
85
|
this.checkBlock(node.body.body);
|
|
85
86
|
this.flow = entry;
|
|
86
87
|
});
|
|
87
88
|
}
|
|
89
|
+
/** `with key todo.id`: a string or a number, by which the blocks of the list are found again. */
|
|
90
|
+
checkListKey(key) {
|
|
91
|
+
const type = this.checkValue(key);
|
|
92
|
+
if (!isListKey(type)) {
|
|
93
|
+
this.error(`the key of a list must be a string or a number, not ${typeToString(type)}: e.g. "with key todo.id"`, key);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
88
96
|
/**
|
|
89
97
|
* The key and the value of a loop over an iterator: what its `yield` takes. Of two, one variable
|
|
90
98
|
* gets the second, as with a Map; of one, there is no key.
|
|
@@ -100,3 +108,20 @@ export class LoopChecker extends BytesChecker {
|
|
|
100
108
|
return [UNKNOWN, taken[0] ?? UNKNOWN];
|
|
101
109
|
}
|
|
102
110
|
}
|
|
111
|
+
/** A key of a list: a string, a number, a literal of one or a union of them. */
|
|
112
|
+
function isListKey(type) {
|
|
113
|
+
switch (type.kind) {
|
|
114
|
+
case 'string':
|
|
115
|
+
case 'number':
|
|
116
|
+
case 'bigint':
|
|
117
|
+
case 'any':
|
|
118
|
+
case 'unknown':
|
|
119
|
+
return true;
|
|
120
|
+
case 'literal':
|
|
121
|
+
return typeof type.value !== 'boolean';
|
|
122
|
+
case 'union':
|
|
123
|
+
return type.types.every(isListKey);
|
|
124
|
+
default:
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
package/dist/checker/markup.js
CHANGED
|
@@ -4,8 +4,10 @@ import { domProperty, elementType } from "./dom.js";
|
|
|
4
4
|
import { svgElementType } from "./dom-svg.js";
|
|
5
5
|
import { isContent, isAttributeValue, isUntyped } from "./helpers.js";
|
|
6
6
|
import { OperatorChecker } from "./operators.js";
|
|
7
|
-
import { BOOL, func, STRING, typeToString } from "./types.js";
|
|
7
|
+
import { arrayOf, BOOL, func, nullable, STRING, typeToString } from "./types.js";
|
|
8
8
|
// Markup: elements, their children and attributes, `bind:` and event handlers.
|
|
9
|
+
/** The type of a list of classes, `class={[styles.tab, active ? styles.active : null]}`. */
|
|
10
|
+
const CLASS_LIST = arrayOf(nullable(STRING));
|
|
9
11
|
export class MarkupChecker extends OperatorChecker {
|
|
10
12
|
/** Whether the markup being checked is inside `<svg>`: its elements are SVG then. */
|
|
11
13
|
inSvg = false;
|
|
@@ -98,7 +100,17 @@ export class MarkupChecker extends OperatorChecker {
|
|
|
98
100
|
}
|
|
99
101
|
if (value?.kind === 'EventHandler')
|
|
100
102
|
return;
|
|
101
|
-
const
|
|
103
|
+
const list = name === 'class' && value?.kind === 'ArrayLiteral' ? CLASS_LIST : null;
|
|
104
|
+
const valueType = value === null
|
|
105
|
+
? BOOL
|
|
106
|
+
: value.kind === 'StringLiteral'
|
|
107
|
+
? STRING
|
|
108
|
+
: this.checkValue(value, list);
|
|
109
|
+
// `class={[styles.tab, active ? styles.active : null]}`: the classes that are not null.
|
|
110
|
+
if (name === 'class' && valueType.kind === 'array') {
|
|
111
|
+
this.expectAssignable(valueType, CLASS_LIST, value, ' for attribute "class"');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
102
114
|
if (name === 'style') {
|
|
103
115
|
if (!isUntyped(valueType) && valueType.kind !== 'string' && valueType.kind !== 'object') {
|
|
104
116
|
this.error(`style must be a string or an object, not ${typeToString(valueType)}`, attribute);
|
package/dist/checker/members.js
CHANGED
|
@@ -16,6 +16,7 @@ export class MemberChecker extends DecoratorMemberChecker {
|
|
|
16
16
|
return [this.checkSuperCall(node), false];
|
|
17
17
|
let [callee, shortCircuits] = this.chainPart(node.callee);
|
|
18
18
|
this.checkDecoratorMutation(node);
|
|
19
|
+
this.checkSharedMutation(node);
|
|
19
20
|
if (isNullable(callee)) {
|
|
20
21
|
if (node.optional)
|
|
21
22
|
shortCircuits = true;
|
|
@@ -46,7 +47,8 @@ export class MemberChecker extends DecoratorMemberChecker {
|
|
|
46
47
|
else if (member !== 'any' && member?.abstract && node.object.kind === 'SuperExpression') {
|
|
47
48
|
this.error(`super.${name} is abstract in ${member.owner?.name ?? 'the base class'}: it has no code to run`, node.property);
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
+
const type = member === 'any' ? ANY : (member?.type ?? UNKNOWN);
|
|
51
|
+
return [this.pathType(node, object, type), shortCircuits];
|
|
50
52
|
}
|
|
51
53
|
const keyed = this.keyedIndex(object, node.index);
|
|
52
54
|
if (keyed)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type * as ast from '../ast.ts';
|
|
2
|
+
import { ImportChecker } from './imports.ts';
|
|
3
|
+
export declare abstract class SharedStateChecker extends ImportChecker {
|
|
4
|
+
/** `items = []` or `items[0].done = true` in a module that imported `items`. */
|
|
5
|
+
protected checkSharedWrite(target: ast.Expression): boolean;
|
|
6
|
+
/** `items.push(x)`: a method that changes state that another module shares. */
|
|
7
|
+
protected checkSharedMutation(node: ast.CallExpression): void;
|
|
8
|
+
/** The import that a target writes into, `items` of `items[0].done`, if it is shared state. */
|
|
9
|
+
private importedShared;
|
|
10
|
+
private sharedError;
|
|
11
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { mayChange } from "../methods.js";
|
|
2
|
+
import { ImportChecker } from "./imports.js";
|
|
3
|
+
// State shared by components: `state items []Item = []` at the top level of a module. Its module
|
|
4
|
+
// changes it anywhere; other modules import it to read, and change it only by calling a function
|
|
5
|
+
// of its module, so that the module keeps its data consistent, as a decorator keeps its state.
|
|
6
|
+
export class SharedStateChecker extends ImportChecker {
|
|
7
|
+
/** `items = []` or `items[0].done = true` in a module that imported `items`. */
|
|
8
|
+
checkSharedWrite(target) {
|
|
9
|
+
const shared = this.importedShared(target);
|
|
10
|
+
if (shared)
|
|
11
|
+
this.sharedError(shared, target);
|
|
12
|
+
return shared !== null;
|
|
13
|
+
}
|
|
14
|
+
/** `items.push(x)`: a method that changes state that another module shares. */
|
|
15
|
+
checkSharedMutation(node) {
|
|
16
|
+
const { callee } = node;
|
|
17
|
+
if (callee.kind !== 'MemberExpression')
|
|
18
|
+
return;
|
|
19
|
+
const shared = this.importedShared(callee.object);
|
|
20
|
+
if (shared && mayChange(this.typeOfChecked(callee.object), callee.property.name)) {
|
|
21
|
+
this.sharedError(shared, node);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** The import that a target writes into, `items` of `items[0].done`, if it is shared state. */
|
|
25
|
+
importedShared(target) {
|
|
26
|
+
let root = target;
|
|
27
|
+
while (root.kind === 'MemberExpression' || root.kind === 'IndexExpression')
|
|
28
|
+
root = root.object;
|
|
29
|
+
if (root.kind !== 'Identifier')
|
|
30
|
+
return null;
|
|
31
|
+
const binding = this.lookupValue(root.name);
|
|
32
|
+
return binding?.shared ? binding : null;
|
|
33
|
+
}
|
|
34
|
+
sharedError(binding, node) {
|
|
35
|
+
this.error(`"${binding.name}" is state of "${binding.shared}", which is read-only here; change it with a function that "${binding.shared}" exports`, node);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -12,10 +12,11 @@ export declare abstract class StatementChecker extends AssignmentChecker {
|
|
|
12
12
|
*/
|
|
13
13
|
protected checkEffect(node: ast.EffectStatement): void;
|
|
14
14
|
/**
|
|
15
|
-
* `mount
|
|
16
|
-
*
|
|
15
|
+
* `mount { ... }`: a function that runs once the markup is in the document, like a handler. It
|
|
16
|
+
* cleans up with `defer`, as an effect does, so it returns nothing.
|
|
17
17
|
*/
|
|
18
18
|
protected checkMount(node: ast.MountStatement): void;
|
|
19
|
+
private checkDeferring;
|
|
19
20
|
/**
|
|
20
21
|
* `delete scores[name]` removes a key of an object used as a map, `{ [string]: T }`, or a field
|
|
21
22
|
* that may be missing, of type `?T`; values of type `any` are not checked.
|
|
@@ -30,4 +31,6 @@ export declare abstract class StatementChecker extends AssignmentChecker {
|
|
|
30
31
|
private linkError;
|
|
31
32
|
/** The types of the variables of a declaration: the written one, or those of their values. */
|
|
32
33
|
private initializerTypes;
|
|
34
|
+
/** `derived total number { ... }`: the block runs like the body of a closure and returns the value. */
|
|
35
|
+
private derivedBlockType;
|
|
33
36
|
}
|