@velarscript/web 0.23.4 → 0.25.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 +7 -4
- package/dist/analyzer.d.ts +74 -5
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +353 -9
- package/dist/analyzer.js.map +1 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/compiler.js +8 -1
- package/dist/compiler.js.map +1 -1
- package/dist/emitter.d.ts +16 -0
- package/dist/emitter.d.ts.map +1 -1
- package/dist/emitter.js +163 -7
- package/dist/emitter.js.map +1 -1
- package/dist/lexer.js +72 -1
- package/dist/lexer.js.map +1 -1
- package/dist/look.d.ts +24 -0
- package/dist/look.d.ts.map +1 -1
- package/dist/look.js +153 -3
- package/dist/look.js.map +1 -1
- package/dist/runtime-foundation.d.ts.map +1 -1
- package/dist/runtime-foundation.js +32 -2
- package/dist/runtime-foundation.js.map +1 -1
- package/dist/runtime.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -92,7 +92,10 @@ The package also owns Look, the checked visual language integrated with VelarScr
|
|
|
92
92
|
values and JSX. `look:` values, ordinary functions, imports/exports, named
|
|
93
93
|
`velar/look` builders, unit-aware properties and arithmetic, bounded `if` conditions, `@state` hooks, and
|
|
94
94
|
`@target` blocks lower to extracted standard CSS with stable readable markers.
|
|
95
|
-
Native and component JSX accept universal `class` and `look` props
|
|
96
|
-
`style` directives
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
Native and component JSX accept universal `class` and `look` props, checked
|
|
96
|
+
`look:property` and `style:property` directives, and a raw `style` attribute is
|
|
97
|
+
rejected. A design system's CSS custom property is read by `token("--name")`,
|
|
98
|
+
which is legal in every Look property, so a token-driven visual layer stays
|
|
99
|
+
inside Look. Raw CSS is available through an explicit `import css unsafe`
|
|
100
|
+
declaration whose mandatory `before look` or `after look` placement defines
|
|
101
|
+
source order without inventing priority semantics.
|
package/dist/analyzer.d.ts
CHANGED
|
@@ -10,10 +10,14 @@ export declare class VelarWebAnalyzer extends Analyzer {
|
|
|
10
10
|
private watchBodyDepth;
|
|
11
11
|
/** D89 A4: the binding identity of every list a keyed `.map(...)` interpolation renders. */
|
|
12
12
|
private readonly keyedListSources;
|
|
13
|
-
/** D89 A4: every
|
|
13
|
+
/** D89 A4: every row rebuild of this module — assigned or derived — in source order. */
|
|
14
14
|
private readonly keyedListRebuilds;
|
|
15
|
+
/** D89 A4: this module's `def` bodies by name, so a `computed` that calls one can be read through. */
|
|
16
|
+
private moduleFunctions;
|
|
15
17
|
private synchronousReactiveDepth;
|
|
16
18
|
private jsxDepth;
|
|
19
|
+
/** VEL5075: how many component bodies enclose the declaration being analyzed. */
|
|
20
|
+
private componentBodyDepth;
|
|
17
21
|
private readonly resources;
|
|
18
22
|
private readonly unsafeCssImports;
|
|
19
23
|
private readonly probedOperandTypes;
|
|
@@ -124,6 +128,32 @@ export declare class VelarWebAnalyzer extends Analyzer {
|
|
|
124
128
|
protected inferExtensionExpression(expression: Expression, _contextualType: ValueType): ValueType | undefined;
|
|
125
129
|
protected inferExpression(expression: Expression, contextualType?: ValueType): ValueType;
|
|
126
130
|
private writableStateName;
|
|
131
|
+
/**
|
|
132
|
+
* P2b-5: a `def` that answers markup and answers it with a component element.
|
|
133
|
+
*
|
|
134
|
+
* Everything about the shape is legal one step out. A component element is a
|
|
135
|
+
* legal module-level expression — `const root = <App />` is the instantiation
|
|
136
|
+
* site D90 R4-b rules on, and `mount` takes exactly the instance it evaluates
|
|
137
|
+
* to. A `def` answering markup is a legal markup helper — dispatch over a
|
|
138
|
+
* closed vocabulary is what the P2b wave was writing. The defect is only
|
|
139
|
+
* where the two meet, and it is a representation split the type does not
|
|
140
|
+
* carry: a component element in a *child* position lowers to `__velarChild`,
|
|
141
|
+
* which owns a scope and answers a DOM node, while the same element standing
|
|
142
|
+
* alone lowers to `__velarInstantiate`, which answers an instance. Both are
|
|
143
|
+
* typed `WebNode`; only one is one. Returned from a helper and handed to a
|
|
144
|
+
* render, the instance reaches `__velarAppend` and takes the whole subtree
|
|
145
|
+
* down with "JSX can render only text, finite numbers, bool, enums, WebNode
|
|
146
|
+
* values, and Lists of those values" — the check-green, runtime-dead shape.
|
|
147
|
+
*
|
|
148
|
+
* The walk stops at every JSX element, which is exactly where the emitter
|
|
149
|
+
* stops: inside one, every position is a child position and every component
|
|
150
|
+
* element there is already correct — `return <div><Badge /></div>` and an
|
|
151
|
+
* interpolated `{cond ? <Badge /> : ...}` both work today and must keep
|
|
152
|
+
* working. Only a component element the returned markup *starts* with is the
|
|
153
|
+
* defect, including one reached through a `.map(...)` answering a row per
|
|
154
|
+
* item, which fails the same way for the same reason.
|
|
155
|
+
*/
|
|
156
|
+
private rejectUnownedComponentElement;
|
|
127
157
|
/**
|
|
128
158
|
* The audit's seventh root cause: a `def` that declares reactive state and
|
|
129
159
|
* answers `WebNode` is a component wearing a function's clothes, and calling
|
|
@@ -184,12 +214,33 @@ export declare class VelarWebAnalyzer extends Analyzer {
|
|
|
184
214
|
*/
|
|
185
215
|
private recordKeyedListRebuild;
|
|
186
216
|
/**
|
|
187
|
-
* D89 A4
|
|
217
|
+
* D89 A4, the wider proven shape: the same churn written as a derived value.
|
|
218
|
+
* `computed rows = source.map(item => {…})` and `computed rows = build(...)`
|
|
219
|
+
* over a `for`/`append` builder both hand a keyed position a fresh record for
|
|
220
|
+
* every row on every recompute, which is exactly what the assignment shape
|
|
221
|
+
* does — the reconciliation wave compiled both and found only one of them
|
|
222
|
+
* named. It is one advisory with a wider proof, not a second code: the defect,
|
|
223
|
+
* the consequence, and the suppression are the same.
|
|
224
|
+
*
|
|
225
|
+
* A `const` in a component body is deliberately not here. It is constructed
|
|
226
|
+
* once, so its records never move, and advising it would be a guess rather
|
|
227
|
+
* than a proof.
|
|
228
|
+
*/
|
|
229
|
+
private recordDerivedKeyedListRebuild;
|
|
230
|
+
/**
|
|
231
|
+
* Whether a derived initializer constructs one fresh record per source
|
|
232
|
+
* element. Two spellings are proven and everything else is silent: a `map`
|
|
233
|
+
* whose callback answers a record literal — the same recognizer the
|
|
234
|
+
* assignment shape uses — and a call to a `def` this module declares whose
|
|
235
|
+
* whole answer is a list it filled with record literals.
|
|
236
|
+
*/
|
|
237
|
+
private rebuildsRecordsPerElement;
|
|
238
|
+
/**
|
|
239
|
+
* D89 A4: raises the advisory for the rebuilds whose list a keyed position
|
|
188
240
|
* really renders. The advisory channel cannot reach `this.diagnostics`, so
|
|
189
241
|
* nothing here fails a build, changes an emitted byte, or moves a semantic
|
|
190
|
-
* rule; `// velar-allow A4: <reason>` suppresses it where
|
|
191
|
-
*
|
|
192
|
-
* makes it.
|
|
242
|
+
* rule; `// velar-allow A4: <reason>` suppresses it where building the rows is
|
|
243
|
+
* the only spelling, which a `readonly` list or one API response makes it.
|
|
193
244
|
*/
|
|
194
245
|
private adviseKeyedListRebuilds;
|
|
195
246
|
/** A value that is read by calling it and takes no arguments to do so. */
|
|
@@ -306,6 +357,24 @@ export declare class VelarWebAnalyzer extends Analyzer {
|
|
|
306
357
|
private checkAnimationKeyword;
|
|
307
358
|
private analyzeKeyframes;
|
|
308
359
|
private reportKeyframeSnapshotReads;
|
|
360
|
+
/**
|
|
361
|
+
* D104 rule 2 — the wider half of the promise VEL5039 already made. A
|
|
362
|
+
* repeated property name is refused because the second entry overwrites the
|
|
363
|
+
* first and Look has no source order to settle which is meant; a shorthand
|
|
364
|
+
* beside a longhand it writes is the same defect with two spellings, and it
|
|
365
|
+
* is the one that hid thirty-six dead declarations in a consumer's shell.
|
|
366
|
+
* Reports once against the entry that completes the pair.
|
|
367
|
+
*/
|
|
368
|
+
private reportLookShorthandOverlap;
|
|
369
|
+
/**
|
|
370
|
+
* The same rule where the properties are written as element directives. A
|
|
371
|
+
* `look:` directive lowers to exactly the rule a block entry does, so an
|
|
372
|
+
* element carrying `look:padding` and `look:paddingTop` has the pair in the
|
|
373
|
+
* same scope as surely as a block does. The `style:` directives are not
|
|
374
|
+
* checked here: they compose one inline style object, where the browser's own
|
|
375
|
+
* source order settles the pair.
|
|
376
|
+
*/
|
|
377
|
+
private reportLookDirectiveOverlap;
|
|
309
378
|
/**
|
|
310
379
|
* Records one entry in its lowered scope (condition signature plus target) so
|
|
311
380
|
* two sibling blocks with the same condition report the property they both
|
package/dist/analyzer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwD,KAAK,UAAU,EAA2C,KAAK,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAClK,OAAO,EACL,QAAQ,EAgBR,KAAK,eAAe,EACpB,KAAK,yBAAyB,EAC9B,KAAK,gCAAgC,EACrC,KAAK,UAAU,EAEf,KAAK,OAAO,EACZ,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,SAAS,EACf,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"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"}
|
package/dist/analyzer.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mechanicalEdits, mechanicalFix, semanticTypeIdentity } from "@velarscri
|
|
|
2
2
|
import { Analyzer, anyType, boolType, describeType, expressionContainsDirectAwait, invalidType, isInvalidType, isAssignable, isReadonlyView, nullType, nonOptional, numberType, optionalOf, spanIdentity, stringType, unknownType, } from "@velarscript/compiler/extension";
|
|
3
3
|
import { BROWSER_TEST_MODULE, BROWSER_TEST_SOURCE_SUFFIX, browserTestImportGuidance } from "./browser-test.js";
|
|
4
4
|
import { cssTokens } from "./css-tokens.js";
|
|
5
|
-
import { LOOK_ABSENT_MEDIA_SUBJECTS, LOOK_ARITHMETIC_HINT, LOOK_ANIMATION_DIRECTIONS, LOOK_ANIMATION_EASINGS, LOOK_ANIMATION_FILLS, LOOK_BORDER_STYLE_NAMES, LOOK_BUILDER_NUMERIC_RANGES, LOOK_BUILDER_SIGNATURES, LOOK_BUILDERS, LOOK_EXCLUDED_PROPERTIES, LOOK_HOOKS, LOOK_CSS_WIDE_KEYWORDS, LOOK_LARGE_KEYWORD_SETS, LOOK_LENGTH_BUILDERS, LOOK_MEDIA_LENGTH_UNITS, LOOK_MEDIA_SUBJECTS, LOOK_NUMERIC_TYPE_NAMES, LOOK_NON_ANIMATABLE_PROPERTIES, LOOK_PARTIAL_KEYWORD_PROPERTIES, LOOK_PROPERTIES, LOOK_PROPERTY_KEYWORDS, LOOK_PROPERTY_VALUE_KINDS, LOOK_TARGETS, LOOK_TOKEN_NAME_RULE, LOOK_TOKEN_NO_FALLBACK_GUIDANCE, LOOK_UNIT_TYPES, LOOK_UNITLESS_PROPERTIES, isLookTokenName, isLookVarReference, lookOwnKeywords, lookTokenReference, lookVarReferenceName, nearestLookName, } from "./look.js";
|
|
5
|
+
import { LOOK_ABSENT_MEDIA_SUBJECTS, LOOK_ARITHMETIC_HINT, LOOK_ANIMATION_DIRECTIONS, LOOK_ANIMATION_EASINGS, LOOK_ANIMATION_FILLS, LOOK_BORDER_STYLE_NAMES, LOOK_BUILDER_NUMERIC_RANGES, LOOK_BUILDER_SIGNATURES, LOOK_BUILDERS, LOOK_EXCLUDED_PROPERTIES, LOOK_HOOKS, LOOK_CSS_WIDE_KEYWORDS, LOOK_LARGE_KEYWORD_SETS, LOOK_LENGTH_BUILDERS, LOOK_MEDIA_LENGTH_UNITS, LOOK_MEDIA_SUBJECTS, LOOK_NUMERIC_TYPE_NAMES, LOOK_NON_ANIMATABLE_PROPERTIES, LOOK_PARTIAL_KEYWORD_PROPERTIES, LOOK_PROPERTIES, LOOK_PROPERTY_KEYWORDS, LOOK_PROPERTY_VALUE_KINDS, LOOK_TARGETS, LOOK_TOKEN_NAME_RULE, LOOK_TOKEN_NO_FALLBACK_GUIDANCE, LOOK_UNIT_TYPES, LOOK_UNITLESS_PROPERTIES, isLookTokenName, isLookVarReference, lookOwnKeywords, lookShorthandOverlap, lookShorthandParts, lookTokenReference, lookVarReferenceName, nearestLookName, } from "./look.js";
|
|
6
6
|
import { collectLookStaticValues, evaluateLookStaticExpression, isLookStaticValue, lookStaticCss } from "./look-static.js";
|
|
7
7
|
import { keyframeCssValue } from "./keyframes.js";
|
|
8
8
|
import { byCodeUnit } from "./stable-order.js";
|
|
@@ -428,6 +428,21 @@ function lookShorthandStringGuidance(name, value) {
|
|
|
428
428
|
}
|
|
429
429
|
return null;
|
|
430
430
|
}
|
|
431
|
+
/**
|
|
432
|
+
* D104 rule 2 — what a refusal says when two entries in one scope write the
|
|
433
|
+
* same CSS declaration. Written once because two positions raise it: a Look
|
|
434
|
+
* block's entries and an element's `look:` directives lower to the same
|
|
435
|
+
* one-rule-per-property stylesheet, so they have the same defect and deserve
|
|
436
|
+
* the same sentence (D57 rule 134).
|
|
437
|
+
*
|
|
438
|
+
* The message leads with the mechanism, because the pair is legal CSS and the
|
|
439
|
+
* author has every reason to expect source order to settle it. It does not
|
|
440
|
+
* here: see LOOK_SHORTHAND_LONGHANDS for why the winner is decided module-wide.
|
|
441
|
+
*/
|
|
442
|
+
function lookShorthandOverlapMessage(shorthand, longhand, subject) {
|
|
443
|
+
const parts = lookShorthandParts(shorthand);
|
|
444
|
+
return `${subject} sets '${shorthand}' and '${longhand}', and the shorthand writes the longhand. Look lowers every entry to its own CSS rule and orders those rules by where each property first appears in the module rather than by this block, so which of the two wins is not this code's to choose — an unrelated look elsewhere in the module decides it. Write ${parts.join(", ")} in place of '${shorthand}', or drop '${longhand}'`;
|
|
445
|
+
}
|
|
431
446
|
const lookCssWideKeywords = new Set(LOOK_CSS_WIDE_KEYWORDS);
|
|
432
447
|
const lookSharedMetricKeywords = ["auto", "none", "normal", "min-content", "max-content", "fit-content", "stretch"];
|
|
433
448
|
const lookMetricKeywords = new Set([...lookCssWideKeywords, ...lookSharedMetricKeywords]);
|
|
@@ -1275,6 +1290,134 @@ function keyedRebuiltRecord(body, row) {
|
|
|
1275
1290
|
}
|
|
1276
1291
|
return null;
|
|
1277
1292
|
}
|
|
1293
|
+
/**
|
|
1294
|
+
* D89 A4, the wider proven shape: every `def` of the module by name, so a
|
|
1295
|
+
* `computed` whose initializer calls one can be read through to what that call
|
|
1296
|
+
* builds. A name declared twice maps to null — two bodies under one name make
|
|
1297
|
+
* the question unanswerable, and an advisory that cannot prove which body runs
|
|
1298
|
+
* stays silent rather than guessing.
|
|
1299
|
+
*/
|
|
1300
|
+
function collectModuleFunctions(program) {
|
|
1301
|
+
const functions = new Map();
|
|
1302
|
+
const record = (statements) => {
|
|
1303
|
+
for (const statement of statements) {
|
|
1304
|
+
if (statement.kind === "FunctionDeclaration") {
|
|
1305
|
+
functions.set(statement.name, functions.has(statement.name) ? null : statement);
|
|
1306
|
+
record(statement.body);
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
if (isWebStatement(statement) && statement.kind === "ExtensionStatement:web:component") {
|
|
1310
|
+
record(statement.body);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
record(program.body);
|
|
1315
|
+
return functions;
|
|
1316
|
+
}
|
|
1317
|
+
/** Every name a binding pattern introduces, added to `names`. */
|
|
1318
|
+
function collectPatternNames(pattern, names) {
|
|
1319
|
+
if (pattern.kind === "NameBindingPattern") {
|
|
1320
|
+
names.add(pattern.name);
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
if (pattern.kind === "ListBindingPattern") {
|
|
1324
|
+
for (const element of pattern.elements)
|
|
1325
|
+
if (element)
|
|
1326
|
+
collectPatternNames(element, names);
|
|
1327
|
+
}
|
|
1328
|
+
else {
|
|
1329
|
+
for (const entry of pattern.entries)
|
|
1330
|
+
collectPatternNames(entry.pattern, names);
|
|
1331
|
+
}
|
|
1332
|
+
if (pattern.rest)
|
|
1333
|
+
names.add(pattern.rest.name);
|
|
1334
|
+
}
|
|
1335
|
+
/** True when the expression reads one of `names` anywhere inside it. */
|
|
1336
|
+
function readsAnyName(value, names) {
|
|
1337
|
+
if (!value || typeof value !== "object")
|
|
1338
|
+
return false;
|
|
1339
|
+
if (Array.isArray(value))
|
|
1340
|
+
return value.some((item) => readsAnyName(item, names));
|
|
1341
|
+
const record = value;
|
|
1342
|
+
if (record.kind === "IdentifierExpression" && typeof record.name === "string" && names.has(record.name))
|
|
1343
|
+
return true;
|
|
1344
|
+
return Object.values(record).some((child) => readsAnyName(child, names));
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* D89 A4, the wider proven shape: whether a `def` answers a list it filled with
|
|
1348
|
+
* freshly constructed records — the `for`/`append` builder that spells the same
|
|
1349
|
+
* churn `list = list.map(item => {…})` spells with `map`, and the spelling the
|
|
1350
|
+
* P2b consumer actually wrote.
|
|
1351
|
+
*
|
|
1352
|
+
* Four things are proven, and any one of them missing leaves the advisory
|
|
1353
|
+
* silent, because the whole product is naming a rebuild that really happens:
|
|
1354
|
+
*
|
|
1355
|
+
* 1. the body's last statement returns a name it declared here, initialized to
|
|
1356
|
+
* an empty list literal — so the list is the `def`'s own, not one handed in;
|
|
1357
|
+
* 2. every value appended to that list is a record literal, so every element is
|
|
1358
|
+
* a new value on every call. One `append(row)` of a source record is the
|
|
1359
|
+
* identity-preserving spelling this advisory teaches, and one member call
|
|
1360
|
+
* the proof cannot read leaves the whole answer unproven;
|
|
1361
|
+
* 3. at least one such record reads a parameter of the `def` or the binding of
|
|
1362
|
+
* the `for` that appends it. A builder whose records are constant answers
|
|
1363
|
+
* the same content every call, so nothing it is derived from can move and
|
|
1364
|
+
* the `computed` over it never recomputes;
|
|
1365
|
+
* 4. nothing else touches the list — it is built and answered, nothing more.
|
|
1366
|
+
*/
|
|
1367
|
+
function buildsFreshRecords(declaration) {
|
|
1368
|
+
const returned = declaration.body.at(-1);
|
|
1369
|
+
if (!returned || returned.kind !== "ReturnStatement" || returned.value?.kind !== "IdentifierExpression")
|
|
1370
|
+
return false;
|
|
1371
|
+
const built = returned.value.name;
|
|
1372
|
+
const local = declaration.body.find((statement) => statement.kind === "VariableDeclaration"
|
|
1373
|
+
&& statement.pattern.kind === "NameBindingPattern" && statement.pattern.name === built);
|
|
1374
|
+
if (!local || local.kind !== "VariableDeclaration")
|
|
1375
|
+
return false;
|
|
1376
|
+
if (local.initializer.kind !== "ListExpression" || local.initializer.elements.length > 0)
|
|
1377
|
+
return false;
|
|
1378
|
+
const parameters = new Set(declaration.parameters.map((parameter) => parameter.name));
|
|
1379
|
+
let records = 0;
|
|
1380
|
+
let varying = false;
|
|
1381
|
+
let unproven = false;
|
|
1382
|
+
const walk = (statements, visible) => {
|
|
1383
|
+
for (const statement of statements) {
|
|
1384
|
+
if (statement.kind === "ForStatement") {
|
|
1385
|
+
const inner = new Set(visible);
|
|
1386
|
+
collectPatternNames(statement.pattern, inner);
|
|
1387
|
+
if (statement.secondPattern)
|
|
1388
|
+
collectPatternNames(statement.secondPattern, inner);
|
|
1389
|
+
walk(statement.body, inner);
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
if (statement.kind === "IfStatement") {
|
|
1393
|
+
walk(statement.thenBody, visible);
|
|
1394
|
+
walk(statement.elseBody ?? [], visible);
|
|
1395
|
+
continue;
|
|
1396
|
+
}
|
|
1397
|
+
if (statement.kind === "WhileStatement") {
|
|
1398
|
+
walk(statement.body, visible);
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
if (statement.kind !== "ExpressionStatement")
|
|
1402
|
+
continue;
|
|
1403
|
+
const call = statement.expression;
|
|
1404
|
+
if (call.kind !== "CallExpression" || call.callee.kind !== "MemberExpression")
|
|
1405
|
+
continue;
|
|
1406
|
+
if (call.callee.object.kind !== "IdentifierExpression" || call.callee.object.name !== built)
|
|
1407
|
+
continue;
|
|
1408
|
+
const [appended] = call.arguments;
|
|
1409
|
+
if (call.callee.property !== "append" || !appended || appended.kind !== "ObjectExpression") {
|
|
1410
|
+
unproven = true;
|
|
1411
|
+
continue;
|
|
1412
|
+
}
|
|
1413
|
+
records += 1;
|
|
1414
|
+
if (readsAnyName(appended, visible))
|
|
1415
|
+
varying = true;
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
walk(declaration.body, parameters);
|
|
1419
|
+
return !unproven && records > 0 && varying;
|
|
1420
|
+
}
|
|
1278
1421
|
/**
|
|
1279
1422
|
* The reactive declarations a `def` body may not hold when the `def` answers
|
|
1280
1423
|
* `WebNode`. `watch`, `resource` and `action` are already refused outside a
|
|
@@ -1396,6 +1539,59 @@ function subtreeHoldsJsx(value) {
|
|
|
1396
1539
|
}
|
|
1397
1540
|
return false;
|
|
1398
1541
|
}
|
|
1542
|
+
/**
|
|
1543
|
+
* VEL5075: the JSX elements a body's own `return`s *start* with — the roots of
|
|
1544
|
+
* the returned markup, which is where the emitter decides between an instance
|
|
1545
|
+
* and a child. The walk descends through ordinary expression nodes, `.map(...)`
|
|
1546
|
+
* callbacks included, and stops at the first JSX element on every path: inside
|
|
1547
|
+
* one, every JSX position is a child position and needs no verdict. A nested
|
|
1548
|
+
* `def` or `component` owns its own returns, exactly as it does elsewhere here.
|
|
1549
|
+
*/
|
|
1550
|
+
function returnedMarkupRoots(body) {
|
|
1551
|
+
const roots = [];
|
|
1552
|
+
const collect = (value) => {
|
|
1553
|
+
const pending = [value];
|
|
1554
|
+
while (pending.length > 0) {
|
|
1555
|
+
const entry = pending.pop();
|
|
1556
|
+
if (Array.isArray(entry)) {
|
|
1557
|
+
for (const item of entry)
|
|
1558
|
+
pending.push(item);
|
|
1559
|
+
continue;
|
|
1560
|
+
}
|
|
1561
|
+
if (entry === null || typeof entry !== "object")
|
|
1562
|
+
continue;
|
|
1563
|
+
const node = entry;
|
|
1564
|
+
if (node.kind === "ExtensionExpression:web:jsx") {
|
|
1565
|
+
roots.push(node);
|
|
1566
|
+
continue;
|
|
1567
|
+
}
|
|
1568
|
+
for (const key of Object.keys(node))
|
|
1569
|
+
pending.push(node[key]);
|
|
1570
|
+
}
|
|
1571
|
+
};
|
|
1572
|
+
const pending = [...body];
|
|
1573
|
+
while (pending.length > 0) {
|
|
1574
|
+
const value = pending.pop();
|
|
1575
|
+
if (Array.isArray(value)) {
|
|
1576
|
+
for (const entry of value)
|
|
1577
|
+
pending.push(entry);
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1580
|
+
if (value === null || typeof value !== "object")
|
|
1581
|
+
continue;
|
|
1582
|
+
const node = value;
|
|
1583
|
+
const kind = typeof node.kind === "string" ? node.kind : null;
|
|
1584
|
+
if (kind === "FunctionDeclaration" || kind === "ExtensionStatement:web:component")
|
|
1585
|
+
continue;
|
|
1586
|
+
if (kind === "ReturnStatement") {
|
|
1587
|
+
collect(node.value);
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
for (const key of Object.keys(node))
|
|
1591
|
+
pending.push(node[key]);
|
|
1592
|
+
}
|
|
1593
|
+
return roots;
|
|
1594
|
+
}
|
|
1399
1595
|
/** The component spelling of a helper's name: components render as a PascalCase tag. */
|
|
1400
1596
|
function componentSpelling(name) {
|
|
1401
1597
|
return name.length === 0 ? name : `${name[0].toUpperCase()}${name.slice(1)}`;
|
|
@@ -1413,10 +1609,14 @@ export class VelarWebAnalyzer extends Analyzer {
|
|
|
1413
1609
|
watchBodyDepth = 0;
|
|
1414
1610
|
/** D89 A4: the binding identity of every list a keyed `.map(...)` interpolation renders. */
|
|
1415
1611
|
keyedListSources = new Set();
|
|
1416
|
-
/** D89 A4: every
|
|
1612
|
+
/** D89 A4: every row rebuild of this module — assigned or derived — in source order. */
|
|
1417
1613
|
keyedListRebuilds = [];
|
|
1614
|
+
/** D89 A4: this module's `def` bodies by name, so a `computed` that calls one can be read through. */
|
|
1615
|
+
moduleFunctions = new Map();
|
|
1418
1616
|
synchronousReactiveDepth = 0;
|
|
1419
1617
|
jsxDepth = 0;
|
|
1618
|
+
/** VEL5075: how many component bodies enclose the declaration being analyzed. */
|
|
1619
|
+
componentBodyDepth = 0;
|
|
1420
1620
|
resources;
|
|
1421
1621
|
unsafeCssImports = new Set();
|
|
1422
1622
|
probedOperandTypes = new Map();
|
|
@@ -1478,6 +1678,7 @@ export class VelarWebAnalyzer extends Analyzer {
|
|
|
1478
1678
|
this.rejectWebOwnedTypeNames(program);
|
|
1479
1679
|
this.keyedListSources.clear();
|
|
1480
1680
|
this.keyedListRebuilds.length = 0;
|
|
1681
|
+
this.moduleFunctions = collectModuleFunctions(program);
|
|
1481
1682
|
super.analyze(program);
|
|
1482
1683
|
this.reportStaticJsxKeys();
|
|
1483
1684
|
this.reportRetiredComputedFunction();
|
|
@@ -1591,8 +1792,10 @@ export class VelarWebAnalyzer extends Analyzer {
|
|
|
1591
1792
|
if (statement.kind === "VariableDeclaration") {
|
|
1592
1793
|
this.recordRetiredAccessorDeclaration(statement);
|
|
1593
1794
|
}
|
|
1594
|
-
if (statement.kind === "FunctionDeclaration")
|
|
1795
|
+
if (statement.kind === "FunctionDeclaration") {
|
|
1595
1796
|
this.rejectStatefulWebNodeFunction(statement);
|
|
1797
|
+
this.rejectUnownedComponentElement(statement);
|
|
1798
|
+
}
|
|
1596
1799
|
if (!isWebStatement(statement))
|
|
1597
1800
|
return false;
|
|
1598
1801
|
switch (statement.kind) {
|
|
@@ -1771,6 +1974,7 @@ export class VelarWebAnalyzer extends Analyzer {
|
|
|
1771
1974
|
this.declareBinding(statement.name, false, declared, statement.span);
|
|
1772
1975
|
this.markDeclaredBindingReactive(statement.name, "prop");
|
|
1773
1976
|
this.computedBindingSpans.add(spanIdentity(statement.span));
|
|
1977
|
+
this.recordDerivedKeyedListRebuild(statement);
|
|
1774
1978
|
}
|
|
1775
1979
|
/**
|
|
1776
1980
|
* True when `name` resolves to a `computed` declaration or to an imported one.
|
|
@@ -1944,6 +2148,49 @@ export class VelarWebAnalyzer extends Analyzer {
|
|
|
1944
2148
|
writableStateName(name) {
|
|
1945
2149
|
return this.reactiveBindingKind(name) === "state";
|
|
1946
2150
|
}
|
|
2151
|
+
/**
|
|
2152
|
+
* P2b-5: a `def` that answers markup and answers it with a component element.
|
|
2153
|
+
*
|
|
2154
|
+
* Everything about the shape is legal one step out. A component element is a
|
|
2155
|
+
* legal module-level expression — `const root = <App />` is the instantiation
|
|
2156
|
+
* site D90 R4-b rules on, and `mount` takes exactly the instance it evaluates
|
|
2157
|
+
* to. A `def` answering markup is a legal markup helper — dispatch over a
|
|
2158
|
+
* closed vocabulary is what the P2b wave was writing. The defect is only
|
|
2159
|
+
* where the two meet, and it is a representation split the type does not
|
|
2160
|
+
* carry: a component element in a *child* position lowers to `__velarChild`,
|
|
2161
|
+
* which owns a scope and answers a DOM node, while the same element standing
|
|
2162
|
+
* alone lowers to `__velarInstantiate`, which answers an instance. Both are
|
|
2163
|
+
* typed `WebNode`; only one is one. Returned from a helper and handed to a
|
|
2164
|
+
* render, the instance reaches `__velarAppend` and takes the whole subtree
|
|
2165
|
+
* down with "JSX can render only text, finite numbers, bool, enums, WebNode
|
|
2166
|
+
* values, and Lists of those values" — the check-green, runtime-dead shape.
|
|
2167
|
+
*
|
|
2168
|
+
* The walk stops at every JSX element, which is exactly where the emitter
|
|
2169
|
+
* stops: inside one, every position is a child position and every component
|
|
2170
|
+
* element there is already correct — `return <div><Badge /></div>` and an
|
|
2171
|
+
* interpolated `{cond ? <Badge /> : ...}` both work today and must keep
|
|
2172
|
+
* working. Only a component element the returned markup *starts* with is the
|
|
2173
|
+
* defect, including one reached through a `.map(...)` answering a row per
|
|
2174
|
+
* item, which fails the same way for the same reason.
|
|
2175
|
+
*/
|
|
2176
|
+
rejectUnownedComponentElement(statement) {
|
|
2177
|
+
// A helper nested in a component body is emitted with that component's
|
|
2178
|
+
// scope in hand, so its component elements are `__velarChild` and answer
|
|
2179
|
+
// nodes. Only a helper standing outside every component body has nowhere
|
|
2180
|
+
// for the emitter to put them.
|
|
2181
|
+
if (this.componentBodyDepth > 0)
|
|
2182
|
+
return;
|
|
2183
|
+
const answersMarkup = statement.returnType
|
|
2184
|
+
? carriesWebNode(this.resolveAnnotation(statement.returnType))
|
|
2185
|
+
: bodyReturnsJsx(statement.body);
|
|
2186
|
+
if (!answersMarkup)
|
|
2187
|
+
return;
|
|
2188
|
+
for (const element of returnedMarkupRoots(statement.body)) {
|
|
2189
|
+
if (!/^[A-Z]/u.test(element.tag))
|
|
2190
|
+
continue;
|
|
2191
|
+
this.diagnostics.push(diagnostic("VEL5075", `'${statement.name}' answers markup with the component element '<${element.tag} />', and a component element standing on its own is an instance rather than a node: it is built by the position that shows it, and a 'def' is not one, so what this returns fails the moment JSX tries to render it. Write '<${element.tag} />' where it is rendered — a component's own body, or a child position inside markup this returns such as '<div><${element.tag} ... /></div>' — and keep the helper for the native elements it builds.`, element.span));
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
1947
2194
|
/**
|
|
1948
2195
|
* The audit's seventh root cause: a `def` that declares reactive state and
|
|
1949
2196
|
* answers `WebNode` is a component wearing a function's clothes, and calling
|
|
@@ -2088,6 +2335,7 @@ export class VelarWebAnalyzer extends Analyzer {
|
|
|
2088
2335
|
if (!binding)
|
|
2089
2336
|
return;
|
|
2090
2337
|
this.keyedListRebuilds.push({
|
|
2338
|
+
kind: "assignment",
|
|
2091
2339
|
source: spanIdentity(binding.span),
|
|
2092
2340
|
name: statement.target.name,
|
|
2093
2341
|
field: rebuilt.field,
|
|
@@ -2095,12 +2343,57 @@ export class VelarWebAnalyzer extends Analyzer {
|
|
|
2095
2343
|
});
|
|
2096
2344
|
}
|
|
2097
2345
|
/**
|
|
2098
|
-
* D89 A4
|
|
2346
|
+
* D89 A4, the wider proven shape: the same churn written as a derived value.
|
|
2347
|
+
* `computed rows = source.map(item => {…})` and `computed rows = build(...)`
|
|
2348
|
+
* over a `for`/`append` builder both hand a keyed position a fresh record for
|
|
2349
|
+
* every row on every recompute, which is exactly what the assignment shape
|
|
2350
|
+
* does — the reconciliation wave compiled both and found only one of them
|
|
2351
|
+
* named. It is one advisory with a wider proof, not a second code: the defect,
|
|
2352
|
+
* the consequence, and the suppression are the same.
|
|
2353
|
+
*
|
|
2354
|
+
* A `const` in a component body is deliberately not here. It is constructed
|
|
2355
|
+
* once, so its records never move, and advising it would be a guess rather
|
|
2356
|
+
* than a proof.
|
|
2357
|
+
*/
|
|
2358
|
+
recordDerivedKeyedListRebuild(statement) {
|
|
2359
|
+
if (!this.rebuildsRecordsPerElement(statement.initializer))
|
|
2360
|
+
return;
|
|
2361
|
+
this.keyedListRebuilds.push({
|
|
2362
|
+
kind: "derived",
|
|
2363
|
+
source: spanIdentity(statement.span),
|
|
2364
|
+
name: statement.name,
|
|
2365
|
+
field: null,
|
|
2366
|
+
span: statement.span,
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
/**
|
|
2370
|
+
* Whether a derived initializer constructs one fresh record per source
|
|
2371
|
+
* element. Two spellings are proven and everything else is silent: a `map`
|
|
2372
|
+
* whose callback answers a record literal — the same recognizer the
|
|
2373
|
+
* assignment shape uses — and a call to a `def` this module declares whose
|
|
2374
|
+
* whole answer is a list it filled with record literals.
|
|
2375
|
+
*/
|
|
2376
|
+
rebuildsRecordsPerElement(value) {
|
|
2377
|
+
if (value.kind !== "CallExpression")
|
|
2378
|
+
return false;
|
|
2379
|
+
if (value.callee.kind === "MemberExpression" && value.callee.property === "map") {
|
|
2380
|
+
const callback = value.arguments[0];
|
|
2381
|
+
if (!callback || callback.kind !== "ArrowFunctionExpression" || callback.parameters.length !== 1)
|
|
2382
|
+
return false;
|
|
2383
|
+
const [row] = callback.parameters;
|
|
2384
|
+
return row !== undefined && keyedRebuiltRecord(callback.body, row.name) !== null;
|
|
2385
|
+
}
|
|
2386
|
+
if (value.callee.kind !== "IdentifierExpression")
|
|
2387
|
+
return false;
|
|
2388
|
+
const declaration = this.moduleFunctions.get(value.callee.name);
|
|
2389
|
+
return declaration !== undefined && declaration !== null && buildsFreshRecords(declaration);
|
|
2390
|
+
}
|
|
2391
|
+
/**
|
|
2392
|
+
* D89 A4: raises the advisory for the rebuilds whose list a keyed position
|
|
2099
2393
|
* really renders. The advisory channel cannot reach `this.diagnostics`, so
|
|
2100
2394
|
* nothing here fails a build, changes an emitted byte, or moves a semantic
|
|
2101
|
-
* rule; `// velar-allow A4: <reason>` suppresses it where
|
|
2102
|
-
*
|
|
2103
|
-
* makes it.
|
|
2395
|
+
* rule; `// velar-allow A4: <reason>` suppresses it where building the rows is
|
|
2396
|
+
* the only spelling, which a `readonly` list or one API response makes it.
|
|
2104
2397
|
*/
|
|
2105
2398
|
adviseKeyedListRebuilds() {
|
|
2106
2399
|
for (const rebuild of this.keyedListRebuilds) {
|
|
@@ -2110,8 +2403,13 @@ export class VelarWebAnalyzer extends Analyzer {
|
|
|
2110
2403
|
// the same key and then drops it because the row it holds is no longer
|
|
2111
2404
|
// the same value, so a message blaming the key sends an author to check
|
|
2112
2405
|
// `id`, find it unchanged, and conclude the advisory is wrong.
|
|
2113
|
-
|
|
2114
|
-
|
|
2406
|
+
// A derived value has no row to write: whatever it hands the keyed
|
|
2407
|
+
// position it built itself. The two ways out are to render the rows that
|
|
2408
|
+
// do have an identity, or to carry those same records through.
|
|
2409
|
+
const remedy = rebuild.kind === "derived"
|
|
2410
|
+
? "A derived value cannot keep rows it builds: render the source rows and change the field on them in place, or carry the source records through instead of constructing new ones."
|
|
2411
|
+
: `Change the field in place instead: '${rebuild.name}[index].${rebuild.field ?? "<field>"} = ...'`;
|
|
2412
|
+
this.advise("A4", `This rebuilds every row of '${rebuild.name}'${rebuild.kind === "derived" ? " on every recompute" : ""}, so every row is a new value and the keyed list that renders '${rebuild.name}' no longer recognises any of them: it destroys and rebuilds all of its children — an input being typed into loses focus. ${remedy}`, rebuild.span);
|
|
2115
2413
|
}
|
|
2116
2414
|
}
|
|
2117
2415
|
/** A value that is read by calling it and takes no arguments to do so. */
|
|
@@ -2419,6 +2717,9 @@ export class VelarWebAnalyzer extends Analyzer {
|
|
|
2419
2717
|
const outerConstructorDepth = this.constructorDepth;
|
|
2420
2718
|
if (!this.isPredeclared(statement))
|
|
2421
2719
|
this.declareBinding(statement.name, false, this.componentType(statement), statement.span);
|
|
2720
|
+
// VEL5075: a `def` written from here to the matching decrement lowers
|
|
2721
|
+
// through this component's scope, so its component elements are children.
|
|
2722
|
+
this.componentBodyDepth += 1;
|
|
2422
2723
|
this.enterScope();
|
|
2423
2724
|
this.flowFrameDepth += 1;
|
|
2424
2725
|
const previousStates = this.componentStates;
|
|
@@ -2559,6 +2860,7 @@ export class VelarWebAnalyzer extends Analyzer {
|
|
|
2559
2860
|
this.explicitReadonlyPropBindings = previousExplicitReadonlyProps;
|
|
2560
2861
|
this.flowFrameDepth -= 1;
|
|
2561
2862
|
this.exitScope();
|
|
2863
|
+
this.componentBodyDepth -= 1;
|
|
2562
2864
|
this.constructorDepth = outerConstructorDepth;
|
|
2563
2865
|
}
|
|
2564
2866
|
containsReadonlyView(type) {
|
|
@@ -2673,6 +2975,7 @@ export class VelarWebAnalyzer extends Analyzer {
|
|
|
2673
2975
|
this.analyzeLookEntries(entry.entries, true, true, inheritedTerms, repeated ? `${scopeKey}@${entry.name}#${entry.span.start}` : `${scopeKey}@${entry.name}`);
|
|
2674
2976
|
continue;
|
|
2675
2977
|
}
|
|
2978
|
+
this.reportLookShorthandOverlap(scopeKey, entry.name, entry.span);
|
|
2676
2979
|
if (!this.recordLookEntry(scopeKey, entry.name)) {
|
|
2677
2980
|
this.diagnostics.push(diagnostic("VEL5039", `Look property '${entry.name}' is defined more than once in the same scope`, entry.span));
|
|
2678
2981
|
}
|
|
@@ -2998,6 +3301,46 @@ export class VelarWebAnalyzer extends Analyzer {
|
|
|
2998
3301
|
};
|
|
2999
3302
|
visit(expression);
|
|
3000
3303
|
}
|
|
3304
|
+
/**
|
|
3305
|
+
* D104 rule 2 — the wider half of the promise VEL5039 already made. A
|
|
3306
|
+
* repeated property name is refused because the second entry overwrites the
|
|
3307
|
+
* first and Look has no source order to settle which is meant; a shorthand
|
|
3308
|
+
* beside a longhand it writes is the same defect with two spellings, and it
|
|
3309
|
+
* is the one that hid thirty-six dead declarations in a consumer's shell.
|
|
3310
|
+
* Reports once against the entry that completes the pair.
|
|
3311
|
+
*/
|
|
3312
|
+
reportLookShorthandOverlap(scopeKey, name, span) {
|
|
3313
|
+
for (const other of this.lookEntryScopes.get(scopeKey) ?? []) {
|
|
3314
|
+
const overlap = lookShorthandOverlap(other, name);
|
|
3315
|
+
if (!overlap)
|
|
3316
|
+
continue;
|
|
3317
|
+
this.diagnostics.push(diagnostic("VEL5039", lookShorthandOverlapMessage(overlap.shorthand, overlap.longhand, "This Look scope"), span));
|
|
3318
|
+
return;
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
/**
|
|
3322
|
+
* The same rule where the properties are written as element directives. A
|
|
3323
|
+
* `look:` directive lowers to exactly the rule a block entry does, so an
|
|
3324
|
+
* element carrying `look:padding` and `look:paddingTop` has the pair in the
|
|
3325
|
+
* same scope as surely as a block does. The `style:` directives are not
|
|
3326
|
+
* checked here: they compose one inline style object, where the browser's own
|
|
3327
|
+
* source order settles the pair.
|
|
3328
|
+
*/
|
|
3329
|
+
reportLookDirectiveOverlap(expression) {
|
|
3330
|
+
const written = [];
|
|
3331
|
+
for (const attribute of expression.attributes) {
|
|
3332
|
+
if (!attribute.name.startsWith("look:"))
|
|
3333
|
+
continue;
|
|
3334
|
+
const property = attribute.name.slice("look:".length);
|
|
3335
|
+
if (!LOOK_PROPERTIES.has(property))
|
|
3336
|
+
continue;
|
|
3337
|
+
const overlapping = written.map((other) => lookShorthandOverlap(other, property)).find(Boolean);
|
|
3338
|
+
if (overlapping) {
|
|
3339
|
+
this.diagnostics.push(diagnostic("VEL5039", lookShorthandOverlapMessage(overlapping.shorthand, overlapping.longhand, `Element '<${expression.tag}>'`), attribute.span));
|
|
3340
|
+
}
|
|
3341
|
+
written.push(property);
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3001
3344
|
/**
|
|
3002
3345
|
* Records one entry in its lowered scope (condition signature plus target) so
|
|
3003
3346
|
* two sibling blocks with the same condition report the property they both
|
|
@@ -3309,6 +3652,7 @@ export class VelarWebAnalyzer extends Analyzer {
|
|
|
3309
3652
|
}
|
|
3310
3653
|
if (attributes.size !== expression.attributes.length)
|
|
3311
3654
|
this.diagnostics.push(diagnostic("VEL5014", `JSX element '${expression.tag}' has duplicate attributes`, expression.span));
|
|
3655
|
+
this.reportLookDirectiveOverlap(expression);
|
|
3312
3656
|
for (const attribute of expression.attributes) {
|
|
3313
3657
|
if (removedJsxControlAttributes.has(attribute.name)) {
|
|
3314
3658
|
this.diagnostics.push(diagnostic("VEL5029", `JSX '${attribute.name}' was removed; branch with an ordinary expression such as {condition ? <A /> : <B />}`, attribute.span));
|