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,10 +1,11 @@
|
|
|
1
|
-
import { assignedNames } from "../names.js";
|
|
2
|
-
import { NARROWABLE } from "./context.js";
|
|
3
|
-
import { narrowByTypeof, literalValue, withoutLiteral, withNarrowing, mergeFlows, leaves, } from "./flow.js";
|
|
1
|
+
import { assignedNames, assignedPaths, pathOf } from "../names.js";
|
|
2
|
+
import { NARROWABLE, placeOf } from "./context.js";
|
|
3
|
+
import { narrowByTypeof, literalValue, withoutLiteral, withNarrowing, mergeFlows, leaves, forgetWritten, withoutAssigned, } from "./flow.js";
|
|
4
4
|
import { instanceTypeOf } from "./helpers.js";
|
|
5
5
|
import { StatementChecker } from "./statements.js";
|
|
6
6
|
import { ANY, BOOL, isAssignable, isComparable, isNullable, isSubclass, literal, nonNull, NULL, typeToString, union, unionMembers, widenLiterals, } from "./types.js";
|
|
7
|
-
// if, for, switch and try, and how conditions narrow the types of variables in the
|
|
7
|
+
// if, for, switch and try, and how conditions narrow the types of variables and paths in the
|
|
8
|
+
// branches.
|
|
8
9
|
export class ControlFlowChecker extends StatementChecker {
|
|
9
10
|
checkIf(node) {
|
|
10
11
|
if (this.checkWithInit(node.init, () => this.checkIf({ ...node, init: null })))
|
|
@@ -39,8 +40,8 @@ export class ControlFlowChecker extends StatementChecker {
|
|
|
39
40
|
this.withScope(() => {
|
|
40
41
|
if (node.init)
|
|
41
42
|
this.checkSimpleStatement(node.init);
|
|
42
|
-
// Variables assigned in the loop may change between iterations.
|
|
43
|
-
this.dropNarrowing(
|
|
43
|
+
// Variables and paths assigned in the loop may change between iterations.
|
|
44
|
+
this.dropNarrowing(node);
|
|
44
45
|
const entry = this.flow;
|
|
45
46
|
if (node.condition)
|
|
46
47
|
this.checkCondition(node.condition);
|
|
@@ -71,7 +72,7 @@ export class ControlFlowChecker extends StatementChecker {
|
|
|
71
72
|
if (this.checkWithInit(node.init, () => this.checkSwitch({ ...node, init: null })))
|
|
72
73
|
return;
|
|
73
74
|
const discriminant = node.discriminant ? this.checkValue(node.discriminant) : null;
|
|
74
|
-
this.dropNarrowing(
|
|
75
|
+
this.dropNarrowing(node);
|
|
75
76
|
const entry = this.flow;
|
|
76
77
|
for (const switchCase of node.cases) {
|
|
77
78
|
this.flow = entry;
|
|
@@ -89,7 +90,7 @@ export class ControlFlowChecker extends StatementChecker {
|
|
|
89
90
|
? switchCase.tests.length === 1
|
|
90
91
|
? this.narrow(switchCase.tests[0], true)
|
|
91
92
|
: []
|
|
92
|
-
: this.narrowCase(node, switchCase
|
|
93
|
+
: this.narrowCase(node, switchCase);
|
|
93
94
|
this.flow = withNarrowing(entry, narrowing);
|
|
94
95
|
this.checkBlock(switchCase.body);
|
|
95
96
|
}
|
|
@@ -99,18 +100,18 @@ export class ControlFlowChecker extends StatementChecker {
|
|
|
99
100
|
* `switch filter { case "all": ... default: ... }`: in a case the discriminant has the literals
|
|
100
101
|
* of its tests, in `default` the literals of no case.
|
|
101
102
|
*/
|
|
102
|
-
narrowCase(node, switchCase
|
|
103
|
-
const
|
|
104
|
-
if (!
|
|
103
|
+
narrowCase(node, switchCase) {
|
|
104
|
+
const target = node.discriminant ? this.narrowable(node.discriminant) : undefined;
|
|
105
|
+
if (!target)
|
|
105
106
|
return [];
|
|
106
|
-
const current =
|
|
107
|
+
const { place, type: current } = target;
|
|
107
108
|
if (!unionMembers(nonNull(current)).some((member) => member.kind === 'literal'))
|
|
108
109
|
return [];
|
|
109
110
|
if (switchCase.tests.length > 0) {
|
|
110
111
|
const values = switchCase.tests.map(literalValue);
|
|
111
112
|
if (values.some((value) => value === undefined))
|
|
112
113
|
return [];
|
|
113
|
-
return [[
|
|
114
|
+
return [[place, union(values.map((value) => literal(value)))]];
|
|
114
115
|
}
|
|
115
116
|
let rest = current;
|
|
116
117
|
for (const other of node.cases) {
|
|
@@ -120,10 +121,10 @@ export class ControlFlowChecker extends StatementChecker {
|
|
|
120
121
|
rest = withoutLiteral(rest, value);
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
|
-
return [[
|
|
124
|
+
return [[place, rest]];
|
|
124
125
|
}
|
|
125
126
|
checkTry(node) {
|
|
126
|
-
this.dropNarrowing(
|
|
127
|
+
this.dropNarrowing(node);
|
|
127
128
|
const entry = this.flow;
|
|
128
129
|
this.flow = new Map(entry);
|
|
129
130
|
this.checkBlock(node.block.body);
|
|
@@ -157,7 +158,7 @@ export class ControlFlowChecker extends StatementChecker {
|
|
|
157
158
|
: '';
|
|
158
159
|
this.error(`condition must be bool, not ${typeToString(type)}${hint}`, node);
|
|
159
160
|
}
|
|
160
|
-
/** Variables whose type is narrowed when `node` evaluates to `assumeTrue`. */
|
|
161
|
+
/** Variables and paths whose type is narrowed when `node` evaluates to `assumeTrue`. */
|
|
161
162
|
narrow(node, assumeTrue) {
|
|
162
163
|
if (node.kind === 'UnaryExpression' && node.operator === '!') {
|
|
163
164
|
return this.narrow(node.argument, !assumeTrue);
|
|
@@ -183,17 +184,21 @@ export class ControlFlowChecker extends StatementChecker {
|
|
|
183
184
|
// `x != null` when true, `x == null` when false: `x` is not null.
|
|
184
185
|
if ((operator === '!=' && assumeTrue) || (operator === '==' && !assumeTrue)) {
|
|
185
186
|
const target = right.kind === 'NullLiteral' ? left : left.kind === 'NullLiteral' ? right : null;
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
188
|
-
return [[
|
|
187
|
+
const found = target ? this.narrowable(target) : undefined;
|
|
188
|
+
if (found)
|
|
189
|
+
return [[found.place, nonNull(found.type)]];
|
|
189
190
|
}
|
|
190
191
|
// `err == null` when true, `err != null` when false: the values returned with `err` are not
|
|
191
192
|
// null (error-results.ts).
|
|
192
193
|
if ((operator === '==' && assumeTrue) || (operator === '!=' && !assumeTrue)) {
|
|
193
194
|
const target = right.kind === 'NullLiteral' ? left : left.kind === 'NullLiteral' ? right : null;
|
|
194
195
|
const guards = target ? this.narrowableBinding(target)?.guards : undefined;
|
|
195
|
-
if (guards)
|
|
196
|
-
return guards.map((value) =>
|
|
196
|
+
if (guards) {
|
|
197
|
+
return guards.map((value) => {
|
|
198
|
+
const place = placeOf(value);
|
|
199
|
+
return [place, nonNull(this.flow.get(place) ?? value.type)];
|
|
200
|
+
});
|
|
201
|
+
}
|
|
197
202
|
}
|
|
198
203
|
if (operator === 'instanceof')
|
|
199
204
|
return this.narrowInstanceof(left, right, assumeTrue);
|
|
@@ -208,41 +213,41 @@ export class ControlFlowChecker extends StatementChecker {
|
|
|
208
213
|
? [right, left]
|
|
209
214
|
: [null, null];
|
|
210
215
|
if (check && tag?.kind === 'StringLiteral') {
|
|
211
|
-
const
|
|
212
|
-
if (!
|
|
216
|
+
const found = this.narrowable(check.argument);
|
|
217
|
+
if (!found)
|
|
213
218
|
return [];
|
|
214
|
-
const type = narrowByTypeof(
|
|
215
|
-
return type ? [[
|
|
219
|
+
const type = narrowByTypeof(found.type, tag.value, equal);
|
|
220
|
+
return type ? [[found.place, type]] : [];
|
|
216
221
|
}
|
|
217
222
|
// `filter == "all"`: narrows a type with literals in it.
|
|
218
223
|
const value = literalValue(right) ?? literalValue(left);
|
|
219
224
|
const target = literalValue(right) !== undefined ? left : right;
|
|
220
|
-
const
|
|
221
|
-
if (value === undefined || !
|
|
225
|
+
const found = value === undefined ? undefined : this.narrowable(target);
|
|
226
|
+
if (value === undefined || !found)
|
|
222
227
|
return [];
|
|
223
|
-
const current =
|
|
228
|
+
const { place, type: current } = found;
|
|
224
229
|
if (!unionMembers(nonNull(current)).some((member) => member.kind === 'literal'))
|
|
225
230
|
return [];
|
|
226
|
-
return [[
|
|
231
|
+
return [[place, equal ? literal(value) : withoutLiteral(current, value)]];
|
|
227
232
|
}
|
|
228
233
|
/** `x instanceof Date`: an instance of the class when true, the rest of a union when false. */
|
|
229
234
|
narrowInstanceof(left, right, assumeTrue) {
|
|
230
|
-
const
|
|
235
|
+
const found = this.narrowable(left);
|
|
231
236
|
const classType = this.typeOfChecked(right);
|
|
232
237
|
const instance = instanceTypeOf(classType);
|
|
233
|
-
if (!
|
|
238
|
+
if (!found || !instance)
|
|
234
239
|
return [];
|
|
235
|
-
const current =
|
|
240
|
+
const { place, type: current } = found;
|
|
236
241
|
const members = unionMembers(nonNull(current));
|
|
237
242
|
if (assumeTrue) {
|
|
238
243
|
const kept = members.filter((member) => isAssignable(member, instance));
|
|
239
|
-
return [[
|
|
244
|
+
return [[place, kept.length > 0 ? union(kept) : instance]];
|
|
240
245
|
}
|
|
241
246
|
// A class keeps what is not its subclass; a DOM interface what is not assignable to it.
|
|
242
247
|
const kept = members.filter((member) => classType.kind === 'classValue'
|
|
243
248
|
? !(member.kind === 'class' && isSubclass(member.info, classType.info))
|
|
244
249
|
: !isAssignable(member, instance));
|
|
245
|
-
return [[
|
|
250
|
+
return [[place, union(isNullable(current) ? [...kept, NULL] : kept)]];
|
|
246
251
|
}
|
|
247
252
|
nullError(node) {
|
|
248
253
|
const name = node.kind === 'Identifier' ? node.name : null;
|
|
@@ -259,13 +264,16 @@ export class ControlFlowChecker extends StatementChecker {
|
|
|
259
264
|
const binding = this.lookupValue(node.name);
|
|
260
265
|
return binding && NARROWABLE.has(binding.kind) ? binding : undefined;
|
|
261
266
|
}
|
|
262
|
-
/** An assignment to a variable ends its narrowing. */
|
|
267
|
+
/** An assignment to a variable ends its narrowing; one to a path, that of the paths it changes. */
|
|
263
268
|
forget(target) {
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
269
|
+
const path = pathOf(target);
|
|
270
|
+
const binding = path ? this.lookupValue(path[0]) : undefined;
|
|
271
|
+
if (path && binding && NARROWABLE.has(binding.kind)) {
|
|
272
|
+
forgetWritten(this.flow, binding, path.slice(1).join('.'), (place, field) => this.isTagOf(place, field));
|
|
273
|
+
}
|
|
267
274
|
}
|
|
268
|
-
|
|
269
|
-
|
|
275
|
+
/** Ends the narrowing of what `node` assigns: before a loop, or a statement with several ways. */
|
|
276
|
+
dropNarrowing(node) {
|
|
277
|
+
this.flow = withoutAssigned(this.flow, assignedNames(node), assignedPaths(node), (place, field) => this.isTagOf(place, field));
|
|
270
278
|
}
|
|
271
279
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type * as ast from '../ast.ts';
|
|
2
|
-
import {
|
|
2
|
+
import { SharedStateChecker } from './shared-state.ts';
|
|
3
3
|
import { type FunctionType, type ObjectType, type Type, type TypeParam } from './types.ts';
|
|
4
|
-
export declare abstract class DeclarationChecker extends
|
|
4
|
+
export declare abstract class DeclarationChecker extends SharedStateChecker {
|
|
5
5
|
/**
|
|
6
6
|
* Declares the names of a statement list first (so functions, classes and types can be used
|
|
7
7
|
* before their declaration), then checks the statements in order. Function and method bodies are
|
|
@@ -3,12 +3,12 @@ import { isIterator } from "../iterators.js";
|
|
|
3
3
|
import { collectValueNames } from "../names.js";
|
|
4
4
|
import { recursiveComponents } from "../recursion.js";
|
|
5
5
|
import { Scope } from "./context.js";
|
|
6
|
-
import {
|
|
6
|
+
import { SharedStateChecker } from "./shared-state.js";
|
|
7
7
|
import { awaited, isPromise, promiseOf } from "./promise.js";
|
|
8
8
|
import { createClass, func, typeToString, UNKNOWN, } from "./types.js";
|
|
9
9
|
// Declarations of a list of statements: imports, types, functions, components, decorators and
|
|
10
10
|
// classes, declared before the statements are checked, so that they can refer to each other.
|
|
11
|
-
export class DeclarationChecker extends
|
|
11
|
+
export class DeclarationChecker extends SharedStateChecker {
|
|
12
12
|
/**
|
|
13
13
|
* Declares the names of a statement list first (so functions, classes and types can be used
|
|
14
14
|
* before their declaration), then checks the statements in order. Function and method bodies are
|
|
@@ -95,6 +95,7 @@ export class DeclarationChecker extends ImportChecker {
|
|
|
95
95
|
freeNames: null,
|
|
96
96
|
wrapper: null,
|
|
97
97
|
needed: [],
|
|
98
|
+
included: [],
|
|
98
99
|
moduleNames: [],
|
|
99
100
|
expressionTypes: null,
|
|
100
101
|
inline: null,
|
|
@@ -20,6 +20,10 @@ export class DecoratorExportChecker extends DecoratorWrapperChecker {
|
|
|
20
20
|
// Other modules can read the variables of this one, but not assign them.
|
|
21
21
|
const assigned = assignedNames(node.body);
|
|
22
22
|
for (const used of info.moduleNames) {
|
|
23
|
+
if (this.moduleScope.values.get(used)?.kind === 'state') {
|
|
24
|
+
this.error(`@${name} is exported, so its code cannot read "${used}", state of its module, yet: read it in a component of the module and pass it to @${name} as an argument`, node.name);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
23
27
|
if (!assigned.has(used))
|
|
24
28
|
continue;
|
|
25
29
|
this.error(`@${name} is exported, so it cannot change "${used}" of its module: other modules can only read it; change it in a function of the module`, node.name);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DecoratorInfo } from './context.ts';
|
|
2
|
+
import { DecoratorChecker } from './decorators.ts';
|
|
3
|
+
export declare abstract class DecoratorIncludeChecker extends DecoratorChecker {
|
|
4
|
+
protected resolveDecoratorParams(info: DecoratorInfo): void;
|
|
5
|
+
/** The decorators it includes are checked before it: see orderByNeeds. */
|
|
6
|
+
protected checkDecoratorBody(info: DecoratorInfo): void;
|
|
7
|
+
/** The public members of the decorators it includes become its own; each name comes once. */
|
|
8
|
+
private addIncludedMembers;
|
|
9
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { DecoratorChecker } from "./decorators.js";
|
|
2
|
+
import { UNKNOWN } from "./types.js";
|
|
3
|
+
// Decorators that include others, `dec shop(margin string) includes visible(margin), cart { ... }`:
|
|
4
|
+
// those are applied with it, above its own body, their arguments see its parameters, and their
|
|
5
|
+
// public members are its own for the component, `@shop.count`.
|
|
6
|
+
export class DecoratorIncludeChecker extends DecoratorChecker {
|
|
7
|
+
resolveDecoratorParams(info) {
|
|
8
|
+
super.resolveDecoratorParams(info);
|
|
9
|
+
const { node } = info;
|
|
10
|
+
const current = node.name.name;
|
|
11
|
+
const included = new Set();
|
|
12
|
+
for (const use of node.includes) {
|
|
13
|
+
const name = use.name.name;
|
|
14
|
+
const other = this.decorators.get(name);
|
|
15
|
+
if (name === current)
|
|
16
|
+
this.error(`@${name} cannot include itself`, use.name);
|
|
17
|
+
else if (included.has(name))
|
|
18
|
+
this.error(`@${name} is included twice: name it once`, use.name);
|
|
19
|
+
else if (node.needs.some((need) => need.name === name)) {
|
|
20
|
+
this.error(`@${current} both needs and includes @${name}: includes is enough`, use.name);
|
|
21
|
+
}
|
|
22
|
+
else if (!other)
|
|
23
|
+
this.error(`unknown decorator @${name}`, use.name);
|
|
24
|
+
else
|
|
25
|
+
info.included.push(other);
|
|
26
|
+
included.add(name);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** The decorators it includes are checked before it: see orderByNeeds. */
|
|
30
|
+
checkDecoratorBody(info) {
|
|
31
|
+
super.checkDecoratorBody(info);
|
|
32
|
+
const { node } = info;
|
|
33
|
+
const params = node.params.map((param, i) => [
|
|
34
|
+
param.name,
|
|
35
|
+
info.params[i] ?? UNKNOWN,
|
|
36
|
+
]);
|
|
37
|
+
for (const use of node.includes) {
|
|
38
|
+
const included = this.decorators.get(use.name.name);
|
|
39
|
+
if (included)
|
|
40
|
+
this.checkDecoratorArguments(use, included, params);
|
|
41
|
+
}
|
|
42
|
+
this.addIncludedMembers(info);
|
|
43
|
+
// A module that applies it would need the code of those it includes too.
|
|
44
|
+
if (node.exported && node.includes.length > 0) {
|
|
45
|
+
const names = node.includes.map((use) => `@${use.name.name}`).join(' ');
|
|
46
|
+
this.error(`@${node.name.name} is exported, so it cannot include decorators yet: apply them before it, e.g. ${names} @${node.name.name} comp ...`, node.includes[0]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** The public members of the decorators it includes become its own; each name comes once. */
|
|
50
|
+
addIncludedMembers(info) {
|
|
51
|
+
const current = info.node.name.name;
|
|
52
|
+
const from = new Map();
|
|
53
|
+
for (const included of info.included) {
|
|
54
|
+
const name = included.node.name.name;
|
|
55
|
+
const use = info.node.includes.find((each) => each.name.name === name);
|
|
56
|
+
for (const [member, found] of included.members) {
|
|
57
|
+
const other = from.get(member);
|
|
58
|
+
if (other !== undefined) {
|
|
59
|
+
this.error(`@${current} gets "${member}" from both @${other} and @${name}: rename one of them`, use);
|
|
60
|
+
}
|
|
61
|
+
else if (info.members.has(member)) {
|
|
62
|
+
this.error(`@${current} declares "${member}", which it also gets from @${name}: rename one of them`, use);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
info.members.set(member, found);
|
|
66
|
+
from.set(member, name);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
import type * as ast from '../ast.ts';
|
|
2
|
-
import type
|
|
3
|
-
import {
|
|
2
|
+
import { type DecoratorInfo, type PublicMember } from './context.ts';
|
|
3
|
+
import { DecoratorIncludeChecker } from './decorator-includes.ts';
|
|
4
4
|
import { type Type } from './types.ts';
|
|
5
|
-
export declare abstract class DecoratorMemberChecker extends
|
|
6
|
-
/** `@visible.show`:
|
|
5
|
+
export declare abstract class DecoratorMemberChecker extends DecoratorIncludeChecker {
|
|
6
|
+
/** `@visible.shown` or `@visible.show`: public state, a constant or a function. */
|
|
7
7
|
protected checkDecoratorMember(node: ast.DecoratorMember): Type;
|
|
8
|
-
/** `get(@visible.shown)`: public state or a constant. */
|
|
9
|
-
protected checkDecoratorGet(node: ast.DecoratorGet): Type;
|
|
10
8
|
/**
|
|
11
9
|
* A public member of a decorator applied to the component being checked, or of one that the
|
|
12
10
|
* decorator being checked needs.
|
|
13
11
|
*/
|
|
14
12
|
protected findDecoratorMember(node: ast.DecoratorMember): PublicMember | null;
|
|
15
|
-
/**
|
|
13
|
+
/** `@cart.count` in `dec cartBadge() needs cart`. */
|
|
16
14
|
protected neededMember(node: ast.DecoratorMember, decorator: ast.DecoratorDeclaration): PublicMember | null;
|
|
17
15
|
protected publicMember(node: ast.DecoratorMember, info: DecoratorInfo): PublicMember | null;
|
|
18
16
|
/**
|
|
19
|
-
* Members of a decorator are read-only outside it:
|
|
20
|
-
*
|
|
17
|
+
* Members of a decorator are read-only outside it: `@cart.count = 0`, `@cart.items[0] = x`.
|
|
18
|
+
* Returns whether `target` is one of them.
|
|
21
19
|
*/
|
|
22
20
|
protected checkDecoratorWrite(target: ast.Expression): boolean;
|
|
23
|
-
/**
|
|
21
|
+
/** `@cart.items.push(x)`: a method that changes the array. */
|
|
24
22
|
protected checkDecoratorMutation(node: ast.CallExpression): void;
|
|
25
23
|
protected readOnlyError(member: ast.DecoratorMember, node: ast.NodeBase): void;
|
|
26
24
|
/** `card.count` without `@`: `card` is not a value, but a decorator may have that name. */
|
|
@@ -1,27 +1,14 @@
|
|
|
1
1
|
import { ARRAY_MUTATORS } from "../methods.js";
|
|
2
|
-
import {
|
|
2
|
+
import { withIncluded } from "./context.js";
|
|
3
|
+
import { DecoratorIncludeChecker } from "./decorator-includes.js";
|
|
3
4
|
import { nonNull, UNKNOWN } from "./types.js";
|
|
4
5
|
// The public members of decorators, in the components they are applied to: `@visible.show()` calls
|
|
5
|
-
// a function,
|
|
6
|
-
//
|
|
7
|
-
export class DecoratorMemberChecker extends
|
|
8
|
-
/** `@visible.show`:
|
|
6
|
+
// a function, `@visible.shown` reads state or a constant, and none of them can be changed outside
|
|
7
|
+
// the decorator.
|
|
8
|
+
export class DecoratorMemberChecker extends DecoratorIncludeChecker {
|
|
9
|
+
/** `@visible.shown` or `@visible.show`: public state, a constant or a function. */
|
|
9
10
|
checkDecoratorMember(node) {
|
|
10
|
-
|
|
11
|
-
if (member && member.kind !== 'func') {
|
|
12
|
-
const { decorator, member: name } = node;
|
|
13
|
-
this.error(`read "${name.name}" of @${decorator.name} with get(@${decorator.name}.${name.name})`, node);
|
|
14
|
-
}
|
|
15
|
-
return member?.type ?? UNKNOWN;
|
|
16
|
-
}
|
|
17
|
-
/** `get(@visible.shown)`: public state or a constant. */
|
|
18
|
-
checkDecoratorGet(node) {
|
|
19
|
-
const member = this.findDecoratorMember(node.target);
|
|
20
|
-
if (member?.kind === 'func') {
|
|
21
|
-
const { decorator, member: name } = node.target;
|
|
22
|
-
this.error(`"${name.name}" of @${decorator.name} is a function: call it, @${decorator.name}.${name.name}()`, node);
|
|
23
|
-
}
|
|
24
|
-
return member?.type ?? UNKNOWN;
|
|
11
|
+
return this.findDecoratorMember(node)?.type ?? UNKNOWN;
|
|
25
12
|
}
|
|
26
13
|
/**
|
|
27
14
|
* A public member of a decorator applied to the component being checked, or of one that the
|
|
@@ -38,6 +25,13 @@ export class DecoratorMemberChecker extends DecoratorChecker {
|
|
|
38
25
|
return null;
|
|
39
26
|
}
|
|
40
27
|
const info = component.decorators.find((decorator) => decorator.node.name.name === name);
|
|
28
|
+
// `@cart.count` of the `cart` that `@shop` includes is `@shop.count`.
|
|
29
|
+
const via = component.decorators.find((decorator) => withIncluded(decorator.included).some((each) => each.node.name.name === name));
|
|
30
|
+
if (!info && via) {
|
|
31
|
+
const combined = via.node.name.name;
|
|
32
|
+
this.error(`"${member}" of @${name} comes through @${combined}: write @${combined}.${member}`, node);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
41
35
|
if (!info) {
|
|
42
36
|
this.error(this.decorators.has(name)
|
|
43
37
|
? `@${name} is not applied to ${component.node.name.name}: write @${name} before comp`
|
|
@@ -46,7 +40,7 @@ export class DecoratorMemberChecker extends DecoratorChecker {
|
|
|
46
40
|
}
|
|
47
41
|
return this.publicMember(node, info);
|
|
48
42
|
}
|
|
49
|
-
/**
|
|
43
|
+
/** `@cart.count` in `dec cartBadge() needs cart`. */
|
|
50
44
|
neededMember(node, decorator) {
|
|
51
45
|
const name = node.decorator.name;
|
|
52
46
|
const current = decorator.name.name;
|
|
@@ -54,8 +48,9 @@ export class DecoratorMemberChecker extends DecoratorChecker {
|
|
|
54
48
|
this.error(`inside @${name} its members are used by their names: ${node.member.name}`, node);
|
|
55
49
|
return null;
|
|
56
50
|
}
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
const uses = [...decorator.needs, ...decorator.includes.map((use) => use.name)];
|
|
52
|
+
if (!uses.some((used) => used.name === name)) {
|
|
53
|
+
this.error(`@${current} can use @${name} only if it needs or includes it: write "needs ${name}" after its parameters`, node.decorator);
|
|
59
54
|
return null;
|
|
60
55
|
}
|
|
61
56
|
const info = this.decorators.get(name);
|
|
@@ -74,18 +69,22 @@ export class DecoratorMemberChecker extends DecoratorChecker {
|
|
|
74
69
|
return found ?? null;
|
|
75
70
|
}
|
|
76
71
|
/**
|
|
77
|
-
* Members of a decorator are read-only outside it:
|
|
78
|
-
*
|
|
72
|
+
* Members of a decorator are read-only outside it: `@cart.count = 0`, `@cart.items[0] = x`.
|
|
73
|
+
* Returns whether `target` is one of them.
|
|
79
74
|
*/
|
|
80
75
|
checkDecoratorWrite(target) {
|
|
81
76
|
const root = decoratorRoot(target);
|
|
82
77
|
if (!root)
|
|
83
78
|
return false;
|
|
84
|
-
|
|
79
|
+
const member = this.findDecoratorMember(root);
|
|
80
|
+
// A ref always points to its element, but the element is the page's: `@framed.box.title = x`.
|
|
81
|
+
if (member?.kind === 'ref' && target !== root)
|
|
82
|
+
return false;
|
|
83
|
+
if (member)
|
|
85
84
|
this.readOnlyError(root, target);
|
|
86
85
|
return true;
|
|
87
86
|
}
|
|
88
|
-
/**
|
|
87
|
+
/** `@cart.items.push(x)`: a method that changes the array. */
|
|
89
88
|
checkDecoratorMutation(node) {
|
|
90
89
|
const { callee } = node;
|
|
91
90
|
if (callee.kind !== 'MemberExpression')
|
|
@@ -118,17 +117,15 @@ export class DecoratorMemberChecker extends DecoratorChecker {
|
|
|
118
117
|
const member = node.property.name;
|
|
119
118
|
this.error(info.members.get(member)?.kind === 'func'
|
|
120
119
|
? `"${name}" is not defined; to call a function of @${name} write @${name}.${member}()`
|
|
121
|
-
: `"${name}" is not defined; to read public state of @${name} write
|
|
120
|
+
: `"${name}" is not defined; to read public state of @${name} write @${name}.${member}`, object);
|
|
122
121
|
return true;
|
|
123
122
|
}
|
|
124
123
|
}
|
|
125
|
-
/** The member of a decorator that
|
|
124
|
+
/** The member of a decorator that `@cart.items[0].done` starts with, if any. */
|
|
126
125
|
function decoratorRoot(node) {
|
|
127
126
|
switch (node.kind) {
|
|
128
127
|
case 'DecoratorMember':
|
|
129
128
|
return node;
|
|
130
|
-
case 'DecoratorGet':
|
|
131
|
-
return node.target;
|
|
132
129
|
case 'MemberExpression':
|
|
133
130
|
case 'IndexExpression':
|
|
134
131
|
return decoratorRoot(node.object);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ComponentInfo, type DecoratorInfo } from './context.ts';
|
|
2
2
|
import { StyleChecker } from './styles.ts';
|
|
3
3
|
import { type Type } from './types.ts';
|
|
4
4
|
type WrapperInfo = NonNullable<DecoratorInfo['wrapper']>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { decoratorNames, wrapperBody, wrapperOf } from "../decorators.js";
|
|
2
|
+
import { withIncluded } from "./context.js";
|
|
2
3
|
import { isUntyped, ownStatements } from "./helpers.js";
|
|
3
4
|
import { StyleChecker } from "./styles.js";
|
|
4
5
|
import { isAssignable, typeToString, UNKNOWN } from "./types.js";
|
|
@@ -78,7 +79,7 @@ export class DecoratorWrapperChecker extends StyleChecker {
|
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
wrappedType(info, own, seen) {
|
|
81
|
-
return info.decorators.reduceRight((type, decorator) => (decorator.wrapper ? this.wrapperResult(decorator.wrapper, seen) : type), own);
|
|
82
|
+
return withIncluded(info.decorators).reduceRight((type, decorator) => (decorator.wrapper ? this.wrapperResult(decorator.wrapper, seen) : type), own);
|
|
82
83
|
}
|
|
83
84
|
/** What a wrapper gives, found without checking it again, like the markup of a component. */
|
|
84
85
|
wrapperResult(wrapper, seen) {
|
|
@@ -96,7 +97,7 @@ export class DecoratorWrapperChecker extends StyleChecker {
|
|
|
96
97
|
checkWrappedTypes(info) {
|
|
97
98
|
let type = this.ownResult(info, new Set());
|
|
98
99
|
let source = null;
|
|
99
|
-
for (const decorator of
|
|
100
|
+
for (const decorator of withIncluded(info.decorators).reverse()) {
|
|
100
101
|
const { wrapper } = decorator;
|
|
101
102
|
if (!wrapper)
|
|
102
103
|
continue;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type * as ast from '../ast.ts';
|
|
2
|
-
import type
|
|
2
|
+
import { type ComponentInfo, type DecoratorInfo, type Scope } from './context.ts';
|
|
3
3
|
import { DecoratorExportChecker } from './decorator-exports.ts';
|
|
4
|
+
import { type Type } from './types.ts';
|
|
4
5
|
export declare abstract class DecoratorChecker extends DecoratorExportChecker {
|
|
5
6
|
protected resolveDecoratorParams(info: DecoratorInfo): void;
|
|
6
7
|
/** `needs cart, auth`: other decorators, each once. */
|
|
7
8
|
protected checkNeedsDeclaration(info: DecoratorInfo): void;
|
|
8
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Decorators after the ones they need or include. A cycle is an error: none can be first.
|
|
11
|
+
*/
|
|
9
12
|
protected orderByNeeds(decorators: readonly DecoratorInfo[]): DecoratorInfo[];
|
|
10
13
|
/**
|
|
11
14
|
* The body is checked once, by itself: it does not see the components it is applied to. Its
|
|
@@ -15,11 +18,17 @@ export declare abstract class DecoratorChecker extends DecoratorExportChecker {
|
|
|
15
18
|
protected collectMembers(info: DecoratorInfo, scope: Scope): void;
|
|
16
19
|
/**
|
|
17
20
|
* The decorators before `comp`: they exist, are applied once and below the ones they need, get
|
|
18
|
-
* valid arguments, and their wrappers get what they can take.
|
|
21
|
+
* valid arguments, and their wrappers get what they can take. Those that a decorator includes
|
|
22
|
+
* are applied with it, above it.
|
|
19
23
|
*/
|
|
20
24
|
protected checkDecoratorUses(info: ComponentInfo): void;
|
|
21
|
-
/**
|
|
22
|
-
|
|
25
|
+
/** Applied once, below the ones it needs; `via` is the decorator that includes it. */
|
|
26
|
+
private checkApplied;
|
|
27
|
+
/**
|
|
28
|
+
* Arguments are positional and see `params`: the properties of the component, as its body does,
|
|
29
|
+
* or the parameters of the decorator that includes this one.
|
|
30
|
+
*/
|
|
31
|
+
protected checkDecoratorArguments(use: ast.DecoratorUse, decorator: DecoratorInfo, scope: readonly [ast.Identifier, Type][]): void;
|
|
23
32
|
/**
|
|
24
33
|
* The code of the decorator and its arguments go into the component's body, so the names of the
|
|
25
34
|
* module they use must not be hidden there by the component's declarations.
|