@wcstack/lint 1.26.0 → 1.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +130 -10
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -89,6 +89,13 @@ var WcsDiagnosticCode = {
|
|
|
89
89
|
TokenUndeclared: "wcs/token-undeclared",
|
|
90
90
|
TokenMisconfigured: "wcs/token-misconfigured",
|
|
91
91
|
NestedAssign: "wcs/nested-assign",
|
|
92
|
+
// --- <wcs-state> script: $watch declaration ---
|
|
93
|
+
// ランタイム(watch/processWatchDeclaration.ts)が raiseError で落とす宣言。
|
|
94
|
+
// 越境 `@` / `$` 始まり / 空キー・空セグメント / 明らかな非関数ハンドラ。
|
|
95
|
+
WatchDeclarationInvalid: "wcs/watch-declaration-invalid",
|
|
96
|
+
// `$watch` のキーが状態定義に存在しない。バインディング側と違い黙って発火しない
|
|
97
|
+
// だけなので気づけない。severity は binding-path-missing に揃える(warning)。
|
|
98
|
+
WatchPathMissing: "wcs/watch-path-missing",
|
|
92
99
|
TypeAnnotation: "wcs/type-annotation",
|
|
93
100
|
TemplateSyntax: "wcs/template-syntax",
|
|
94
101
|
// --- <wcs-state> script: array reactivity hazards ---
|
|
@@ -134,6 +141,8 @@ var builtinFilterMeta = {
|
|
|
134
141
|
mul: { description: "\u4E57\u7B97", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
|
|
135
142
|
div: { description: "\u9664\u7B97", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
|
|
136
143
|
mod: { description: "\u5270\u4F59", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
|
|
144
|
+
abs: { description: "\u7D76\u5BFE\u5024", hasArgs: false, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 0 },
|
|
145
|
+
clamp: { description: "\u7BC4\u56F2\u5185\u306B\u4E38\u3081\u308B (min,max)", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 2, maxArgs: 2, argTypes: ["number", "number"] },
|
|
137
146
|
// 数値フォーマット
|
|
138
147
|
fix: { description: "\u56FA\u5B9A\u5C0F\u6570\u70B9\u8868\u8A18", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
|
|
139
148
|
locale: { description: "\u30ED\u30B1\u30FC\u30EB\u5F62\u5F0F\u3067\u6570\u5024\u30D5\u30A9\u30FC\u30DE\u30C3\u30C8", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["string"] },
|
|
@@ -147,6 +156,8 @@ var builtinFilterMeta = {
|
|
|
147
156
|
pad: { description: "\u30D1\u30C7\u30A3\u30F3\u30B0 (length[,char])", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "string"] },
|
|
148
157
|
rep: { description: "\u7E70\u308A\u8FD4\u3057 (count)", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
|
|
149
158
|
rev: { description: "\u6587\u5B57\u9806\u3092\u53CD\u8EE2", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
|
|
159
|
+
truncate: { description: "\u5207\u308A\u8A70\u3081\u3066\u7701\u7565\u8A18\u53F7 (length[,suffix])", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "string"] },
|
|
160
|
+
join: { description: "\u914D\u5217\u3092\u9023\u7D50 ([separator])", hasArgs: true, resultType: "string", acceptTypes: ["array"], minArgs: 0, maxArgs: 1, argTypes: ["string"] },
|
|
150
161
|
// 数値パース・丸め
|
|
151
162
|
int: { description: "\u6574\u6570\u306B\u30D1\u30FC\u30B9", hasArgs: false, resultType: "number", acceptTypes: ["string", "number"], minArgs: 0, maxArgs: 0 },
|
|
152
163
|
float: { description: "\u6D6E\u52D5\u5C0F\u6570\u70B9\u6570\u306B\u30D1\u30FC\u30B9", hasArgs: false, resultType: "number", acceptTypes: ["string", "number"], minArgs: 0, maxArgs: 0 },
|
|
@@ -154,11 +165,15 @@ var builtinFilterMeta = {
|
|
|
154
165
|
floor: { description: "\u5207\u308A\u4E0B\u3052", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
|
|
155
166
|
ceil: { description: "\u5207\u308A\u4E0A\u3052", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
|
|
156
167
|
percent: { description: "\u30D1\u30FC\u30BB\u30F3\u30C6\u30FC\u30B8\u5F62\u5F0F", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
|
|
168
|
+
// number だけでなく string も受ける。実用チェーンは fix / percent の後ろに繋がり、
|
|
169
|
+
// それらは既に string を返すため(builtinFilters.ts の unit を参照)
|
|
170
|
+
unit: { description: "\u5358\u4F4D\uFF08\u63A5\u5C3E\u8F9E\uFF09\u3092\u4ED8\u52A0", hasArgs: true, resultType: "string", acceptTypes: ["number", "string"], minArgs: 1, maxArgs: 1, argTypes: ["string"] },
|
|
157
171
|
// 日付・時刻
|
|
158
172
|
date: { description: "\u30ED\u30B1\u30FC\u30EB\u5F62\u5F0F\u306E\u65E5\u4ED8", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
|
|
159
173
|
time: { description: "\u30ED\u30B1\u30FC\u30EB\u5F62\u5F0F\u306E\u6642\u523B", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
|
|
160
174
|
datetime: { description: "\u30ED\u30B1\u30FC\u30EB\u5F62\u5F0F\u306E\u65E5\u6642", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
|
|
161
175
|
ymd: { description: "YYYY-MM-DD \u5F62\u5F0F", hasArgs: true, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 1, argTypes: ["string"] },
|
|
176
|
+
hms: { description: "HH:MM:SS \u5F62\u5F0F", hasArgs: true, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 1, argTypes: ["string"] },
|
|
162
177
|
// 真偽値・変換
|
|
163
178
|
falsy: { description: "\u507D\u5024\u304B\u5224\u5B9A", hasArgs: false, resultType: "boolean", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
|
|
164
179
|
truthy: { description: "\u771F\u5024\u304B\u5224\u5B9A", hasArgs: false, resultType: "boolean", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
|
|
@@ -419,6 +434,7 @@ var RESERVED_STREAMS_KEY = "$streams";
|
|
|
419
434
|
var RESERVED_COMMAND_TOKENS_KEY = "$commandTokens";
|
|
420
435
|
var RESERVED_EVENT_TOKENS_KEY = "$eventTokens";
|
|
421
436
|
var RESERVED_LIST_KEYS_KEY = "$listKeys";
|
|
437
|
+
var RESERVED_WATCH_KEY = "$watch";
|
|
422
438
|
function analyzeStatePaths(scriptContent, stateName = "default") {
|
|
423
439
|
const objectContent = extractDefaultExportObject(scriptContent);
|
|
424
440
|
if (!objectContent) return [];
|
|
@@ -452,6 +468,36 @@ function analyzeStatePaths(scriptContent, stateName = "default") {
|
|
|
452
468
|
}
|
|
453
469
|
return paths;
|
|
454
470
|
}
|
|
471
|
+
function analyzeWatchEntries(scriptContent) {
|
|
472
|
+
const root = locateDefaultExportObject(scriptContent);
|
|
473
|
+
if (!root) return [];
|
|
474
|
+
const watchProp = parseTopLevelProperties(root.content).find((p) => p.name === RESERVED_WATCH_KEY);
|
|
475
|
+
if (!watchProp || watchProp.kind !== "data" || !watchProp.value || !isObjectLiteral(watchProp.value) || watchProp.valueStart === void 0) {
|
|
476
|
+
return [];
|
|
477
|
+
}
|
|
478
|
+
const leading = watchProp.value.length - watchProp.value.trimStart().length;
|
|
479
|
+
const innerStart = root.start + watchProp.valueStart + leading + 1;
|
|
480
|
+
const entries = [];
|
|
481
|
+
for (const entry of parseTopLevelProperties(extractObjectContent(watchProp.value))) {
|
|
482
|
+
if (entry.nameStart === void 0 || entry.nameEnd === void 0) continue;
|
|
483
|
+
entries.push({
|
|
484
|
+
key: entry.name,
|
|
485
|
+
start: innerStart + entry.nameStart,
|
|
486
|
+
end: innerStart + entry.nameEnd,
|
|
487
|
+
// メソッド短縮記法は関数。data は値リテラルの形で判定し、識別子参照は疑わない。
|
|
488
|
+
definitelyNotFunction: entry.kind === "data" && isNonFunctionLiteral(entry.value)
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
return entries;
|
|
492
|
+
}
|
|
493
|
+
function isNonFunctionLiteral(value) {
|
|
494
|
+
if (value === void 0) return false;
|
|
495
|
+
const trimmed = value.trim();
|
|
496
|
+
if (trimmed.length === 0) return false;
|
|
497
|
+
const scan = maskCommentsAndStrings(trimmed);
|
|
498
|
+
if (/^(?:async\s+)?function\b/.test(trimmed) || scan.includes("=>")) return false;
|
|
499
|
+
return /^["'`]/.test(trimmed) || /^-?\d/.test(trimmed) || /^(?:true|false|null|undefined)\b/.test(trimmed) || trimmed.startsWith("[") || trimmed.startsWith("{");
|
|
500
|
+
}
|
|
455
501
|
function collectReservedKeyPaths(prop, paths, pendingStreamValues, pendingListKeys, stateName) {
|
|
456
502
|
if (prop.name === RESERVED_STREAMS_KEY && prop.kind === "data" && prop.value && isObjectLiteral(prop.value)) {
|
|
457
503
|
const entries = parseTopLevelProperties(extractObjectContent(prop.value));
|
|
@@ -598,23 +644,29 @@ function inferJsonTypeHint(value) {
|
|
|
598
644
|
if (typeof value === "object") return "object";
|
|
599
645
|
return void 0;
|
|
600
646
|
}
|
|
601
|
-
function
|
|
647
|
+
function locateDefaultExportObject(script) {
|
|
602
648
|
const scan = maskCommentsAndStrings(script);
|
|
603
649
|
const match = scan.match(/export\s+default\s+(?:defineState\s*\(\s*)?(\{)/);
|
|
604
650
|
if (!match) return null;
|
|
605
|
-
const
|
|
606
|
-
return extractBracedContent(script, scan,
|
|
651
|
+
const braceIndex = scan.indexOf(match[1], match.index);
|
|
652
|
+
return { content: extractBracedContent(script, scan, braceIndex), start: braceIndex + 1 };
|
|
653
|
+
}
|
|
654
|
+
function extractDefaultExportObject(script) {
|
|
655
|
+
return locateDefaultExportObject(script)?.content ?? null;
|
|
607
656
|
}
|
|
608
657
|
function parseTopLevelProperties(objectContent) {
|
|
609
658
|
const props = [];
|
|
610
659
|
const scan = maskCommentsAndStrings(objectContent);
|
|
611
|
-
const regex = /(?:(?:get|set)\s+(?:"([^"]+)"|'([^']+)'|([$\w]+))\s*\([^)]*\)\s*\{)|(?:(?:async\s+)?([$\w]+)\s*\([^)]*\)\s*\{)|(?:(?:"([^"]+)"|'([^']+)'|([$\w]+))\s*:\s*)/gd;
|
|
660
|
+
const regex = /(?:(?:get|set)\s+(?:"([^"]+)"|'([^']+)'|([$\w]+))\s*\([^)]*\)\s*\{)|(?:(?:async\s+)?(?:"([^"]+)"|'([^']+)'|([$\w]+))\s*\([^)]*\)\s*\{)|(?:(?:"([^"]+)"|'([^']+)'|([$\w]+))\s*:\s*)/gd;
|
|
612
661
|
let match;
|
|
613
662
|
while ((match = regex.exec(scan)) !== null) {
|
|
614
663
|
const indices = match.indices;
|
|
664
|
+
let nameSpan;
|
|
615
665
|
const nameAt = (group) => {
|
|
616
666
|
const span = indices[group];
|
|
617
|
-
|
|
667
|
+
if (!span) return void 0;
|
|
668
|
+
nameSpan = [span[0], span[1]];
|
|
669
|
+
return objectContent.slice(span[0], span[1]);
|
|
618
670
|
};
|
|
619
671
|
const skipBody = () => {
|
|
620
672
|
const braceStart = match.index + match[0].length - 1;
|
|
@@ -623,23 +675,31 @@ function parseTopLevelProperties(objectContent) {
|
|
|
623
675
|
};
|
|
624
676
|
const accessorName = nameAt(1) ?? nameAt(2) ?? nameAt(3);
|
|
625
677
|
if (accessorName) {
|
|
626
|
-
props.push({ name: accessorName, kind: "getter" });
|
|
678
|
+
props.push({ name: accessorName, kind: "getter", nameStart: nameSpan[0], nameEnd: nameSpan[1] });
|
|
627
679
|
skipBody();
|
|
628
680
|
continue;
|
|
629
681
|
}
|
|
630
|
-
const methodName = nameAt(4);
|
|
682
|
+
const methodName = nameAt(4) ?? nameAt(5) ?? nameAt(6);
|
|
631
683
|
if (methodName) {
|
|
632
|
-
props.push({ name: methodName, kind: "method" });
|
|
684
|
+
props.push({ name: methodName, kind: "method", nameStart: nameSpan[0], nameEnd: nameSpan[1] });
|
|
633
685
|
skipBody();
|
|
634
686
|
continue;
|
|
635
687
|
}
|
|
636
|
-
const propName = nameAt(
|
|
688
|
+
const propName = nameAt(7) ?? nameAt(8) ?? nameAt(9);
|
|
637
689
|
if (propName) {
|
|
638
690
|
const valueStartIndex = match.index + match[0].length;
|
|
639
691
|
const value = extractFullValue(objectContent, scan, valueStartIndex);
|
|
640
692
|
const jsdocType = extractJsDocType(objectContent, match.index);
|
|
641
693
|
const typeHint = jsdocType ?? inferTypeHint(value);
|
|
642
|
-
props.push({
|
|
694
|
+
props.push({
|
|
695
|
+
name: propName,
|
|
696
|
+
kind: "data",
|
|
697
|
+
value,
|
|
698
|
+
typeHint,
|
|
699
|
+
nameStart: nameSpan[0],
|
|
700
|
+
nameEnd: nameSpan[1],
|
|
701
|
+
valueStart: valueStartIndex
|
|
702
|
+
});
|
|
643
703
|
regex.lastIndex = valueStartIndex + value.length;
|
|
644
704
|
}
|
|
645
705
|
}
|
|
@@ -971,6 +1031,11 @@ var ja = {
|
|
|
971
1031
|
wcsTextInfo: (e) => `wcs-text \u30D0\u30A4\u30F3\u30C7\u30A3\u30F3\u30B0: ${e}`,
|
|
972
1032
|
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`,
|
|
973
1033
|
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`,
|
|
1034
|
+
watchKeyCrossState: (k) => `$watch \u306E\u30AD\u30FC "${k}" \u306F\u4ED6\u306E state \u3092\u6307\u3057\u3066\u3044\u307E\u3059\u3002@ \u4ED8\u304D\u306E\u8D8A\u5883 watch \u306F\u4F7F\u3048\u307E\u305B\u3093\uFF08\u81EA state \u306E\u30D1\u30B9\u306E\u307F\uFF09`,
|
|
1035
|
+
watchKeyReserved: (k) => `$watch \u306E\u30AD\u30FC "${k}" \u306F "$" \u3067\u59CB\u3081\u3089\u308C\u307E\u305B\u3093\uFF08\u4E88\u7D04\u540D\u524D\u7A7A\u9593\uFF09`,
|
|
1036
|
+
watchKeyEmptySegment: (k) => `$watch \u306E\u30AD\u30FC "${k}" \u306B\u7A7A\u306E\u30D1\u30B9\u30BB\u30B0\u30E1\u30F3\u30C8\u304C\u3042\u308A\u307E\u3059`,
|
|
1037
|
+
watchHandlerNotFunction: (k) => `$watch \u306E\u30A8\u30F3\u30C8\u30EA "${k}" \u306E\u5024\u306F\u95A2\u6570\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059`,
|
|
1038
|
+
watchPathMissing: (k) => `$watch \u306E\u30AD\u30FC "${k}" \u306F\u72B6\u614B\u5B9A\u7FA9\u306B\u5B58\u5728\u3057\u307E\u305B\u3093\uFF08\u4E00\u5EA6\u3082\u767A\u706B\u3057\u307E\u305B\u3093\uFF09`,
|
|
974
1039
|
typeAnnotationIncompatible: (vt, rt) => `\u578B "${vt}" \u306F @type {${rt}} \u3068\u4E92\u63DB\u6027\u304C\u3042\u308A\u307E\u305B\u3093`,
|
|
975
1040
|
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`,
|
|
976
1041
|
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`,
|
|
@@ -1013,6 +1078,11 @@ var en = {
|
|
|
1013
1078
|
wcsTextInfo: (e) => `wcs-text binding: ${e}`,
|
|
1014
1079
|
moustacheFouc: (e) => `{{ }} outside a <template> causes FOUC (the raw template string is visible before binding). Consider the comment syntax <!--@@:${e}--> instead.`,
|
|
1015
1080
|
nestedAssign: (sp) => `Assigning to a nested property does not trigger a reactive update. Use this["${sp}"] instead.`,
|
|
1081
|
+
watchKeyCrossState: (k) => `$watch key "${k}" targets another state. Cross-state watching with @ is not supported (own paths only)`,
|
|
1082
|
+
watchKeyReserved: (k) => `$watch key "${k}" must not start with "$" (reserved namespace)`,
|
|
1083
|
+
watchKeyEmptySegment: (k) => `$watch key "${k}" has an empty path segment`,
|
|
1084
|
+
watchHandlerNotFunction: (k) => `The value of $watch entry "${k}" must be a function`,
|
|
1085
|
+
watchPathMissing: (k) => `$watch key "${k}" does not exist in the state definition (it will never fire)`,
|
|
1016
1086
|
typeAnnotationIncompatible: (vt, rt) => `Type "${vt}" is not compatible with @type {${rt}}`,
|
|
1017
1087
|
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}).`,
|
|
1018
1088
|
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.`,
|
|
@@ -3078,6 +3148,55 @@ function blankJsComments(code) {
|
|
|
3078
3148
|
return code.replace(/\/\*[\s\S]*?\*\//g, (m) => " ".repeat(m.length)).replace(/(^|[^:])\/\/[^\n]*/g, (m, pre) => pre + " ".repeat(m.length - pre.length));
|
|
3079
3149
|
}
|
|
3080
3150
|
|
|
3151
|
+
// src/service/watchDeclarationValidator.ts
|
|
3152
|
+
var STATE_NAME_SEPARATOR = "@";
|
|
3153
|
+
function validateWatchDeclarations(html, stateTagName = "wcs-state", locale) {
|
|
3154
|
+
const msgs = getMessages(locale);
|
|
3155
|
+
const out = [];
|
|
3156
|
+
for (const block of parseWcsScriptBlocks(html, stateTagName)) {
|
|
3157
|
+
const entries = analyzeWatchEntries(block.content);
|
|
3158
|
+
if (entries.length === 0) continue;
|
|
3159
|
+
const paths = analyzeStatePaths(block.content, block.stateName);
|
|
3160
|
+
const pathSet = new Set(paths.map((p) => p.path));
|
|
3161
|
+
for (const entry of entries) {
|
|
3162
|
+
const diagnostic = validateEntry(entry, pathSet, msgs);
|
|
3163
|
+
if (diagnostic === null) continue;
|
|
3164
|
+
out.push({
|
|
3165
|
+
code: diagnostic.code,
|
|
3166
|
+
start: block.contentStart + entry.start,
|
|
3167
|
+
end: block.contentStart + entry.end,
|
|
3168
|
+
message: diagnostic.message,
|
|
3169
|
+
severity: diagnostic.severity
|
|
3170
|
+
});
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
return out;
|
|
3174
|
+
}
|
|
3175
|
+
function validateEntry(entry, pathSet, msgs) {
|
|
3176
|
+
const { key } = entry;
|
|
3177
|
+
const invalid = (message) => ({ code: WcsDiagnosticCode.WatchDeclarationInvalid, message, severity: "error" });
|
|
3178
|
+
if (key.includes(STATE_NAME_SEPARATOR)) {
|
|
3179
|
+
return invalid(msgs.watchKeyCrossState(key));
|
|
3180
|
+
}
|
|
3181
|
+
if (key.startsWith("$")) {
|
|
3182
|
+
return invalid(msgs.watchKeyReserved(key));
|
|
3183
|
+
}
|
|
3184
|
+
if (key.split(".").some((segment) => segment.length === 0)) {
|
|
3185
|
+
return invalid(msgs.watchKeyEmptySegment(key));
|
|
3186
|
+
}
|
|
3187
|
+
if (entry.definitelyNotFunction) {
|
|
3188
|
+
return invalid(msgs.watchHandlerNotFunction(key));
|
|
3189
|
+
}
|
|
3190
|
+
if (pathSet.size > 0 && !pathSet.has(key)) {
|
|
3191
|
+
return {
|
|
3192
|
+
code: WcsDiagnosticCode.WatchPathMissing,
|
|
3193
|
+
message: msgs.watchPathMissing(key),
|
|
3194
|
+
severity: "warning"
|
|
3195
|
+
};
|
|
3196
|
+
}
|
|
3197
|
+
return null;
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3081
3200
|
// src/core/validateDocument.ts
|
|
3082
3201
|
function validateDocument(text, options = {}) {
|
|
3083
3202
|
const bindAttribute = options.bindAttribute ?? "data-wcs";
|
|
@@ -3089,6 +3208,7 @@ function validateDocument(text, options = {}) {
|
|
|
3089
3208
|
out.push(...validateIoNodes(text, bindAttribute, stateTagName, locale));
|
|
3090
3209
|
out.push(...validateDocumentEnv(text, locale));
|
|
3091
3210
|
out.push(...validateArrayMutations(text, stateTagName, locale));
|
|
3211
|
+
out.push(...validateWatchDeclarations(text, stateTagName, locale));
|
|
3092
3212
|
for (const d of validateStateTypes(text, stateTagName, locale)) {
|
|
3093
3213
|
out.push({ code: WcsDiagnosticCode.TypeAnnotation, start: d.start, end: d.end, message: d.message, severity: d.severity });
|
|
3094
3214
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wcstack/lint",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.0",
|
|
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": {
|