@velarscript/web 0.28.1 → 0.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/analysis/component-guidance.d.ts +52 -0
  2. package/dist/analysis/component-guidance.d.ts.map +1 -0
  3. package/dist/analysis/component-guidance.js +103 -0
  4. package/dist/analysis/component-guidance.js.map +1 -0
  5. package/dist/analysis/look-values.d.ts +30 -0
  6. package/dist/analysis/look-values.d.ts.map +1 -0
  7. package/dist/analysis/look-values.js +61 -0
  8. package/dist/analysis/look-values.js.map +1 -0
  9. package/dist/analysis/public-config.d.ts +44 -0
  10. package/dist/analysis/public-config.d.ts.map +1 -0
  11. package/dist/analysis/public-config.js +133 -0
  12. package/dist/analysis/public-config.js.map +1 -0
  13. package/dist/analysis/watch-cycles.d.ts +176 -0
  14. package/dist/analysis/watch-cycles.d.ts.map +1 -0
  15. package/dist/analysis/watch-cycles.js +300 -0
  16. package/dist/analysis/watch-cycles.js.map +1 -0
  17. package/dist/analyzer.d.ts +45 -0
  18. package/dist/analyzer.d.ts.map +1 -1
  19. package/dist/analyzer.js +134 -214
  20. package/dist/analyzer.js.map +1 -1
  21. package/dist/browser-host-runtime.d.ts +23 -0
  22. package/dist/browser-host-runtime.d.ts.map +1 -0
  23. package/dist/browser-host-runtime.js +29 -0
  24. package/dist/browser-host-runtime.js.map +1 -0
  25. package/dist/compiler.d.ts +1 -1
  26. package/dist/compiler.js +6 -6
  27. package/dist/compiler.js.map +1 -1
  28. package/dist/emitter.js +25 -5
  29. package/dist/emitter.js.map +1 -1
  30. package/dist/lexer.js +89 -16
  31. package/dist/lexer.js.map +1 -1
  32. package/dist/look.d.ts +1 -1
  33. package/dist/look.d.ts.map +1 -1
  34. package/dist/look.js +5 -3
  35. package/dist/look.js.map +1 -1
  36. package/dist/parser.d.ts +2 -0
  37. package/dist/parser.d.ts.map +1 -1
  38. package/dist/parser.js +23 -25
  39. package/dist/parser.js.map +1 -1
  40. package/dist/runtime-foundation.d.ts.map +1 -1
  41. package/dist/runtime-foundation.js +5 -5
  42. package/dist/runtime-foundation.js.map +1 -1
  43. package/dist/runtime.d.ts.map +1 -1
  44. package/dist/runtime.js +12 -11
  45. package/dist/runtime.js.map +1 -1
  46. package/dist/types.d.ts +9 -0
  47. package/dist/types.d.ts.map +1 -1
  48. package/dist/types.js +12 -0
  49. package/dist/types.js.map +1 -1
  50. package/dist/visual-blocks.d.ts +28 -0
  51. package/dist/visual-blocks.d.ts.map +1 -0
  52. package/dist/visual-blocks.js +23 -0
  53. package/dist/visual-blocks.js.map +1 -0
  54. package/package.json +3 -3
package/dist/analyzer.js CHANGED
@@ -1,15 +1,19 @@
1
1
  import { mechanicalEdits, mechanicalFix, semanticTypeIdentity } from "@velarscript/compiler";
2
- import { Analyzer, anyType, boolType, describeType, expressionContainsDirectAwait, invalidType, isInvalidType, isAssignable, isReadonlyView, mutatingCollectionMethods, nullType, nonOptional, numberType, optionalOf, spanIdentity, stringType, unknownType, } from "@velarscript/compiler/extension";
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
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_COLOR_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_SHARED_METRIC_KEYWORDS, 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
+ import { componentCallRefusal, componentRefHandleRefusal, componentSectionCountDiagnostics, watchedResourceSurfaceRefusal } from "./analysis/component-guidance.js";
9
+ import { foldedLengthPercentage, isLookNumericType, lookAdditiveType } from "./analysis/look-values.js";
10
+ import { collectPublicConfigNames, declaredPublicConfig, publicConfigDiagnostic } from "./analysis/public-config.js";
8
11
  import { byCodeUnit } from "./stable-order.js";
9
12
  import { dynamicChildLeaves, JSX_SCALAR_TEXT_HINT } from "./emitter.js";
10
13
  import { isWebCustomElementName, WEB_ARIA_ATTRIBUTES, WEB_ARIA_ENUMERATED_VALUES, WEB_ARIA_ROLES, WEB_ARIA_ROLE_SYNONYMS, WEB_BOOL_PRESENCE_HTML_ATTRIBUTES, WEB_HTML_ELEMENTS, WEB_MISSPELLED_ATTRIBUTES, WEB_NATIVE_ELEMENTS, } from "./elements.js";
11
14
  import { isWebExpression, isWebJsx, isWebKeyframes, isWebLook, isWebStatement, isWebUnit, } from "./ast.js";
12
15
  import { isWebComponentConstructor, isWebComponentType, isWebComputedExport, isWebNodeType, normalizeWebComponentType, webComponentConstructor, webComponentHandle, webComponentIntrinsic, webComponentName, webNodeType, WEB_EVENT_TYPE_NAMES, WEB_OWNED_TYPE_NAMES, } from "./types.js";
16
+ import { collectionMutators, collectReactiveWriters, finallySelfWrite, reactivePathOf, reactivePathRoot, statementBindsName, topLevelCall, watchSelfWrite, writerWritesPath, } from "./analysis/watch-cycles.js";
13
17
  // The canonical nominal identity of the Web RouteContext record. Route checks
14
18
  // probe with this identity so they succeed in modules that use route() without
15
19
  // importing RouteContext by name.
@@ -1000,9 +1004,6 @@ function firstRelativeCssAssetAddress(source) {
1000
1004
  }
1001
1005
  return null;
1002
1006
  }
1003
- function isLookNumericType(type) {
1004
- return type.kind === "named" && LOOK_NUMERIC_TYPE_NAMES.has(type.name);
1005
- }
1006
1007
  /** True when a property's declared domain includes a spelled visual unit type. */
1007
1008
  function mentionsLookUnitType(type) {
1008
1009
  if (type.kind === "named")
@@ -1020,17 +1021,7 @@ function lookLiteralZero(expression) {
1020
1021
  return lookLiteralZero(expression.operand);
1021
1022
  return isWebUnit(expression) && expression.value === 0;
1022
1023
  }
1023
- function lookAdditiveType(left, right) {
1024
- if (!isLookNumericType(left) || !isLookNumericType(right))
1025
- return null;
1026
- if (semanticTypeIdentity(left) === semanticTypeIdentity(right))
1027
- return left;
1028
- const lengthPercentageNames = new Set(["Length", "Percentage", "LengthPercentage"]);
1029
- if (left.kind === "named" && right.kind === "named"
1030
- && lengthPercentageNames.has(left.name) && lengthPercentageNames.has(right.name))
1031
- return lookLengthPercentage;
1032
- return null;
1033
- }
1024
+ const lookJoin = (left, right) => lookAdditiveType(left, right, semanticTypeIdentity(left) === semanticTypeIdentity(right));
1034
1025
  function containsCssImport(source) {
1035
1026
  for (const token of cssTokens(source)) {
1036
1027
  if (token.kind === "at-keyword" && token.name.toLowerCase() === "import")
@@ -1181,169 +1172,6 @@ function watchSubjectPath(expression) {
1181
1172
  return false;
1182
1173
  }
1183
1174
  }
1184
- /**
1185
- * D114 W: a reactive place written as one comparable key, so "the write and the
1186
- * subject name the same place" is one string equality.
1187
- *
1188
- * It is deliberately narrower than `renderWatchSubject`, which reconstructs any
1189
- * expression for a message. A key has to *decide*, so only the parts that name
1190
- * the same place on two evaluations are allowed into one: names, fields, and an
1191
- * index that is either a literal or another such path. `items[next()]` renders
1192
- * perfectly well and answers a different element every call, so it has no key
1193
- * and the shapes below stay silent on it — which is the right answer for a
1194
- * refusal that has to be right every time.
1195
- */
1196
- function reactiveWritePath(expression) {
1197
- switch (expression.kind) {
1198
- case "IdentifierExpression":
1199
- return expression.name;
1200
- case "MemberExpression": {
1201
- if (expression.optional)
1202
- return null;
1203
- const object = reactiveWritePath(expression.object);
1204
- return object === null ? null : `${object}.${expression.property}`;
1205
- }
1206
- case "IndexExpression": {
1207
- if (expression.optional)
1208
- return null;
1209
- const object = reactiveWritePath(expression.object);
1210
- if (object === null)
1211
- return null;
1212
- const index = expression.index.kind === "LiteralExpression"
1213
- ? (typeof expression.index.value === "string" ? JSON.stringify(expression.index.value) : expression.index.raw)
1214
- : reactiveWritePath(expression.index);
1215
- return index === null ? null : `${object}[${index}]`;
1216
- }
1217
- default:
1218
- return null;
1219
- }
1220
- }
1221
- /** The root name a reactive path starts from, which is the binding it resolves through. */
1222
- function reactivePathRoot(expression) {
1223
- switch (expression.kind) {
1224
- case "IdentifierExpression":
1225
- return expression.name;
1226
- case "MemberExpression":
1227
- case "IndexExpression":
1228
- return reactivePathRoot(expression.object);
1229
- default:
1230
- return null;
1231
- }
1232
- }
1233
- function bindingPatternBinds(pattern, name) {
1234
- switch (pattern.kind) {
1235
- case "NameBindingPattern":
1236
- return pattern.name === name;
1237
- case "ObjectBindingPattern":
1238
- return pattern.rest?.name === name || pattern.entries.some((entry) => bindingPatternBinds(entry.pattern, name));
1239
- case "ListBindingPattern":
1240
- return pattern.rest?.name === name
1241
- || pattern.elements.some((element) => element !== null && bindingPatternBinds(element, name));
1242
- default:
1243
- return false;
1244
- }
1245
- }
1246
- /**
1247
- * D114 W: whether a body statement introduces its own binding of `name`. From
1248
- * that statement on, the spelling names something else, and a write through it
1249
- * is not a write of the watched place. The scan stops there rather than
1250
- * guessing which of the two a later line meant.
1251
- */
1252
- function statementBindsName(statement, name) {
1253
- switch (statement.kind) {
1254
- case "VariableDeclaration":
1255
- return bindingPatternBinds(statement.pattern, name);
1256
- case "UsingDeclaration":
1257
- case "FunctionDeclaration":
1258
- case "ClassDeclaration":
1259
- return statement.name === name;
1260
- default:
1261
- return false;
1262
- }
1263
- }
1264
- /**
1265
- * D114 W: the call a body statement makes when the statement is nothing but
1266
- * that call. `detach` is included because it is how a synchronous watch body
1267
- * starts asynchronous work — the tour and four charter fences spell the reload
1268
- * that way — so a refusal that only saw the bare call would miss the shape it
1269
- * exists for. Everything else (a call inside an `if`, an argument, an assigned
1270
- * result) is not a plain top-level call and is not offered here.
1271
- */
1272
- function topLevelCall(statement) {
1273
- const expression = statement.kind === "ExpressionStatement" ? statement.expression
1274
- : statement.kind === "DetachStatement" ? statement.expression
1275
- : null;
1276
- return expression !== null && expression.kind === "CallExpression" ? expression : null;
1277
- }
1278
- /**
1279
- * D114 W: whether one plain body statement writes the reactive place `path`.
1280
- * An assignment or a compound assignment to it is one; so is a call of a
1281
- * mutating collection method on it, because a watch on a collection fires on
1282
- * its deep mutation and `mutating` is the compiler's own roster of the calls
1283
- * that mutate.
1284
- */
1285
- function reactiveWriteOf(statement, path, mutating) {
1286
- if (statement.kind === "AssignmentStatement")
1287
- return reactiveWritePath(statement.target) === path;
1288
- if (mutating === null)
1289
- return false;
1290
- const call = statement.kind === "ExpressionStatement" && statement.expression.kind === "CallExpression"
1291
- ? statement.expression
1292
- : null;
1293
- if (call === null || call.callee.kind !== "MemberExpression" || call.callee.optional)
1294
- return false;
1295
- return mutating.has(call.callee.property) && reactiveWritePath(call.callee.object) === path;
1296
- }
1297
- /**
1298
- * D114 W A2(b): whether an `action` or `async def` writes `path` at its own top
1299
- * level, unconditionally. One hop: what the callee itself calls is not
1300
- * followed. A parameter of the callee's own that is spelled like the path's
1301
- * root, or a binding it declares before the write, means the write is not of
1302
- * the watched place and the answer is no.
1303
- */
1304
- function writerWritesPath(writer, path, root, mutating) {
1305
- if (writer.parameters.includes(root))
1306
- return false;
1307
- for (const statement of writer.body) {
1308
- if (statementBindsName(statement, root))
1309
- return false;
1310
- if (reactiveWriteOf(statement, path, mutating))
1311
- return true;
1312
- }
1313
- return false;
1314
- }
1315
- function collectReactiveWriters(program) {
1316
- const writers = new Map();
1317
- const claim = (name, declaration) => {
1318
- writers.set(name, writers.has(name) ? null : declaration);
1319
- };
1320
- const record = (statements) => {
1321
- for (const statement of statements) {
1322
- if (statement.kind === "FunctionDeclaration") {
1323
- claim(statement.name, statement.asynchronous
1324
- ? { spelling: "async def", parameters: statement.parameters.map((parameter) => parameter.name), body: statement.body }
1325
- : null);
1326
- record(statement.body);
1327
- continue;
1328
- }
1329
- if (!isWebStatement(statement))
1330
- continue;
1331
- if (statement.kind === "ExtensionStatement:web:action") {
1332
- claim(statement.name, {
1333
- spelling: "action",
1334
- parameters: statement.parameters.map((parameter) => parameter.name),
1335
- body: statement.body,
1336
- });
1337
- record(statement.body);
1338
- continue;
1339
- }
1340
- if (statement.kind === "ExtensionStatement:web:component")
1341
- record(statement.body);
1342
- }
1343
- };
1344
- record(program.body);
1345
- return writers;
1346
- }
1347
1175
  /** The escapes a text literal carries back into source (`scanStringEscape`). */
1348
1176
  const WATCH_SUBJECT_TEXT_ESCAPES = {
1349
1177
  "\\": "\\\\",
@@ -1971,11 +1799,17 @@ export class VelarWebAnalyzer extends Analyzer {
1971
1799
  webSourceText;
1972
1800
  /** D74: only props whose authors wrote a readonly contract receive prop-specific guidance. */
1973
1801
  explicitReadonlyPropBindings = new Map();
1802
+ /** D114 0.29.0 LC-D1: the manifest's `web.publicConfig`, or null when this compile read no project manifest — an empty section is a claim the compile may check, and no manifest is no claim at all. */
1803
+ webPublicConfig;
1804
+ publicConfigNames = new Set();
1805
+ /** velar/look builder calls this compile refused on their own arguments (D114 0.29.0 LK-I2). */
1806
+ refusedBuilderCalls = [];
1974
1807
  constructor(context = {}, extensions = []) {
1975
1808
  super(context, extensions);
1976
1809
  this.webModulePath = context.path ?? null;
1977
1810
  this.webSourceText = context.sourceText ?? "";
1978
1811
  this.resources = context.resources ?? new Map();
1812
+ this.webPublicConfig = declaredPublicConfig(context.extensionProjectConfig?.get("@velarscript/web"));
1979
1813
  const webImports = [...(context.extensionImports?.get("@velarscript/web") ?? [])];
1980
1814
  this.importedLookStaticValues = new Map(webImports.filter((entry) => isLookStaticValue(entry[1])));
1981
1815
  this.importedComputedNames = new Set(webImports.filter(([, value]) => isWebComputedExport(value)).map(([name]) => name));
@@ -1983,6 +1817,7 @@ export class VelarWebAnalyzer extends Analyzer {
1983
1817
  analyze(program) {
1984
1818
  this.lookStaticValues = collectLookStaticValues(program, this.importedLookStaticValues);
1985
1819
  this.lookBuilderNames = collectLookBuilderNames(program);
1820
+ this.publicConfigNames = collectPublicConfigNames(program);
1986
1821
  this.lookImport = collectLookImportSite(program);
1987
1822
  this.lookDeclarations = collectLookDeclarations(program);
1988
1823
  for (const name of collectDerivedReactiveNames(program))
@@ -2034,11 +1869,19 @@ export class VelarWebAnalyzer extends Analyzer {
2034
1869
  * `builtinTypeNameDeclarationMessage` in packages/compiler/src/analyzer.ts,
2035
1870
  * reported as VEL3007. The rosters differ; the wording is meant to read
2036
1871
  * alike, so a change to either sentence belongs in both.
1872
+ *
1873
+ * `Duration` is on both rosters — Core owns it as a primitive and
1874
+ * `velar/look` republishes it — so a Web module used to report it twice. This
1875
+ * refusal is the more specific of the two, because it names the surface the
1876
+ * author is writing against, so it marks the name refused and Core's stays
1877
+ * unsaid. The mark is Core's own hook, which is what lets this pass take
1878
+ * precedence without either side learning the other's roster.
2037
1879
  */
2038
1880
  rejectWebOwnedTypeNames(program) {
2039
1881
  const reject = (name, errorSpan, noun) => {
2040
1882
  if (!WEB_OWNED_TYPE_NAMES.has(name))
2041
1883
  return;
1884
+ this.markTypeNameRefused(name);
2042
1885
  this.diagnostics.push(diagnostic("VEL5065", `'${name}' is a Web type name, so it cannot also name ${/^[aeiou]/iu.test(noun) ? "an" : "a"} ${noun}; every use of it in a Web module resolves to the built-in. Rename this declaration`, errorSpan));
2043
1886
  };
2044
1887
  for (const statement of program.body) {
@@ -2380,7 +2223,7 @@ export class VelarWebAnalyzer extends Analyzer {
2380
2223
  this.probedOperandTypes.set(spanIdentity(expression.right.span), right);
2381
2224
  }
2382
2225
  else {
2383
- const additive = expression.operator === "+" || expression.operator === "-" ? lookAdditiveType(left, right) : null;
2226
+ const additive = expression.operator === "+" || expression.operator === "-" ? lookJoin(left, right) : null;
2384
2227
  const result = additive
2385
2228
  ?? ((expression.operator === "*" || expression.operator === "/") && isLookNumericType(left) && right.kind === "number" ? left : null)
2386
2229
  ?? (expression.operator === "*" && left.kind === "number" && isLookNumericType(right) ? right : null);
@@ -2467,11 +2310,23 @@ export class VelarWebAnalyzer extends Analyzer {
2467
2310
  if (retired)
2468
2311
  return RETIRED_ACCESSOR_TYPE;
2469
2312
  }
2313
+ const folded = expression.kind !== "CallExpression" ? null
2314
+ : foldedLengthPercentage(expression, (name) => this.lookBuilderNames.get(name), (argument) => this.probedSlotType(argument), lookJoin);
2470
2315
  const result = super.inferExpression(expression, contextualType);
2471
2316
  if (expression.kind === "CallExpression") {
2472
2317
  this.checkLookBuilderCall(expression);
2318
+ // D114 0.29.0 LC-D1: the manifest this build bakes in, proved against the declared type.
2319
+ const report = publicConfigDiagnostic(expression, result, this.publicConfigNames, this.webPublicConfig, { expandAliases: (type) => this.expandAliases(type), fieldsOf: (identity) => this.fieldsOf(identity), describeType });
2320
+ if (report)
2321
+ this.diagnostics.push(report);
2473
2322
  }
2474
- return result;
2323
+ return folded ?? result;
2324
+ }
2325
+ /** Infers one builder slot and parks the answer in the probe cache, so the call's own analysis reads it back. */
2326
+ probedSlotType(argument) {
2327
+ const inferred = this.inferExpression(argument);
2328
+ this.probedOperandTypes.set(spanIdentity(argument.span), inferred);
2329
+ return this.expandAliases(inferred);
2475
2330
  }
2476
2331
  // A name refers to writable reactive state only when ordinary lexical lookup
2477
2332
  // still resolves it to the state binding; a shadowing local wins instead.
@@ -2606,6 +2461,11 @@ export class VelarWebAnalyzer extends Analyzer {
2606
2461
  this.diagnostics.push(diagnostic("VEL5064", `'${name}' is the reader itself, so watching it watches a value that never changes; declare the derived value — 'computed name = ${name}()' — then 'watch name:'`, expression.span));
2607
2462
  return false;
2608
2463
  }
2464
+ // ST-D1: the resource surface is answered ahead of the frozen rule, because it is a reactive value whose *own* handle never moves.
2465
+ if (name !== null && this.isResourceBinding(name)) {
2466
+ this.diagnostics.push(diagnostic("VEL5064", watchedResourceSurfaceRefusal(name), expression.span));
2467
+ return false;
2468
+ }
2609
2469
  if (this.frozenWatchSubject(expression)) {
2610
2470
  this.diagnostics.push(diagnostic("VEL5064", `This watch subject never changes, so its body can never run${name === null ? "" : ` — '${name}' is not a reactive source`}; watch a 'state', a 'computed', a prop, or a resource field, or move these statements to where they should run`, expression.span));
2611
2471
  return false;
@@ -2659,12 +2519,8 @@ export class VelarWebAnalyzer extends Analyzer {
2659
2519
  const root = reactivePathRoot(subject);
2660
2520
  if (root === null)
2661
2521
  return;
2662
- const path = reactiveWritePath(subject);
2663
- const collection = nonOptional(this.expandAliases(watched));
2664
- const mutating = collection.kind === "list" || collection.kind === "map"
2665
- || collection.kind === "set" || collection.kind === "record"
2666
- ? mutatingCollectionMethods(collection.kind)
2667
- : null;
2522
+ const place = reactivePathOf(subject);
2523
+ const writes = (steps, method) => this.watchSubjectWrite(watched, steps, method);
2668
2524
  // A resource publishes `value`, `loading`, `ready` and `error`, and
2669
2525
  // `reload` is the one member of the five that is not one of them. Asking it
2670
2526
  // that way keeps `analyzeResourceDeclaration`'s field map the only roster:
@@ -2677,14 +2533,14 @@ export class VelarWebAnalyzer extends Analyzer {
2677
2533
  for (const statement of body) {
2678
2534
  if (statementBindsName(statement, root))
2679
2535
  return;
2680
- if (path !== null && reactiveWriteOf(statement, path, mutating)) {
2681
- // A derived value is offered only where it could be declared. A field
2682
- // or an element has no `computed` spelling of its own, so naming one
2683
- // would hand the author a line that does not compile.
2684
- const derived = subject.kind === "IdentifierExpression"
2685
- ? `declare 'computed ${path} = ...' instead`
2686
- : "write this value where it is produced instead";
2687
- this.diagnostics.push(diagnostic("VEL5077", `This watch writes its own subject '${path}' at the top of its body, so every run re-triggers it and the runtime stops the loop after 100 rounds; write the condition that ends it, or watch the input this value follows and ${derived}`, statement.span));
2536
+ const selfWrite = place === null ? null : watchSelfWrite(subject, place, statement, writes);
2537
+ if (selfWrite !== null) {
2538
+ this.diagnostics.push(diagnostic("VEL5077", selfWrite, statement.span));
2539
+ return;
2540
+ }
2541
+ const inFinally = place === null ? null : finallySelfWrite(subject, place, statement, writes, root);
2542
+ if (inFinally !== null) {
2543
+ this.diagnostics.push(diagnostic("VEL5077", inFinally.message, inFinally.span));
2688
2544
  return;
2689
2545
  }
2690
2546
  const call = topLevelCall(statement);
@@ -2696,15 +2552,68 @@ export class VelarWebAnalyzer extends Analyzer {
2696
2552
  this.diagnostics.push(diagnostic("VEL5078", `This watch reloads '${resource}' — the resource it watches — so every completed load re-triggers it; watch the input the load reads instead, as 'watch userId:' with 'detach ${resource}.reload()' in its body`, call.span));
2697
2553
  return;
2698
2554
  }
2699
- if (path === null || call.callee.kind !== "IdentifierExpression")
2555
+ if (place === null || call.callee.kind !== "IdentifierExpression")
2700
2556
  continue;
2701
2557
  const writer = this.reactiveWriters.get(call.callee.name) ?? null;
2702
- if (writer === null || !writerWritesPath(writer, path, root, mutating))
2558
+ const written = writer === null ? null : writerWritesPath(writer, place, writes);
2559
+ if (written === null)
2703
2560
  continue;
2704
- this.diagnostics.push(diagnostic("VEL5079", `This watch starts '${call.callee.name}', which writes '${path}' the reactive value this watch is on — so each completed run re-triggers the watch; make the write conditional, or watch the input '${call.callee.name}' reads`, call.span));
2561
+ // D114 0.28.0 H-D1's other half, in the message family F1 gave VEL5077:
2562
+ // a writer that reaches a *part* of the subject names the part it wrote
2563
+ // and the subject it belongs to, because those are two different places
2564
+ // and the author has to find the one the helper touches.
2565
+ const reached = written.text === place.text
2566
+ ? `'${place.text}' — the reactive value this watch is on`
2567
+ : `'${written.text}', a part of its subject '${place.text}'`;
2568
+ this.diagnostics.push(diagnostic("VEL5079", `This watch starts '${call.callee.name}', which writes ${reached} — so each completed run re-triggers the watch; make the write conditional, or watch the input '${call.callee.name}' reads`, call.span));
2705
2569
  return;
2706
2570
  }
2707
2571
  }
2572
+ /**
2573
+ * D114 0.28.0 H-D1: the type of the place `steps` below the watched subject,
2574
+ * or null when the walk cannot reach one.
2575
+ *
2576
+ * Only the steps the reactive graph publishes as part of the subject are
2577
+ * walked — a record field and a collection element — because those are the
2578
+ * writes a watch on the containing value is woken by. A step that leaves them
2579
+ * (a class instance, a capability handle, a field the record does not declare)
2580
+ * is not provably part of the subject, and a refusal that must be right every
2581
+ * time answers no there rather than guessing.
2582
+ */
2583
+ /**
2584
+ * Whether a write `steps` below the watched subject, made the given way, is a
2585
+ * write of the subject — the one definition both the body scan (VEL5077) and
2586
+ * the one-hop writer scan (VEL5079) read. An assignment to a place the walk
2587
+ * can reach is a write; a call is one only when the type at that depth is a
2588
+ * collection and the call is on its own mutating roster.
2589
+ */
2590
+ watchSubjectWrite(watched, steps, method) {
2591
+ const written = this.reactivePlaceType(watched, steps);
2592
+ if (written === null)
2593
+ return false;
2594
+ if (method === null)
2595
+ return true;
2596
+ const mutating = collectionMutators(nonOptional(this.expandAliases(written)));
2597
+ return mutating !== null && mutating.has(method);
2598
+ }
2599
+ reactivePlaceType(subject, steps) {
2600
+ let current = subject;
2601
+ for (const step of steps) {
2602
+ const owner = nonOptional(this.expandAliases(current));
2603
+ const next = step.kind === "field"
2604
+ ? (owner.kind === "object" ? owner.fields.get(step.name) ?? null
2605
+ : owner.kind === "record" ? owner.value
2606
+ : owner.kind === "named" ? this.fieldsOf(owner.identity ?? owner.name)?.get(step.name) ?? null
2607
+ : null)
2608
+ : (owner.kind === "list" || owner.kind === "set" ? owner.element
2609
+ : owner.kind === "map" || owner.kind === "record" ? owner.value
2610
+ : null);
2611
+ if (next === null)
2612
+ return null;
2613
+ current = next;
2614
+ }
2615
+ return current;
2616
+ }
2708
2617
  /**
2709
2618
  * D89 A4: records `list = list.map(item => {…})`, React's immutable update,
2710
2619
  * where the callback builds a new record rather than changing a field.
@@ -2981,11 +2890,7 @@ export class VelarWebAnalyzer extends Analyzer {
2981
2890
  inferExtensionCall(callee, arguments_, argumentNames, callSpan) {
2982
2891
  if (!isWebComponentType(callee))
2983
2892
  return undefined;
2984
- const name = webComponentName(callee);
2985
- this.typeError(name ? `Render component '${name}' with JSX` : "Render a Component value with JSX", callSpan);
2986
- if (argumentNames?.some((argument) => argument !== null)) {
2987
- this.typeError("Components use JSX props rather than named call arguments", callSpan);
2988
- }
2893
+ this.typeError(componentCallRefusal(webComponentName(callee), arguments_, argumentNames, this.webSourceText), callSpan);
2989
2894
  for (const argument of arguments_)
2990
2895
  this.inferExpression(argument);
2991
2896
  return webNodeType;
@@ -3056,6 +2961,17 @@ export class VelarWebAnalyzer extends Analyzer {
3056
2961
  }
3057
2962
  return super.ownershipScopeRejection();
3058
2963
  }
2964
+ /**
2965
+ * D114 0.29.0 JX-I2: a component body has no function frame, so a `return` nested
2966
+ * inside it — in a `match` arm, an `if`, a `for` — used to earn VEL3003 next to
2967
+ * VEL5008 ("exactly one top-level return"), two rules its author can only read as
2968
+ * contradicting each other. A lifecycle hook and a watch body are inside the same
2969
+ * component and are *not* it: neither returns anything, so a `return` there keeps
2970
+ * VEL3003.
2971
+ */
2972
+ extensionOwnsFunctionlessReturn() {
2973
+ return this.componentBodyDepth > 0 && this.mountedDepth === 0 && this.cleanupDepth === 0 && this.watchBodyDepth === 0;
2974
+ }
3059
2975
  invalidExtensionAwaitContext() {
3060
2976
  return this.synchronousReactiveDepth > 0 || this.jsxDepth > 0
3061
2977
  || (this.componentStates !== null && this.mountedDepth === 0);
@@ -3253,16 +3169,7 @@ export class VelarWebAnalyzer extends Analyzer {
3253
3169
  this.analyzeStatement(item);
3254
3170
  }
3255
3171
  }
3256
- if (renders !== 1)
3257
- this.diagnostics.push(diagnostic("VEL5008", `Component '${statement.name}' must have exactly one top-level return`, statement.span));
3258
- if (mounted > 1)
3259
- this.diagnostics.push(diagnostic("VEL5009", `Component '${statement.name}' has more than one '@mounted' block`, statement.span));
3260
- if (cleanup > 1)
3261
- this.diagnostics.push(diagnostic("VEL5010", `Component '${statement.name}' has more than one '@cleanup' block`, statement.span));
3262
- if (exposes > 1)
3263
- this.diagnostics.push(diagnostic("VEL5056", `Component '${statement.name}' has more than one expose declaration`, statement.span));
3264
- if (statement.handleType && exposes === 0)
3265
- this.diagnostics.push(diagnostic("VEL5056", `Component '${statement.name}' declares an exposed Handle but does not provide an expose value`, statement.handleType.span));
3172
+ this.diagnostics.push(...componentSectionCountDiagnostics(statement.name, { renders, mounted, cleanup, exposes }, statement.span, statement.handleType?.span ?? null));
3266
3173
  if (renderValue && isWebJsx(renderValue))
3267
3174
  this.validateComponentHost(renderValue, statement);
3268
3175
  this.componentStates = previousStates;
@@ -3486,6 +3393,7 @@ export class VelarWebAnalyzer extends Analyzer {
3486
3393
  // table: `rgba(0, 0, 0, alpha=2)` compiled clean while `rgba(0, 0, 0, 2)`
3487
3394
  // was refused.
3488
3395
  const parameters = LOOK_BUILDER_SIGNATURES.get(builder)?.parameters;
3396
+ const before = this.diagnostics.length;
3489
3397
  for (const [index, argument] of expression.arguments.entries()) {
3490
3398
  const named = expression.argumentNames?.[index] ?? null;
3491
3399
  const position = named === null ? index : parameters?.indexOf(named) ?? -1;
@@ -3536,6 +3444,9 @@ export class VelarWebAnalyzer extends Analyzer {
3536
3444
  if (builder === "filters" && expression.arguments.length > 64) {
3537
3445
  this.diagnostics.push(diagnostic("VEL5042", "filters cannot compose more than 64 values", expression.span));
3538
3446
  }
3447
+ // D114 0.29.0 LK-I2: a call its own argument check refused is recorded so a `keyframes:` stop can drop the consequence. The record is the call rather than the code, because a bad token name and a stop's one-declaration rule are two facts about one value (D103-2) and both are still reported.
3448
+ if (this.diagnostics.length > before)
3449
+ this.refusedBuilderCalls.push(expression.span);
3539
3450
  }
3540
3451
  /**
3541
3452
  * D103 rules 1 and 5 — what a checked token reference has to be at the site
@@ -3676,7 +3587,11 @@ export class VelarWebAnalyzer extends Analyzer {
3676
3587
  const expected = LOOK_PROPERTY_TYPES.get(entry.name) ?? stringType;
3677
3588
  const actual = this.inferExpression(entry.value, expected);
3678
3589
  this.reportKeyframeSnapshotReads(entry.value);
3679
- if (keyframeCssValue(entry.value, this.lookStaticValues) === null) {
3590
+ // LK-I2: a builder that failed its own argument check has already been told what
3591
+ // is wrong with it, and "does not resolve to static CSS" is only the consequence —
3592
+ // a sentence that sends its author looking for a rule against named arguments in
3593
+ // a stop, which there is not (`spread=2px` in range compiles).
3594
+ if (keyframeCssValue(entry.value, this.lookStaticValues) === null && !this.refusedBuilderCallWithin(entry.value.span)) {
3680
3595
  this.diagnostics.push(diagnostic("VEL5060", "A keyframe value must resolve to static CSS from literals, unit values, arithmetic, velar/look builders, or const bindings — local or imported — that hold any of those, and the text it resolves to must read as one declaration value: no ';', '{', '}', or '@' outside a string, with parentheses, strings, and comments all closed", entry.value.span));
3681
3596
  }
3682
3597
  if (mentionsLookUnitType(expected) && lookLiteralZero(entry.value))
@@ -3688,6 +3603,10 @@ export class VelarWebAnalyzer extends Analyzer {
3688
3603
  }
3689
3604
  }
3690
3605
  }
3606
+ /** Whether a builder call this compile refused on its own arguments lies inside `sourceSpan`. */
3607
+ refusedBuilderCallWithin(sourceSpan) {
3608
+ return this.refusedBuilderCalls.some((call) => call.start >= sourceSpan.start && call.end <= sourceSpan.end);
3609
+ }
3691
3610
  reportKeyframeSnapshotReads(expression) {
3692
3611
  const visit = (value) => {
3693
3612
  if (Array.isArray(value)) {
@@ -4448,7 +4367,8 @@ export class VelarWebAnalyzer extends Analyzer {
4448
4367
  }
4449
4368
  const handle = webComponentHandle(component);
4450
4369
  if (!handle) {
4451
- this.diagnostics.push(diagnostic("VEL5057", `Component '${expression.tag}' does not expose a Handle`, attribute.span));
4370
+ const stored = nonOptional(this.expandAliases(this.lookup(value.name)?.declaredType ?? unknownType));
4371
+ this.diagnostics.push(diagnostic("VEL5057", componentRefHandleRefusal(expression.tag, component, stored), attribute.span));
4452
4372
  return;
4453
4373
  }
4454
4374
  const bindingType = this.lookup(value.name).type;