@wcstack/lint 1.22.1 → 1.22.3

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 (2) hide show
  1. package/dist/cli.cjs +139 -24
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -91,6 +91,13 @@ var WcsDiagnosticCode = {
91
91
  NestedAssign: "wcs/nested-assign",
92
92
  TypeAnnotation: "wcs/type-annotation",
93
93
  TemplateSyntax: "wcs/template-syntax",
94
+ // --- <wcs-state> script: array reactivity hazards ---
95
+ // 配列破壊的メソッド呼び出し(push 等 9 種)。Proxy を素通りしリアクティブ更新されない。
96
+ // 同一参照の自己再代入でも要素の追加・削除は反映されない(docs/array-mutation-diagnostic-design.md §3)。
97
+ ArrayMutation: "wcs/array-mutation",
98
+ // 配列インデックスへの直接代入(bracket-only チェーン)。同上。正はドットパス代入。
99
+ // ドットアクセスを含むチェーン代入は NestedAssign の担当(相補・二重報告なし)。
100
+ ArrayIndexAssign: "wcs/array-index-assign",
94
101
  // --- built-in wcs-* tag contract (generated/builtinTags.generated.ts が正本) ---
95
102
  // 未知メンバーへのバインド(プロパティ / command. / eventToken. キー)。黙って無視される。
96
103
  TagMemberUnknown: "wcs/tag-member-unknown",
@@ -223,7 +230,7 @@ function parseWcsScriptBlocks(html, stateTagName = "wcs-state") {
223
230
  continue;
224
231
  }
225
232
  const typeAttr = extractAttribute(scriptMatch.tagContent, "type");
226
- if (typeAttr !== "module") {
233
+ if (typeAttr?.toLowerCase() !== "module") {
227
234
  pos = scriptMatch.end;
228
235
  continue;
229
236
  }
@@ -288,7 +295,7 @@ function parseWcsStateElements(html, stateTagName = "wcs-state") {
288
295
  continue;
289
296
  }
290
297
  const typeAttr = extractAttribute(scriptMatch.tagContent, "type");
291
- if (typeAttr !== "module") {
298
+ if (typeAttr?.toLowerCase() !== "module") {
292
299
  pos = scriptMatch.end;
293
300
  continue;
294
301
  }
@@ -333,7 +340,7 @@ function findScriptJsonById(html, id) {
333
340
  }
334
341
  const typeAttr = extractAttribute(scriptMatch.tagContent, "type");
335
342
  const idAttr = extractAttribute(scriptMatch.tagContent, "id");
336
- if (typeAttr === "application/json" && idAttr === id) {
343
+ if (typeAttr?.toLowerCase() === "application/json" && idAttr === id) {
337
344
  const contentStart = scriptMatch.end;
338
345
  const scriptCloseIdx = findCloseTag(html, contentStart, "script");
339
346
  if (scriptCloseIdx === -1) return null;
@@ -898,6 +905,8 @@ var ja = {
898
905
  moustacheFouc: (e) => `<template> \u5916\u306E {{ }} \u69CB\u6587\u306F FOUC\uFF08\u521D\u671F\u8868\u793A\u6642\u306B\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u6587\u5B57\u5217\u304C\u898B\u3048\u308B\uFF09\u306E\u539F\u56E0\u306B\u306A\u308A\u307E\u3059\u3002<!--@@:${e}--> \u307E\u305F\u306F\u30B3\u30E1\u30F3\u30C8\u69CB\u6587\u306E\u4F7F\u7528\u3092\u691C\u8A0E\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
899
906
  nestedAssign: (sp) => `\u30CD\u30B9\u30C8\u3055\u308C\u305F\u30D7\u30ED\u30D1\u30C6\u30A3\u3078\u306E\u4EE3\u5165\u306F\u30EA\u30A2\u30AF\u30C6\u30A3\u30D6\u66F4\u65B0\u3092\u30C8\u30EA\u30AC\u30FC\u3057\u307E\u305B\u3093\u3002this["${sp}"] \u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
900
907
  typeAnnotationIncompatible: (vt, rt) => `\u578B "${vt}" \u306F @type {${rt}} \u3068\u4E92\u63DB\u6027\u304C\u3042\u308A\u307E\u305B\u3093`,
908
+ arrayMutation: (m, alt) => `\u914D\u5217\u306E\u7834\u58CA\u7684\u30E1\u30BD\u30C3\u30C9 "${m}" \u306F\u30EA\u30A2\u30AF\u30C6\u30A3\u30D6\u66F4\u65B0\u3092\u30C8\u30EA\u30AC\u30FC\u3057\u307E\u305B\u3093\uFF08\u540C\u4E00\u53C2\u7167\u306E\u81EA\u5DF1\u518D\u4EE3\u5165\u3067\u3082\u8981\u7D20\u306E\u8FFD\u52A0\u30FB\u524A\u9664\u306F\u53CD\u6620\u3055\u308C\u307E\u305B\u3093\uFF09\u3002\u975E\u7834\u58CA\u30E1\u30BD\u30C3\u30C9\u3068\u518D\u4EE3\u5165\u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u4F8B: ${alt}\uFF09\u3002`,
909
+ arrayIndexAssign: (sp) => `\u914D\u5217\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u3078\u306E\u76F4\u63A5\u4EE3\u5165\u306F\u30EA\u30A2\u30AF\u30C6\u30A3\u30D6\u66F4\u65B0\u3092\u30C8\u30EA\u30AC\u30FC\u3057\u307E\u305B\u3093\u3002this["${sp}"] \u306E\u3088\u3046\u306A\u30C9\u30C3\u30C8\u30D1\u30B9\u4EE3\u5165\u3001\u307E\u305F\u306F with() \u3068\u518D\u4EE3\u5165\u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
901
910
  tagMemberUnknown: (prop, tag) => `"${prop}" \u306F <${tag}> \u306E wcBindable \u30E1\u30F3\u30D0\u30FC\u3067\u306F\u3042\u308A\u307E\u305B\u3093\uFF08\u672A\u77E5\u30E1\u30F3\u30D0\u30FC\u3078\u306E\u30D0\u30A4\u30F3\u30C9\u306F\u9ED9\u3063\u3066\u7121\u8996\u3055\u308C\u307E\u3059\uFF09`,
902
911
  tagCommandUnknown: (name, tag, declared) => `"${name}" \u306F <${tag}> \u306E command \u3067\u306F\u3042\u308A\u307E\u305B\u3093\uFF08\u5BA3\u8A00\u6E08\u307F: ${declared}\uFF09`,
903
912
  tagEventTokenKeyUnknown: (name, tag, declared) => `eventToken \u306E\u30AD\u30FC "${name}" \u306F <${tag}> \u306E wcBindable \u30D7\u30ED\u30D1\u30C6\u30A3\u3067\u306F\u3042\u308A\u307E\u305B\u3093\u3002\u751F DOM \u30A4\u30D9\u30F3\u30C8\u540D\u306F\u767A\u706B\u3057\u307E\u305B\u3093 \u2014 \u30D7\u30ED\u30D1\u30C6\u30A3\u540D\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u5BA3\u8A00\u6E08\u307F: ${declared}\uFF09`,
@@ -938,6 +947,8 @@ var en = {
938
947
  moustacheFouc: (e) => `{{ }} outside a <template> causes FOUC (the raw template string is visible before binding). Consider the comment syntax <!--@@:${e}--> instead.`,
939
948
  nestedAssign: (sp) => `Assigning to a nested property does not trigger a reactive update. Use this["${sp}"] instead.`,
940
949
  typeAnnotationIncompatible: (vt, rt) => `Type "${vt}" is not compatible with @type {${rt}}`,
950
+ arrayMutation: (m, alt) => `Destructive array method "${m}" does not trigger a reactive update (re-assigning the same reference does not reflect added/removed elements either). Use a non-destructive method with reassignment (e.g. ${alt}).`,
951
+ arrayIndexAssign: (sp) => `Assigning directly to an array index does not trigger a reactive update. Use a dot-path assignment like this["${sp}"], or with() plus reassignment.`,
941
952
  tagMemberUnknown: (prop, tag) => `"${prop}" is not a wcBindable member of <${tag}> (bindings to unknown members are silently ignored)`,
942
953
  tagCommandUnknown: (name, tag, declared) => `"${name}" is not a command of <${tag}> (declared: ${declared})`,
943
954
  tagEventTokenKeyUnknown: (name, tag, declared) => `eventToken key "${name}" is not a wcBindable property of <${tag}>. Raw DOM event names never fire \u2014 use the property name (declared: ${declared})`,
@@ -1502,40 +1513,143 @@ function isValueCompatible(declaredTypes, valueType) {
1502
1513
  return declaredTypes.includes(valueType);
1503
1514
  }
1504
1515
 
1516
+ // src/service/scriptPatterns.ts
1517
+ var ID = String.raw`[\w$]+`;
1518
+ var SUB = String.raw`\s*(?:\?\.)?\s*\[(?!\s*["'])[^\[\]]+\]`;
1519
+ var DOT_SEG = String.raw`\s*\??\.\s*${ID}`;
1520
+ var CHAIN = String.raw`(?:${DOT_SEG}|${SUB})*`;
1521
+ var BRACKETS_ONLY = String.raw`(?:${SUB})+`;
1522
+ var CHAIN_ONE_PLUS = String.raw`(?:${DOT_SEG}|${SUB})+`;
1523
+ var ROOT_DOT = String.raw`\bthis\s*\??\.\s*(${ID})`;
1524
+ var ROOT_BRACKET = String.raw`\bthis\s*(?:\?\.)?\s*\[\s*["']([^"']+)["']\s*\]`;
1525
+ var ASSIGN_TAIL = String.raw`\s*(?:(?:\*\*|<<|>>>|>>|&&|\|\||\?\?|[+\-*/%&|^])?=(?!=)|\+\+|--)`;
1526
+ var PRE_INCDEC = String.raw`(?:\+\+|--)\s*`;
1527
+ function chainToDotted(chain) {
1528
+ const token = new RegExp(String.raw`\s*(?:\??\.\s*(${ID})|(?:\?\.)?\s*\[([^\[\]]+)\])`, "g");
1529
+ let out = "";
1530
+ let match;
1531
+ while ((match = token.exec(chain)) !== null) {
1532
+ if (match[1] !== void 0) {
1533
+ out += `.${match[1]}`;
1534
+ } else {
1535
+ const key = match[2].trim();
1536
+ out += /^\d+$/.test(key) ? `.${key}` : `.<${key}>`;
1537
+ }
1538
+ }
1539
+ return out;
1540
+ }
1541
+ function hasDotSegment(chain) {
1542
+ return /[.]/.test(chain.replace(/\s*(?:\?\.)?\s*\[[^\[\]]+\]/g, ""));
1543
+ }
1544
+ function isApiRoot(root) {
1545
+ return root.startsWith("$");
1546
+ }
1547
+
1505
1548
  // src/service/nestedAssignValidator.ts
1549
+ var NESTED_ASSIGN = new RegExp(`${ROOT_DOT}(${CHAIN_ONE_PLUS})${ASSIGN_TAIL}`, "g");
1550
+ var PRE_NESTED_INCDEC = new RegExp(`${PRE_INCDEC}${ROOT_DOT}(${CHAIN_ONE_PLUS})`, "g");
1506
1551
  function validateNestedAssigns(html, stateTagName = "wcs-state", locale) {
1507
1552
  const msgs = getMessages(locale);
1508
1553
  const blocks = parseWcsScriptBlocks(html, stateTagName);
1509
1554
  const diagnostics = [];
1510
1555
  for (const block of blocks) {
1511
- const blockDiags = findNestedAssigns(block.content, block.contentStart, msgs);
1512
- diagnostics.push(...blockDiags);
1556
+ findNestedAssigns(block.content, block.contentStart, msgs, diagnostics);
1513
1557
  }
1514
1558
  return diagnostics;
1515
1559
  }
1516
- function findNestedAssigns(script, baseOffset, msgs = getMessages()) {
1560
+ function findNestedAssigns(script, baseOffset, msgs, out) {
1561
+ for (const regex of [NESTED_ASSIGN, PRE_NESTED_INCDEC]) {
1562
+ regex.lastIndex = 0;
1563
+ let match;
1564
+ while ((match = regex.exec(script)) !== null) {
1565
+ const [full, topProp, chainPart] = match;
1566
+ if (isApiRoot(topProp)) continue;
1567
+ if (!hasDotSegment(chainPart)) continue;
1568
+ const suggestedPath = topProp + chainToDotted(chainPart);
1569
+ const start = baseOffset + match.index;
1570
+ out.push({
1571
+ start,
1572
+ end: start + full.length,
1573
+ message: msgs.nestedAssign(suggestedPath),
1574
+ severity: "warning"
1575
+ });
1576
+ }
1577
+ }
1578
+ }
1579
+
1580
+ // src/service/arrayMutationValidator.ts
1581
+ var DESTRUCTIVE_METHODS = "push|pop|shift|unshift|splice|sort|reverse|fill|copyWithin";
1582
+ var ALTERNATIVES = {
1583
+ push: (a) => `${a} = ${a}.concat(item)`,
1584
+ unshift: (a) => `${a} = [item, ...${a}]`,
1585
+ pop: (a) => `${a} = ${a}.slice(0, -1)`,
1586
+ shift: (a) => `${a} = ${a}.slice(1)`,
1587
+ splice: (a) => `${a} = ${a}.toSpliced(...)`,
1588
+ sort: (a) => `${a} = ${a}.toSorted(...)`,
1589
+ reverse: (a) => `${a} = ${a}.toReversed()`,
1590
+ fill: (a) => `${a} = ${a}.map(...)`,
1591
+ copyWithin: (a) => `${a} = ${a}.map(...)`
1592
+ };
1593
+ var METHOD_TAIL = String.raw`\s*\??\.\s*(${DESTRUCTIVE_METHODS})(?=\s*\()`;
1594
+ var DOT_ROOT_CALL = new RegExp(`${ROOT_DOT}(${CHAIN})${METHOD_TAIL}`, "g");
1595
+ var BRACKET_ROOT_CALL = new RegExp(`${ROOT_BRACKET}(${CHAIN})${METHOD_TAIL}`, "g");
1596
+ var DOT_INDEX_ASSIGN = new RegExp(`${ROOT_DOT}(${BRACKETS_ONLY})${ASSIGN_TAIL}`, "g");
1597
+ var BRACKET_INDEX_ASSIGN = new RegExp(`${ROOT_BRACKET}(${BRACKETS_ONLY})${ASSIGN_TAIL}`, "g");
1598
+ var PRE_DOT_INDEX = new RegExp(`${PRE_INCDEC}${ROOT_DOT}(${BRACKETS_ONLY})`, "g");
1599
+ var PRE_BRACKET_INDEX = new RegExp(`${PRE_INCDEC}${ROOT_BRACKET}(${BRACKETS_ONLY})`, "g");
1600
+ function toAccessor(path) {
1601
+ return /^[A-Za-z_]\w*$/.test(path) ? `this.${path}` : `this["${path}"]`;
1602
+ }
1603
+ function validateArrayMutations(html, stateTagName = "wcs-state", locale) {
1604
+ const msgs = getMessages(locale);
1605
+ const blocks = parseWcsScriptBlocks(html, stateTagName);
1517
1606
  const diagnostics = [];
1518
- const regex = /\bthis\.(\w+)((?:\.\w+|\[\w+\])+)\s*=[^=]/g;
1519
- let match;
1520
- while ((match = regex.exec(script)) !== null) {
1521
- const fullMatch = match[0];
1522
- const topProp = match[1];
1523
- const chainPart = match[2];
1524
- if (topProp.startsWith("$")) continue;
1525
- if (!/\.\w+/.test(chainPart)) continue;
1526
- const assignStart = baseOffset + match.index;
1527
- const assignEnd = assignStart + fullMatch.length - 1;
1528
- const dotPath = topProp + chainPart.replace(/\[(\w+)\]/g, ".$1").replace(/^\./, "");
1529
- const suggestedPath = topProp + chainPart.replace(/\[(\w+)\]/g, ".$1");
1530
- diagnostics.push({
1531
- start: assignStart,
1532
- end: assignEnd,
1533
- message: msgs.nestedAssign(suggestedPath),
1534
- severity: "warning"
1535
- });
1607
+ for (const block of blocks) {
1608
+ findDestructiveCalls(block.content, block.contentStart, msgs, diagnostics);
1609
+ findIndexAssigns(block.content, block.contentStart, msgs, diagnostics);
1536
1610
  }
1537
1611
  return diagnostics;
1538
1612
  }
1613
+ function findDestructiveCalls(script, baseOffset, msgs, out) {
1614
+ for (const regex of [DOT_ROOT_CALL, BRACKET_ROOT_CALL]) {
1615
+ regex.lastIndex = 0;
1616
+ let match;
1617
+ while ((match = regex.exec(script)) !== null) {
1618
+ const [full, root, chain, method] = match;
1619
+ if (isApiRoot(root)) continue;
1620
+ const statePath = root + chainToDotted(chain);
1621
+ const start = baseOffset + match.index;
1622
+ out.push({
1623
+ code: WcsDiagnosticCode.ArrayMutation,
1624
+ start,
1625
+ end: start + full.length,
1626
+ message: msgs.arrayMutation(method, ALTERNATIVES[method](toAccessor(statePath))),
1627
+ severity: "warning",
1628
+ statePath
1629
+ });
1630
+ }
1631
+ }
1632
+ }
1633
+ function findIndexAssigns(script, baseOffset, msgs, out) {
1634
+ for (const regex of [DOT_INDEX_ASSIGN, BRACKET_INDEX_ASSIGN, PRE_DOT_INDEX, PRE_BRACKET_INDEX]) {
1635
+ regex.lastIndex = 0;
1636
+ let match;
1637
+ while ((match = regex.exec(script)) !== null) {
1638
+ const [full, root, chain] = match;
1639
+ if (isApiRoot(root)) continue;
1640
+ const suggestedPath = root + chainToDotted(chain);
1641
+ const start = baseOffset + match.index;
1642
+ out.push({
1643
+ code: WcsDiagnosticCode.ArrayIndexAssign,
1644
+ start,
1645
+ end: start + full.length,
1646
+ message: msgs.arrayIndexAssign(suggestedPath),
1647
+ severity: "warning",
1648
+ statePath: suggestedPath
1649
+ });
1650
+ }
1651
+ }
1652
+ }
1539
1653
 
1540
1654
  // src/service/templateSyntax.ts
1541
1655
  function findAllMustacheSyntax(html) {
@@ -2859,6 +2973,7 @@ function validateDocument(text, options = {}) {
2859
2973
  out.push(...validateTemplateSyntax(text, stateTagName, bindAttribute, locale));
2860
2974
  out.push(...validateIoNodes(text, bindAttribute, stateTagName, locale));
2861
2975
  out.push(...validateDocumentEnv(text, locale));
2976
+ out.push(...validateArrayMutations(text, stateTagName, locale));
2862
2977
  for (const d of validateStateTypes(text, stateTagName, locale)) {
2863
2978
  out.push({ code: WcsDiagnosticCode.TypeAnnotation, start: d.start, end: d.end, message: d.message, severity: d.severity });
2864
2979
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wcstack/lint",
3
- "version": "1.22.1",
3
+ "version": "1.22.3",
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": {