@wcstack/lint 2.1.0 → 2.1.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/README.md CHANGED
@@ -56,7 +56,7 @@ app.manifest.json:1:3 error wcs/manifest-broken Broken manifest JSON: ...
56
56
 
57
57
  ## Declaring a state contract (`stateSchema`)
58
58
 
59
- Without a contract, a path the validator cannot resolve is only a **warning** (`wcs/binding-path-missing`): `count` may well exist at runtime even when the inline script cannot be read statically. Put an `application` sidecar next to (or above) the HTML and the same typo becomes an **error**:
59
+ Without a contract, a path the validator cannot resolve is only a **warning** (`wcs/binding-path-missing`): `count` may well exist at runtime even when the inline script cannot be read statically. Note that a list starting as `[]` does not need a contract just to name its row fields — the analyzer reads them from the row literals in the assignments that add or replace rows (`this.items = this.items.concat({ id, kind: "general" })`, `.toSpliced(i, n, { … })`, `.with(i, { … })`, `[...this.items, { … }]`); only a row passed as a variable (`concat(row)`) is invisible to it. Put an `application` sidecar next to (or above) the HTML and the same typo becomes an **error**:
60
60
 
61
61
  ```json
62
62
  {
package/dist/cli.cjs CHANGED
@@ -955,6 +955,7 @@ function analyzeStatePaths(scriptContent) {
955
955
  for (const listKeyEntry of pendingListKeys) {
956
956
  pushListKeyPaths(listKeyEntry, paths);
957
957
  }
958
+ collectRowShapesFromAssignments(scriptContent, paths);
958
959
  return paths;
959
960
  }
960
961
  function analyzeWatchEntries(scriptContent) {
@@ -1100,6 +1101,95 @@ function extractStringLiteralValue(value) {
1100
1101
  const match = value.trim().match(/^["']([^"'\\]*)["']$/);
1101
1102
  return match && match[1].length > 0 ? match[1] : null;
1102
1103
  }
1104
+ var ROW_ASSIGN = new RegExp(
1105
+ String.raw`\bthis\s*(?:\.\s*([$\w]+)|\[\s*["']([^"']+)["']\s*\])\s*=(?![=>])\s*(?:(\[)|(?:[^;={}]|=>)*?\.\s*(?:concat|toSpliced|with)\s*(\())`,
1106
+ "gd"
1107
+ );
1108
+ function collectRowShapesFromAssignments(script, paths) {
1109
+ const scan = maskCommentsAndStrings(script);
1110
+ ROW_ASSIGN.lastIndex = 0;
1111
+ let match;
1112
+ while ((match = ROW_ASSIGN.exec(scan)) !== null) {
1113
+ const span = match.indices[1] ?? match.indices[2];
1114
+ const listPath = script.slice(span[0], span[1]);
1115
+ if (listPath.startsWith("$") || listPath.includes("*") || !hasPath(paths, `${listPath}.*`)) continue;
1116
+ const openIndex = match.index + match[0].length - 1;
1117
+ const isArrayLiteral2 = scan[openIndex] === "[";
1118
+ const inner = extractDelimitedContent(script, scan, openIndex, scan[openIndex], isArrayLiteral2 ? "]" : ")");
1119
+ for (const literal of collectRowLiterals(inner, isArrayLiteral2 ? 0 : 1)) {
1120
+ for (const field of extractRowLiteralFields(literal)) {
1121
+ pushRowFieldPaths(`${listPath}.*.${field.name}`, field, paths, 1);
1122
+ }
1123
+ }
1124
+ }
1125
+ }
1126
+ function collectRowLiterals(elementList, arrayDepth) {
1127
+ const out = [];
1128
+ for (const element of splitTopLevelElements(elementList)) {
1129
+ if (element.startsWith("{")) {
1130
+ out.push(element);
1131
+ } else if (element.startsWith("[") && arrayDepth > 0) {
1132
+ const scan = maskCommentsAndStrings(element);
1133
+ out.push(...collectRowLiterals(extractDelimitedContent(element, scan, 0, "[", "]"), arrayDepth - 1));
1134
+ }
1135
+ }
1136
+ return out;
1137
+ }
1138
+ function extractRowLiteralFields(literal) {
1139
+ const content = extractObjectContent(literal);
1140
+ const fields = parseTopLevelProperties(content).filter((p) => p.kind === "data");
1141
+ for (const element of splitTopLevelElements(content)) {
1142
+ const shorthand = /^([$\w]+)$/.exec(element);
1143
+ if (shorthand && !fields.some((f) => f.name === shorthand[1])) {
1144
+ fields.push({ name: shorthand[1], kind: "data" });
1145
+ }
1146
+ }
1147
+ return fields;
1148
+ }
1149
+ function pushRowFieldPaths(path, prop, paths, depth) {
1150
+ if (!hasPath(paths, path)) paths.push(withHint({ path, kind: "data" }, prop.typeHint));
1151
+ if (!prop.value) return;
1152
+ if (isArrayLiteral(prop.value)) {
1153
+ if (!hasPath(paths, `${path}.*`)) paths.push({ path: `${path}.*`, kind: "list" });
1154
+ if (!hasPath(paths, `${path}.length`)) {
1155
+ paths.push({ path: `${path}.length`, kind: "data", typeHint: "number" });
1156
+ }
1157
+ if (depth >= MAX_OBJECT_NEST_DEPTH) return;
1158
+ for (const child of extractArrayElementDataProperties(prop.value)) {
1159
+ pushRowFieldPaths(`${path}.*.${child.name}`, child, paths, depth + 1);
1160
+ }
1161
+ return;
1162
+ }
1163
+ if (isObjectLiteral(prop.value)) {
1164
+ if (depth >= MAX_OBJECT_NEST_DEPTH) return;
1165
+ for (const child of parseTopLevelProperties(extractObjectContent(prop.value))) {
1166
+ if (child.kind !== "data") continue;
1167
+ pushRowFieldPaths(`${path}.${child.name}`, child, paths, depth + 1);
1168
+ }
1169
+ }
1170
+ }
1171
+ function hasPath(paths, path) {
1172
+ return paths.some((p) => p.path === path);
1173
+ }
1174
+ function splitTopLevelElements(text) {
1175
+ const scan = maskCommentsAndStrings(text);
1176
+ const out = [];
1177
+ let depth = 0;
1178
+ let start = 0;
1179
+ for (let i = 0; i < scan.length; i++) {
1180
+ const ch = scan[i];
1181
+ if (ch === "{" || ch === "[" || ch === "(") {
1182
+ depth++;
1183
+ } else if (ch === "}" || ch === "]" || ch === ")") {
1184
+ depth--;
1185
+ } else if (ch === "," && depth === 0) {
1186
+ out.push(text.slice(start, i));
1187
+ start = i + 1;
1188
+ }
1189
+ }
1190
+ out.push(text.slice(start));
1191
+ return out.map((e) => e.trim()).filter((e) => e.length > 0);
1192
+ }
1103
1193
  function findStreamInitialProperty(entryValue) {
1104
1194
  const defProps = parseTopLevelProperties(extractObjectContent(entryValue));
1105
1195
  return defProps.find((p) => p.kind === "data" && p.name === "initial");
@@ -1321,9 +1411,12 @@ function extractFullValue(content, scan, startIndex) {
1321
1411
  return content.slice(startIndex, i).trim();
1322
1412
  }
1323
1413
  function extractBracedContent(text, scan, openBraceIndex) {
1414
+ return extractDelimitedContent(text, scan, openBraceIndex, "{", "}");
1415
+ }
1416
+ function extractDelimitedContent(text, scan, openIndex, open, close) {
1324
1417
  let depth = 0;
1325
1418
  let inString = null;
1326
- for (let i = openBraceIndex; i < scan.length; i++) {
1419
+ for (let i = openIndex; i < scan.length; i++) {
1327
1420
  const ch = scan[i];
1328
1421
  if (inString) {
1329
1422
  if (ch === inString && !isEscaped(scan, i)) {
@@ -1333,16 +1426,16 @@ function extractBracedContent(text, scan, openBraceIndex) {
1333
1426
  }
1334
1427
  if (ch === '"' || ch === "'" || ch === "`") {
1335
1428
  inString = ch;
1336
- } else if (ch === "{") {
1429
+ } else if (ch === open) {
1337
1430
  depth++;
1338
- } else if (ch === "}") {
1431
+ } else if (ch === close) {
1339
1432
  depth--;
1340
1433
  if (depth === 0) {
1341
- return text.slice(openBraceIndex + 1, i);
1434
+ return text.slice(openIndex + 1, i);
1342
1435
  }
1343
1436
  }
1344
1437
  }
1345
- return text.slice(openBraceIndex + 1);
1438
+ return text.slice(openIndex + 1);
1346
1439
  }
1347
1440
  function isArrayLiteral(value) {
1348
1441
  return value.trimStart().startsWith("[");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wcstack/lint",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "Static-contract validator CLI (wcs-validate) for wcstack data-wcs bindings and wcstack.manifest.json sidecars. Thin npm wrapper around the wcstack-intellisense validator core.",
5
5
  "type": "module",
6
6
  "bin": {