@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.
- package/dist/analysis/component-guidance.d.ts +52 -0
- package/dist/analysis/component-guidance.d.ts.map +1 -0
- package/dist/analysis/component-guidance.js +103 -0
- package/dist/analysis/component-guidance.js.map +1 -0
- package/dist/analysis/look-values.d.ts +30 -0
- package/dist/analysis/look-values.d.ts.map +1 -0
- package/dist/analysis/look-values.js +61 -0
- package/dist/analysis/look-values.js.map +1 -0
- package/dist/analysis/public-config.d.ts +44 -0
- package/dist/analysis/public-config.d.ts.map +1 -0
- package/dist/analysis/public-config.js +133 -0
- package/dist/analysis/public-config.js.map +1 -0
- package/dist/analysis/watch-cycles.d.ts +176 -0
- package/dist/analysis/watch-cycles.d.ts.map +1 -0
- package/dist/analysis/watch-cycles.js +300 -0
- package/dist/analysis/watch-cycles.js.map +1 -0
- package/dist/analyzer.d.ts +45 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +134 -214
- package/dist/analyzer.js.map +1 -1
- package/dist/browser-host-runtime.d.ts +23 -0
- package/dist/browser-host-runtime.d.ts.map +1 -0
- package/dist/browser-host-runtime.js +29 -0
- package/dist/browser-host-runtime.js.map +1 -0
- package/dist/compiler.d.ts +1 -1
- package/dist/compiler.js +6 -6
- package/dist/compiler.js.map +1 -1
- package/dist/emitter.js +25 -5
- package/dist/emitter.js.map +1 -1
- package/dist/lexer.js +89 -16
- package/dist/lexer.js.map +1 -1
- package/dist/look.d.ts +1 -1
- package/dist/look.d.ts.map +1 -1
- package/dist/look.js +5 -3
- package/dist/look.js.map +1 -1
- package/dist/parser.d.ts +2 -0
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +23 -25
- package/dist/parser.js.map +1 -1
- package/dist/runtime-foundation.d.ts.map +1 -1
- package/dist/runtime-foundation.js +5 -5
- package/dist/runtime-foundation.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +12 -11
- package/dist/runtime.js.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -1
- package/dist/visual-blocks.d.ts +28 -0
- package/dist/visual-blocks.d.ts.map +1 -0
- package/dist/visual-blocks.js +23 -0
- package/dist/visual-blocks.js.map +1 -0
- 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,
|
|
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
|
-
|
|
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 === "-" ?
|
|
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
|
|
2663
|
-
const
|
|
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
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
this.diagnostics.push(diagnostic("VEL5077",
|
|
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 (
|
|
2555
|
+
if (place === null || call.callee.kind !== "IdentifierExpression")
|
|
2700
2556
|
continue;
|
|
2701
2557
|
const writer = this.reactiveWriters.get(call.callee.name) ?? null;
|
|
2702
|
-
|
|
2558
|
+
const written = writer === null ? null : writerWritesPath(writer, place, writes);
|
|
2559
|
+
if (written === null)
|
|
2703
2560
|
continue;
|
|
2704
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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;
|