@fairfox/polly 0.15.1 → 0.15.4
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/tools/analysis/src/extract/handlers.d.ts +72 -0
- package/dist/tools/teach/src/cli.js +375 -35
- package/dist/tools/teach/src/cli.js.map +3 -3
- package/dist/tools/teach/src/index.js +375 -35
- package/dist/tools/teach/src/index.js.map +3 -3
- package/dist/tools/verify/src/cli.js +521 -73
- package/dist/tools/verify/src/cli.js.map +4 -4
- package/dist/tools/verify/src/config.d.ts +1 -1
- package/dist/tools/verify/src/config.js +21 -1
- package/dist/tools/verify/src/config.js.map +3 -3
- package/dist/tools/visualize/src/cli.js +375 -35
- package/dist/tools/visualize/src/cli.js.map +3 -3
- package/package.json +1 -1
|
@@ -61,4 +61,4 @@ type UnifiedVerificationConfig = LegacyVerificationConfig | AdapterVerificationC
|
|
|
61
61
|
* ```
|
|
62
62
|
*/
|
|
63
63
|
export declare function defineVerification<T extends UnifiedVerificationConfig>(config: T): T;
|
|
64
|
-
export { $constraints } from "./primitives/index.js";
|
|
64
|
+
export { $constraints, ensures, hasLength, inRange, oneOf, requires } from "./primitives/index.js";
|
|
@@ -49,6 +49,21 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
// tools/verify/src/primitives/index.ts
|
|
52
|
+
function requires(condition, message) {}
|
|
53
|
+
function ensures(condition, message) {}
|
|
54
|
+
function inRange(value, min, max) {
|
|
55
|
+
return value >= min && value <= max;
|
|
56
|
+
}
|
|
57
|
+
function oneOf(value, allowed) {
|
|
58
|
+
return allowed.includes(value);
|
|
59
|
+
}
|
|
60
|
+
function hasLength(array, constraint) {
|
|
61
|
+
if (constraint.min !== undefined && array.length < constraint.min)
|
|
62
|
+
return false;
|
|
63
|
+
if (constraint.max !== undefined && array.length > constraint.max)
|
|
64
|
+
return false;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
52
67
|
function $constraints(stateField, constraints, options) {
|
|
53
68
|
if (options?.runtime) {
|
|
54
69
|
import("../../../src/shared/lib/constraints.js").then(({ registerConstraints }) => {
|
|
@@ -79,8 +94,13 @@ function defineVerification(config) {
|
|
|
79
94
|
return config;
|
|
80
95
|
}
|
|
81
96
|
export {
|
|
97
|
+
requires,
|
|
98
|
+
oneOf,
|
|
99
|
+
inRange,
|
|
100
|
+
hasLength,
|
|
101
|
+
ensures,
|
|
82
102
|
defineVerification,
|
|
83
103
|
$constraints
|
|
84
104
|
};
|
|
85
105
|
|
|
86
|
-
//# debugId=
|
|
106
|
+
//# debugId=477FC1CD67CF195864756E2164756E21
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"sources": ["../tools/verify/src/primitives/index.ts", "../tools/verify/src/config.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"// Verification primitives for formal verification\n// These are runtime no-ops but extracted during verification\n\n/**\n * Assert a precondition that must be true when the handler executes.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ assertion\n *\n * @example\n * messageBus.on(\"USER_LOGIN\", (payload) => {\n * requires(state.user.loggedIn === false, \"User must not be logged in\")\n * state.user.loggedIn = true\n * })\n */\nexport function requires(condition: boolean, message?: string): void {\n // Runtime no-op - only used during verification\n // Condition and message are checked during static analysis only\n void condition;\n void message;\n}\n\n/**\n * Assert a postcondition that must be true after the handler completes.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ assertion\n *\n * @example\n * messageBus.on(\"USER_LOGIN\", (payload) => {\n * state.user.loggedIn = true\n * ensures(state.user.loggedIn === true, \"User must be logged in\")\n * })\n */\nexport function ensures(condition: boolean, message?: string): void {\n // Runtime no-op - only used during verification\n // Condition and message are checked during static analysis only\n void condition;\n void message;\n}\n\n/**\n * Define a global invariant that must always hold.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ invariant\n *\n * @example\n * invariant(\"UserIdConsistent\", () =>\n * state.user.loggedIn === false || state.user.id !== null\n * )\n */\nexport function invariant(_name: string, condition: () => boolean): void {\n // Runtime no-op - only used during verification\n // Name and condition are checked during static analysis only\n void condition;\n}\n\n/**\n * Assert that a value is within a valid range.\n *\n * @example\n * requires(inRange(todoCount, 0, 100), \"Todo count must be 0-100\")\n */\nexport function inRange(value: number, min: number, max: number): boolean {\n return value >= min && value <= max;\n}\n\n/**\n * Assert that a value is one of the allowed values.\n *\n * @example\n * requires(oneOf(state.user.role, [\"admin\", \"user\"]), \"Role must be admin or user\")\n */\nexport function oneOf<T>(value: T, allowed: T[]): boolean {\n return allowed.includes(value);\n}\n\n/**\n * Assert that an array has a specific length constraint.\n *\n * @example\n * requires(hasLength(state.todos, { max: 10 }), \"Too many todos\")\n */\nexport function hasLength(array: unknown[], constraint: { min?: number; max?: number }): boolean {\n if (constraint.min !== undefined && array.length < constraint.min) return false;\n if (constraint.max !== undefined && array.length > constraint.max) return false;\n return true;\n}\n\n/**\n * Declare state-level constraints for verification and optional runtime checking.\n * Maps message types to preconditions on state fields.\n *\n * The parser automatically wires these constraints to handlers during verification.\n * Optionally, constraints can be enforced at runtime by passing `{ runtime: true }`.\n *\n * @example\n * // Verification only (TLA+ generation)\n * const state = { loggedIn: false };\n *\n * $constraints(\"loggedIn\", {\n * USER_LOGOUT: { requires: \"state.loggedIn === true\", message: \"Must be logged in\" },\n * BOOKMARK_ADD: { requires: \"state.loggedIn === true\", message: \"Must be logged in\" },\n * });\n *\n * @example\n * // Runtime enforcement (function predicates)\n * $constraints(\"loggedIn\", {\n * USER_LOGOUT: {\n * requires: (state) => state.loggedIn === true,\n * message: \"Must be logged in to logout\"\n * },\n * }, { runtime: true });\n */\nexport function $constraints(\n stateField: string,\n constraints: Record<\n string,\n {\n requires?: string | ((state: unknown) => boolean);\n ensures?: string | ((state: unknown) => boolean);\n message?: string;\n }\n >,\n options?: { runtime?: boolean }\n): void {\n // Register constraints for runtime checking if enabled\n if (options?.runtime) {\n // Import dynamically to avoid circular dependencies\n // This is safe because it only happens at runtime, not during static analysis\n // @ts-expect-error - Dynamic import path resolves correctly at runtime\n import(\"../../../src/shared/lib/constraints.js\")\n .then(({ registerConstraints }) => {\n registerConstraints(stateField, constraints);\n })\n .catch(() => {\n // Silently ignore - constraints module may not be available during static analysis\n });\n }\n\n // For verification: Still a no-op at runtime\n // Parser extracts these and wires them to TLA+ handlers\n}\n\n// Re-export for convenience\nexport const verify = {\n requires,\n ensures,\n invariant,\n inRange,\n oneOf,\n hasLength,\n $constraints,\n};\n",
|
|
6
|
-
"// ═══════════════════════════════════════════════════════════════\n// Configuration Helper for @fairfox/polly/verify\n// ═══════════════════════════════════════════════════════════════\n//\n// Lightweight entry point for user configuration files.\n// Does NOT include heavy dependencies (ts-morph, analysis, etc.)\n// which are only needed by the CLI tool.\n\n// ─────────────────────────────────────────────────────────────────\n// Configuration Types (inlined to avoid heavy dependencies)\n// ─────────────────────────────────────────────────────────────────\n\n// Legacy verification configuration\ninterface LegacyVerificationConfig {\n state: Record<string, unknown>;\n messages: {\n // Basic bounds\n maxInFlight?: number;\n maxTabs?: number;\n maxClients?: number;\n maxRenderers?: number;\n maxWorkers?: number;\n maxContexts?: number;\n\n // Tier 1 Optimizations (no precision loss)\n include?: string[]; // Only verify these message types\n exclude?: string[]; // Exclude these message types (mutually exclusive with include)\n symmetry?: string[][]; // Groups of symmetric message types [[type1, type2], [type3, type4]]\n perMessageBounds?: Record<string, number>; // Different maxInFlight per message type\n };\n onBuild?: \"warn\" | \"error\" | \"off\";\n onRelease?: \"warn\" | \"error\" | \"off\";\n\n // Verification engine options\n verification?: {\n timeout?: number; // Timeout in seconds (0 = no timeout)\n workers?: number; // Number of TLC workers\n };\n\n // Tier 2 Optimizations (controlled approximations)\n tier2?: {\n // Temporal constraints: ordering requirements between messages\n temporalConstraints?: Array<{\n before: string; // Message type that must occur first\n after: string; // Message type that must occur after\n description?: string; // Human-readable description\n }>;\n\n // Bounded exploration: limit depth for specific scenarios\n boundedExploration?: {\n maxDepth?: number; // Maximum state depth to explore\n criticalPaths?: string[][]; // Sequences of message types that must be fully explored\n };\n };\n}\n\n// Adapter-based configuration (for future use)\ninterface AdapterVerificationConfig {\n adapter: unknown; // Adapter interface not exported to avoid heavy deps\n state: Record<string, unknown>;\n bounds?: {\n maxInFlight?: number;\n [key: string]: unknown;\n };\n onBuild?: \"warn\" | \"error\" | \"off\";\n}\n\n// Union type for both config formats\ntype UnifiedVerificationConfig = LegacyVerificationConfig | AdapterVerificationConfig;\n\n/**\n * Define verification configuration with type checking\n *\n * Used in generated verification.config.ts files.\n *\n * @example\n * ```typescript\n * import { defineVerification } from '@fairfox/polly/verify'\n *\n * export default defineVerification({\n * state: {\n * \"user.role\": { type: \"enum\", values: [\"admin\", \"user\", \"guest\"] },\n * },\n * messages: {\n * maxInFlight: 6,\n * maxTabs: 2,\n * },\n * })\n * ```\n */\nexport function defineVerification<T extends UnifiedVerificationConfig>(config: T): T {\n // Validate configuration structure\n if (\"adapter\" in config) {\n // New adapter-based format\n if (!config.adapter) {\n throw new Error(\"Configuration must include an adapter\");\n }\n if (!config.state) {\n throw new Error(\"Configuration must include state bounds\");\n }\n } else if (\"messages\" in config) {\n // Legacy format\n if (!config.state) {\n throw new Error(\"Configuration must include state bounds\");\n }\n if (!config.messages) {\n throw new Error(\"Legacy configuration must include messages bounds\");\n }\n } else {\n throw new Error(\n \"Invalid configuration format. Must include either 'adapter' (new format) or 'messages' (legacy format)\"\n );\n }\n\n return config;\n}\n\n// Re-export
|
|
6
|
+
"// ═══════════════════════════════════════════════════════════════\n// Configuration Helper for @fairfox/polly/verify\n// ═══════════════════════════════════════════════════════════════\n//\n// Lightweight entry point for user configuration files.\n// Does NOT include heavy dependencies (ts-morph, analysis, etc.)\n// which are only needed by the CLI tool.\n\n// ─────────────────────────────────────────────────────────────────\n// Configuration Types (inlined to avoid heavy dependencies)\n// ─────────────────────────────────────────────────────────────────\n\n// Legacy verification configuration\ninterface LegacyVerificationConfig {\n state: Record<string, unknown>;\n messages: {\n // Basic bounds\n maxInFlight?: number;\n maxTabs?: number;\n maxClients?: number;\n maxRenderers?: number;\n maxWorkers?: number;\n maxContexts?: number;\n\n // Tier 1 Optimizations (no precision loss)\n include?: string[]; // Only verify these message types\n exclude?: string[]; // Exclude these message types (mutually exclusive with include)\n symmetry?: string[][]; // Groups of symmetric message types [[type1, type2], [type3, type4]]\n perMessageBounds?: Record<string, number>; // Different maxInFlight per message type\n };\n onBuild?: \"warn\" | \"error\" | \"off\";\n onRelease?: \"warn\" | \"error\" | \"off\";\n\n // Verification engine options\n verification?: {\n timeout?: number; // Timeout in seconds (0 = no timeout)\n workers?: number; // Number of TLC workers\n };\n\n // Tier 2 Optimizations (controlled approximations)\n tier2?: {\n // Temporal constraints: ordering requirements between messages\n temporalConstraints?: Array<{\n before: string; // Message type that must occur first\n after: string; // Message type that must occur after\n description?: string; // Human-readable description\n }>;\n\n // Bounded exploration: limit depth for specific scenarios\n boundedExploration?: {\n maxDepth?: number; // Maximum state depth to explore\n criticalPaths?: string[][]; // Sequences of message types that must be fully explored\n };\n };\n}\n\n// Adapter-based configuration (for future use)\ninterface AdapterVerificationConfig {\n adapter: unknown; // Adapter interface not exported to avoid heavy deps\n state: Record<string, unknown>;\n bounds?: {\n maxInFlight?: number;\n [key: string]: unknown;\n };\n onBuild?: \"warn\" | \"error\" | \"off\";\n}\n\n// Union type for both config formats\ntype UnifiedVerificationConfig = LegacyVerificationConfig | AdapterVerificationConfig;\n\n/**\n * Define verification configuration with type checking\n *\n * Used in generated verification.config.ts files.\n *\n * @example\n * ```typescript\n * import { defineVerification } from '@fairfox/polly/verify'\n *\n * export default defineVerification({\n * state: {\n * \"user.role\": { type: \"enum\", values: [\"admin\", \"user\", \"guest\"] },\n * },\n * messages: {\n * maxInFlight: 6,\n * maxTabs: 2,\n * },\n * })\n * ```\n */\nexport function defineVerification<T extends UnifiedVerificationConfig>(config: T): T {\n // Validate configuration structure\n if (\"adapter\" in config) {\n // New adapter-based format\n if (!config.adapter) {\n throw new Error(\"Configuration must include an adapter\");\n }\n if (!config.state) {\n throw new Error(\"Configuration must include state bounds\");\n }\n } else if (\"messages\" in config) {\n // Legacy format\n if (!config.state) {\n throw new Error(\"Configuration must include state bounds\");\n }\n if (!config.messages) {\n throw new Error(\"Legacy configuration must include messages bounds\");\n }\n } else {\n throw new Error(\n \"Invalid configuration format. Must include either 'adapter' (new format) or 'messages' (legacy format)\"\n );\n }\n\n return config;\n}\n\n// Re-export verification primitives for user code\nexport { $constraints, ensures, hasLength, inRange, oneOf, requires } from \"./primitives/index.js\";\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAeO,SAAS,QAAQ,CAAC,WAAoB,SAAwB;AAmB9D,SAAS,OAAO,CAAC,WAAoB,SAAwB;AA8B7D,SAAS,OAAO,CAAC,OAAe,KAAa,KAAsB;AAAA,EACxE,OAAO,SAAS,OAAO,SAAS;AAAA;AAS3B,SAAS,KAAQ,CAAC,OAAU,SAAuB;AAAA,EACxD,OAAO,QAAQ,SAAS,KAAK;AAAA;AASxB,SAAS,SAAS,CAAC,OAAkB,YAAqD;AAAA,EAC/F,IAAI,WAAW,QAAQ,aAAa,MAAM,SAAS,WAAW;AAAA,IAAK,OAAO;AAAA,EAC1E,IAAI,WAAW,QAAQ,aAAa,MAAM,SAAS,WAAW;AAAA,IAAK,OAAO;AAAA,EAC1E,OAAO;AAAA;AA4BF,SAAS,YAAY,CAC1B,YACA,aAQA,SACM;AAAA,EAEN,IAAI,SAAS,SAAS;AAAA,IAIb,iDACJ,KAAK,GAAG,0BAA0B;AAAA,MACjC,oBAAoB,YAAY,WAAW;AAAA,KAC5C,EACA,MAAM,MAAM,EAEZ;AAAA,EACL;AAAA;;;ACjDK,SAAS,kBAAuD,CAAC,QAAc;AAAA,EAEpF,IAAI,aAAa,QAAQ;AAAA,IAEvB,IAAI,CAAC,OAAO,SAAS;AAAA,MACnB,MAAM,IAAI,MAAM,uCAAuC;AAAA,IACzD;AAAA,IACA,IAAI,CAAC,OAAO,OAAO;AAAA,MACjB,MAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAAA,EACF,EAAO,SAAI,cAAc,QAAQ;AAAA,IAE/B,IAAI,CAAC,OAAO,OAAO;AAAA,MACjB,MAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAAA,IACA,IAAI,CAAC,OAAO,UAAU;AAAA,MACpB,MAAM,IAAI,MAAM,mDAAmD;AAAA,IACrE;AAAA,EACF,EAAO;AAAA,IACL,MAAM,IAAI,MACR,wGACF;AAAA;AAAA,EAGF,OAAO;AAAA;",
|
|
9
|
+
"debugId": "477FC1CD67CF195864756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -1598,6 +1598,7 @@ class HandlerExtractor {
|
|
|
1598
1598
|
relationshipExtractor;
|
|
1599
1599
|
analyzedFiles;
|
|
1600
1600
|
packageRoot;
|
|
1601
|
+
warnings;
|
|
1601
1602
|
constructor(tsConfigPath) {
|
|
1602
1603
|
this.project = new Project3({
|
|
1603
1604
|
tsConfigFilePath: tsConfigPath
|
|
@@ -1605,8 +1606,27 @@ class HandlerExtractor {
|
|
|
1605
1606
|
this.typeGuardCache = new WeakMap;
|
|
1606
1607
|
this.relationshipExtractor = new RelationshipExtractor;
|
|
1607
1608
|
this.analyzedFiles = new Set;
|
|
1609
|
+
this.warnings = [];
|
|
1608
1610
|
this.packageRoot = this.findPackageRoot(tsConfigPath);
|
|
1609
1611
|
}
|
|
1612
|
+
warnUnsupportedPattern(pattern, location, suggestion) {
|
|
1613
|
+
const exists = this.warnings.some((w) => w.pattern === pattern && w.location === location);
|
|
1614
|
+
if (!exists) {
|
|
1615
|
+
this.warnings.push({
|
|
1616
|
+
type: "unsupported_pattern",
|
|
1617
|
+
pattern,
|
|
1618
|
+
location,
|
|
1619
|
+
suggestion
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
getNodeLocation(node) {
|
|
1624
|
+
const sourceFile = node.getSourceFile();
|
|
1625
|
+
const lineAndCol = sourceFile.getLineAndColumnAtPos(node.getStart());
|
|
1626
|
+
const filePath = sourceFile.getFilePath();
|
|
1627
|
+
const relativePath = filePath.startsWith(this.packageRoot) ? filePath.substring(this.packageRoot.length + 1) : filePath;
|
|
1628
|
+
return `${relativePath}:${lineAndCol.line}`;
|
|
1629
|
+
}
|
|
1610
1630
|
findPackageRoot(tsConfigPath) {
|
|
1611
1631
|
let dir = tsConfigPath.substring(0, tsConfigPath.lastIndexOf("/"));
|
|
1612
1632
|
while (dir.length > 1) {
|
|
@@ -1630,6 +1650,7 @@ class HandlerExtractor {
|
|
|
1630
1650
|
const invalidMessageTypes = new Set;
|
|
1631
1651
|
const stateConstraints = [];
|
|
1632
1652
|
const verifiedStates = [];
|
|
1653
|
+
this.warnings = [];
|
|
1633
1654
|
const allSourceFiles = this.project.getSourceFiles();
|
|
1634
1655
|
const entryPoints = allSourceFiles.filter((f) => this.isWithinPackage(f.getFilePath()));
|
|
1635
1656
|
this.debugLogSourceFiles(allSourceFiles, entryPoints);
|
|
@@ -1664,7 +1685,8 @@ class HandlerExtractor {
|
|
|
1664
1685
|
handlers,
|
|
1665
1686
|
messageTypes,
|
|
1666
1687
|
stateConstraints,
|
|
1667
|
-
verifiedStates
|
|
1688
|
+
verifiedStates,
|
|
1689
|
+
warnings: this.warnings
|
|
1668
1690
|
};
|
|
1669
1691
|
}
|
|
1670
1692
|
analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates) {
|
|
@@ -1894,46 +1916,364 @@ class HandlerExtractor {
|
|
|
1894
1916
|
if (!Node4.isPropertyAccessExpression(left))
|
|
1895
1917
|
return;
|
|
1896
1918
|
const fieldPath = this.getPropertyPath(left);
|
|
1897
|
-
if (
|
|
1898
|
-
const field = fieldPath.substring(6);
|
|
1899
|
-
const value = this.extractValue(right);
|
|
1900
|
-
if (value !== undefined) {
|
|
1901
|
-
assignments.push({ field, value });
|
|
1902
|
-
}
|
|
1919
|
+
if (this.tryExtractStateFieldPattern(fieldPath, right, assignments))
|
|
1903
1920
|
return;
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
if (
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1921
|
+
if (this.tryExtractSignalNestedFieldPattern(fieldPath, right, assignments))
|
|
1922
|
+
return;
|
|
1923
|
+
if (this.tryExtractSignalObjectPattern(fieldPath, right, assignments))
|
|
1924
|
+
return;
|
|
1925
|
+
if (this.tryExtractSignalArrayPattern(fieldPath, right, assignments))
|
|
1926
|
+
return;
|
|
1927
|
+
if (this.tryExtractSignalMethodPattern(fieldPath, right, assignments))
|
|
1928
|
+
return;
|
|
1929
|
+
if (this.tryExtractSetConstructorPattern(fieldPath, right, assignments))
|
|
1912
1930
|
return;
|
|
1931
|
+
this.tryExtractMapConstructorPattern(fieldPath, right, assignments);
|
|
1932
|
+
}
|
|
1933
|
+
tryExtractStateFieldPattern(fieldPath, right, assignments) {
|
|
1934
|
+
if (!fieldPath.startsWith("state."))
|
|
1935
|
+
return false;
|
|
1936
|
+
const field = fieldPath.substring(6);
|
|
1937
|
+
const value = this.extractValue(right);
|
|
1938
|
+
if (value !== undefined) {
|
|
1939
|
+
assignments.push({ field, value });
|
|
1913
1940
|
}
|
|
1914
|
-
|
|
1915
|
-
|
|
1941
|
+
return true;
|
|
1942
|
+
}
|
|
1943
|
+
tryExtractSignalNestedFieldPattern(fieldPath, right, assignments) {
|
|
1944
|
+
const valueFieldMatch = fieldPath.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\.value\.(.+)$/);
|
|
1945
|
+
if (!valueFieldMatch?.[1] || !valueFieldMatch?.[2])
|
|
1946
|
+
return false;
|
|
1947
|
+
const signalName = valueFieldMatch[1];
|
|
1948
|
+
const fieldName = valueFieldMatch[2];
|
|
1949
|
+
const value = this.extractValue(right);
|
|
1950
|
+
if (value !== undefined) {
|
|
1951
|
+
assignments.push({ field: `${signalName}_${fieldName}`, value });
|
|
1916
1952
|
}
|
|
1953
|
+
return true;
|
|
1917
1954
|
}
|
|
1918
|
-
|
|
1919
|
-
if (!Node4.isObjectLiteralExpression(
|
|
1955
|
+
tryExtractSignalObjectPattern(fieldPath, right, assignments) {
|
|
1956
|
+
if (!fieldPath.endsWith(".value") || !Node4.isObjectLiteralExpression(right))
|
|
1957
|
+
return false;
|
|
1958
|
+
const signalName = fieldPath.slice(0, -6);
|
|
1959
|
+
if (this.isSpreadUpdatePattern(right, fieldPath)) {
|
|
1960
|
+
this.extractSpreadUpdateAssignments(right, assignments, signalName);
|
|
1961
|
+
} else {
|
|
1962
|
+
this.extractObjectLiteralAssignments(right, assignments, signalName);
|
|
1963
|
+
}
|
|
1964
|
+
return true;
|
|
1965
|
+
}
|
|
1966
|
+
isSpreadUpdatePattern(objectLiteral, fieldPath) {
|
|
1967
|
+
const properties = objectLiteral.getProperties();
|
|
1968
|
+
if (properties.length === 0)
|
|
1969
|
+
return false;
|
|
1970
|
+
const firstProp = properties[0];
|
|
1971
|
+
if (!firstProp || !Node4.isSpreadAssignment(firstProp))
|
|
1972
|
+
return false;
|
|
1973
|
+
const spreadExpr = firstProp.getExpression();
|
|
1974
|
+
if (!spreadExpr)
|
|
1975
|
+
return false;
|
|
1976
|
+
return this.getPropertyPath(spreadExpr) === fieldPath;
|
|
1977
|
+
}
|
|
1978
|
+
tryExtractSignalArrayPattern(fieldPath, right, assignments) {
|
|
1979
|
+
if (!fieldPath.endsWith(".value") || !Node4.isArrayLiteralExpression(right))
|
|
1980
|
+
return false;
|
|
1981
|
+
const signalName = fieldPath.slice(0, -6);
|
|
1982
|
+
const arrayAssignment = this.extractArraySpreadOperation(right, fieldPath, signalName);
|
|
1983
|
+
if (arrayAssignment) {
|
|
1984
|
+
assignments.push(arrayAssignment);
|
|
1985
|
+
}
|
|
1986
|
+
return true;
|
|
1987
|
+
}
|
|
1988
|
+
tryExtractSignalMethodPattern(fieldPath, right, assignments) {
|
|
1989
|
+
if (!fieldPath.endsWith(".value") || !Node4.isCallExpression(right))
|
|
1990
|
+
return false;
|
|
1991
|
+
const signalName = fieldPath.slice(0, -6);
|
|
1992
|
+
this.checkForMutatingCollectionMethods(right);
|
|
1993
|
+
const methodAssignment = this.extractArrayMethodOperation(right, fieldPath, signalName);
|
|
1994
|
+
if (methodAssignment) {
|
|
1995
|
+
assignments.push(methodAssignment);
|
|
1996
|
+
}
|
|
1997
|
+
return true;
|
|
1998
|
+
}
|
|
1999
|
+
checkForMutatingCollectionMethods(callExpr) {
|
|
2000
|
+
const expression = callExpr.getExpression();
|
|
2001
|
+
if (!Node4.isPropertyAccessExpression(expression))
|
|
1920
2002
|
return;
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
2003
|
+
const methodName = expression.getName();
|
|
2004
|
+
const sourceExpr = expression.getExpression();
|
|
2005
|
+
const setMethods = ["add", "delete", "clear"];
|
|
2006
|
+
if (setMethods.includes(methodName)) {
|
|
2007
|
+
const sourceText = sourceExpr.getText();
|
|
2008
|
+
if (sourceText.includes("Set") || this.looksLikeSetOrMap(sourceExpr, "Set")) {
|
|
2009
|
+
this.warnUnsupportedPattern(`set.${methodName}()`, this.getNodeLocation(callExpr), `Set.${methodName}() mutates in place. Use: new Set([...set, item]) for add, new Set([...set].filter(...)) for delete.`);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
const mapMethods = ["set", "delete", "clear"];
|
|
2013
|
+
if (mapMethods.includes(methodName)) {
|
|
2014
|
+
const sourceText = sourceExpr.getText();
|
|
2015
|
+
if (sourceText.includes("Map") || this.looksLikeSetOrMap(sourceExpr, "Map")) {
|
|
2016
|
+
this.warnUnsupportedPattern(`map.${methodName}()`, this.getNodeLocation(callExpr), `Map.${methodName}() mutates in place. Use: new Map([...map, [key, value]]) for set, new Map([...map].filter(...)) for delete.`);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
looksLikeSetOrMap(expr, collectionType) {
|
|
2021
|
+
const text = expr.getText().toLowerCase();
|
|
2022
|
+
return text.includes(collectionType.toLowerCase());
|
|
2023
|
+
}
|
|
2024
|
+
tryExtractSetConstructorPattern(fieldPath, right, assignments) {
|
|
2025
|
+
if (!fieldPath.endsWith(".value") || !Node4.isNewExpression(right))
|
|
2026
|
+
return false;
|
|
2027
|
+
const constructorExpr = right.getExpression();
|
|
2028
|
+
if (!Node4.isIdentifier(constructorExpr) || constructorExpr.getText() !== "Set")
|
|
2029
|
+
return false;
|
|
2030
|
+
const signalName = fieldPath.slice(0, -6);
|
|
2031
|
+
const setAssignment = this.extractSetOperation(right, fieldPath, signalName);
|
|
2032
|
+
if (setAssignment) {
|
|
2033
|
+
assignments.push(setAssignment);
|
|
2034
|
+
}
|
|
2035
|
+
return true;
|
|
2036
|
+
}
|
|
2037
|
+
tryExtractMapConstructorPattern(fieldPath, right, assignments) {
|
|
2038
|
+
if (!fieldPath.endsWith(".value") || !Node4.isNewExpression(right))
|
|
2039
|
+
return false;
|
|
2040
|
+
const constructorExpr = right.getExpression();
|
|
2041
|
+
if (!Node4.isIdentifier(constructorExpr) || constructorExpr.getText() !== "Map")
|
|
2042
|
+
return false;
|
|
2043
|
+
const signalName = fieldPath.slice(0, -6);
|
|
2044
|
+
const mapAssignment = this.extractMapOperation(right, fieldPath, signalName);
|
|
2045
|
+
if (mapAssignment) {
|
|
2046
|
+
assignments.push(mapAssignment);
|
|
2047
|
+
}
|
|
2048
|
+
return true;
|
|
2049
|
+
}
|
|
2050
|
+
extractSetOperation(newExpr, fieldPath, signalName) {
|
|
2051
|
+
const args = newExpr.getArguments();
|
|
2052
|
+
if (args.length === 0) {
|
|
2053
|
+
return { field: signalName, value: "{}" };
|
|
2054
|
+
}
|
|
2055
|
+
const firstArg = args[0];
|
|
2056
|
+
if (!firstArg)
|
|
2057
|
+
return null;
|
|
2058
|
+
if (Node4.isArrayLiteralExpression(firstArg)) {
|
|
2059
|
+
return this.extractSetArrayOperation(firstArg, fieldPath, signalName);
|
|
2060
|
+
}
|
|
2061
|
+
if (Node4.isCallExpression(firstArg)) {
|
|
2062
|
+
return this.extractSetMethodChainOperation(firstArg, fieldPath, signalName);
|
|
2063
|
+
}
|
|
2064
|
+
return null;
|
|
2065
|
+
}
|
|
2066
|
+
extractSetArrayOperation(arrayLiteral, fieldPath, signalName) {
|
|
2067
|
+
const elements = arrayLiteral.getElements();
|
|
2068
|
+
if (elements.length < 1)
|
|
2069
|
+
return null;
|
|
2070
|
+
const firstElement = elements[0];
|
|
2071
|
+
const lastElement = elements[elements.length - 1];
|
|
2072
|
+
if (firstElement && Node4.isSpreadElement(firstElement)) {
|
|
2073
|
+
const spreadExpr = firstElement.getExpression();
|
|
2074
|
+
if (spreadExpr && this.getPropertyPath(spreadExpr) === fieldPath) {
|
|
2075
|
+
return { field: signalName, value: "@ \\union {payload}" };
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
if (lastElement && Node4.isSpreadElement(lastElement)) {
|
|
2079
|
+
const spreadExpr = lastElement.getExpression();
|
|
2080
|
+
if (spreadExpr && this.getPropertyPath(spreadExpr) === fieldPath) {
|
|
2081
|
+
return { field: signalName, value: "{payload} \\union @" };
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
return null;
|
|
2085
|
+
}
|
|
2086
|
+
extractSetMethodChainOperation(callExpr, fieldPath, signalName) {
|
|
2087
|
+
const expression = callExpr.getExpression();
|
|
2088
|
+
if (!Node4.isPropertyAccessExpression(expression))
|
|
2089
|
+
return null;
|
|
2090
|
+
const methodName = expression.getName();
|
|
2091
|
+
const sourceExpr = expression.getExpression();
|
|
2092
|
+
if (methodName === "filter" && Node4.isArrayLiteralExpression(sourceExpr)) {
|
|
2093
|
+
const elements = sourceExpr.getElements();
|
|
2094
|
+
if (elements.length === 1) {
|
|
2095
|
+
const spreadEl = elements[0];
|
|
2096
|
+
if (spreadEl && Node4.isSpreadElement(spreadEl)) {
|
|
2097
|
+
const spreadExpr = spreadEl.getExpression();
|
|
2098
|
+
if (spreadExpr && this.getPropertyPath(spreadExpr) === fieldPath) {
|
|
2099
|
+
return { field: signalName, value: "@ \\ {payload}" };
|
|
2100
|
+
}
|
|
1930
2101
|
}
|
|
1931
2102
|
}
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
2103
|
+
}
|
|
2104
|
+
return null;
|
|
2105
|
+
}
|
|
2106
|
+
extractMapOperation(newExpr, fieldPath, signalName) {
|
|
2107
|
+
const args = newExpr.getArguments();
|
|
2108
|
+
if (args.length === 0) {
|
|
2109
|
+
return { field: signalName, value: "<<>>" };
|
|
2110
|
+
}
|
|
2111
|
+
const firstArg = args[0];
|
|
2112
|
+
if (!firstArg)
|
|
2113
|
+
return null;
|
|
2114
|
+
if (Node4.isArrayLiteralExpression(firstArg)) {
|
|
2115
|
+
return this.extractMapArrayOperation(firstArg, fieldPath, signalName);
|
|
2116
|
+
}
|
|
2117
|
+
if (Node4.isCallExpression(firstArg)) {
|
|
2118
|
+
return this.extractMapMethodChainOperation(firstArg, fieldPath, signalName);
|
|
2119
|
+
}
|
|
2120
|
+
return null;
|
|
2121
|
+
}
|
|
2122
|
+
extractMapArrayOperation(arrayLiteral, fieldPath, signalName) {
|
|
2123
|
+
const elements = arrayLiteral.getElements();
|
|
2124
|
+
if (elements.length < 1)
|
|
2125
|
+
return null;
|
|
2126
|
+
const firstElement = elements[0];
|
|
2127
|
+
if (firstElement && Node4.isSpreadElement(firstElement)) {
|
|
2128
|
+
const spreadExpr = firstElement.getExpression();
|
|
2129
|
+
if (spreadExpr && this.getPropertyPath(spreadExpr) === fieldPath) {
|
|
2130
|
+
return { field: signalName, value: "[@ EXCEPT ![payload.key] = payload.value]" };
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
return null;
|
|
2134
|
+
}
|
|
2135
|
+
extractMapMethodChainOperation(callExpr, fieldPath, signalName) {
|
|
2136
|
+
const expression = callExpr.getExpression();
|
|
2137
|
+
if (!Node4.isPropertyAccessExpression(expression))
|
|
2138
|
+
return null;
|
|
2139
|
+
const methodName = expression.getName();
|
|
2140
|
+
const sourceExpr = expression.getExpression();
|
|
2141
|
+
if (methodName === "filter" && Node4.isArrayLiteralExpression(sourceExpr)) {
|
|
2142
|
+
const elements = sourceExpr.getElements();
|
|
2143
|
+
if (elements.length === 1) {
|
|
2144
|
+
const spreadEl = elements[0];
|
|
2145
|
+
if (spreadEl && Node4.isSpreadElement(spreadEl)) {
|
|
2146
|
+
const spreadExpr = spreadEl.getExpression();
|
|
2147
|
+
if (spreadExpr && this.getPropertyPath(spreadExpr) === fieldPath) {
|
|
2148
|
+
return { field: signalName, value: "[k \\in DOMAIN @ \\ {payload.key} |-> @[k]]" };
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
1935
2151
|
}
|
|
1936
2152
|
}
|
|
2153
|
+
return null;
|
|
2154
|
+
}
|
|
2155
|
+
extractArrayMethodOperation(callExpr, fieldPath, signalName) {
|
|
2156
|
+
const expression = callExpr.getExpression();
|
|
2157
|
+
if (!Node4.isPropertyAccessExpression(expression))
|
|
2158
|
+
return null;
|
|
2159
|
+
const methodName = expression.getName();
|
|
2160
|
+
const sourceExpr = expression.getExpression();
|
|
2161
|
+
const sourcePath = this.getPropertyPath(sourceExpr);
|
|
2162
|
+
if (sourcePath !== fieldPath)
|
|
2163
|
+
return null;
|
|
2164
|
+
switch (methodName) {
|
|
2165
|
+
case "filter":
|
|
2166
|
+
return { field: signalName, value: "SelectSeq(@, LAMBDA t: TRUE)" };
|
|
2167
|
+
case "map":
|
|
2168
|
+
return { field: signalName, value: "[i \\in DOMAIN @ |-> @[i]]" };
|
|
2169
|
+
case "slice":
|
|
2170
|
+
return { field: signalName, value: "SubSeq(@, 1, Len(@))" };
|
|
2171
|
+
case "concat":
|
|
2172
|
+
return { field: signalName, value: "@ \\o <<payload>>" };
|
|
2173
|
+
case "reverse":
|
|
2174
|
+
return { field: signalName, value: "[i \\in DOMAIN @ |-> @[Len(@) - i + 1]]" };
|
|
2175
|
+
default:
|
|
2176
|
+
this.warnUnsupportedArrayMethod(methodName, callExpr);
|
|
2177
|
+
return null;
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
warnUnsupportedArrayMethod(methodName, node) {
|
|
2181
|
+
const mutatingMethods = [
|
|
2182
|
+
"push",
|
|
2183
|
+
"pop",
|
|
2184
|
+
"shift",
|
|
2185
|
+
"unshift",
|
|
2186
|
+
"splice",
|
|
2187
|
+
"sort",
|
|
2188
|
+
"fill",
|
|
2189
|
+
"copyWithin"
|
|
2190
|
+
];
|
|
2191
|
+
const queryMethods = [
|
|
2192
|
+
"find",
|
|
2193
|
+
"findIndex",
|
|
2194
|
+
"reduce",
|
|
2195
|
+
"reduceRight",
|
|
2196
|
+
"some",
|
|
2197
|
+
"every",
|
|
2198
|
+
"includes",
|
|
2199
|
+
"indexOf",
|
|
2200
|
+
"lastIndexOf"
|
|
2201
|
+
];
|
|
2202
|
+
const otherMethods = ["flat", "flatMap", "join", "toString", "toLocaleString"];
|
|
2203
|
+
if (mutatingMethods.includes(methodName)) {
|
|
2204
|
+
this.warnUnsupportedPattern(`array.${methodName}()`, this.getNodeLocation(node), `'${methodName}' mutates in place. Use spread syntax: [...arr, item] for append, arr.filter() for removal.`);
|
|
2205
|
+
} else if (queryMethods.includes(methodName)) {
|
|
2206
|
+
this.warnUnsupportedPattern(`array.${methodName}()`, this.getNodeLocation(node), `'${methodName}' returns a single value, not a new array. State assignment won't be extracted.`);
|
|
2207
|
+
} else if (otherMethods.includes(methodName)) {
|
|
2208
|
+
this.warnUnsupportedPattern(`array.${methodName}()`, this.getNodeLocation(node), `'${methodName}' is not supported for state extraction. Consider using map/filter instead.`);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
extractSpreadUpdateAssignments(objectLiteral, assignments, signalName) {
|
|
2212
|
+
for (const prop of objectLiteral.getProperties()) {
|
|
2213
|
+
if (Node4.isSpreadAssignment(prop))
|
|
2214
|
+
continue;
|
|
2215
|
+
this.extractPropertyAssignment(prop, assignments, signalName);
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
extractArraySpreadOperation(arrayLiteral, fieldPath, signalName) {
|
|
2219
|
+
const elements = arrayLiteral.getElements();
|
|
2220
|
+
if (elements.length < 1)
|
|
2221
|
+
return null;
|
|
2222
|
+
return this.tryExtractAppendOperation(elements, fieldPath, signalName) ?? this.tryExtractPrependOperation(elements, fieldPath, signalName);
|
|
2223
|
+
}
|
|
2224
|
+
tryExtractAppendOperation(elements, fieldPath, signalName) {
|
|
2225
|
+
const firstElement = elements[0];
|
|
2226
|
+
if (!firstElement || !Node4.isSpreadElement(firstElement))
|
|
2227
|
+
return null;
|
|
2228
|
+
const spreadExpr = firstElement.getExpression();
|
|
2229
|
+
if (!spreadExpr || this.getPropertyPath(spreadExpr) !== fieldPath)
|
|
2230
|
+
return null;
|
|
2231
|
+
if (elements.length === 2) {
|
|
2232
|
+
return { field: signalName, value: "Append(@, payload)" };
|
|
2233
|
+
}
|
|
2234
|
+
const placeholders = Array(elements.length - 1).fill("payload").join(", ");
|
|
2235
|
+
return { field: signalName, value: `@ \\o <<${placeholders}>>` };
|
|
2236
|
+
}
|
|
2237
|
+
tryExtractPrependOperation(elements, fieldPath, signalName) {
|
|
2238
|
+
if (elements.length < 2)
|
|
2239
|
+
return null;
|
|
2240
|
+
const lastElement = elements[elements.length - 1];
|
|
2241
|
+
if (!lastElement || !Node4.isSpreadElement(lastElement))
|
|
2242
|
+
return null;
|
|
2243
|
+
const spreadExpr = lastElement.getExpression();
|
|
2244
|
+
if (!spreadExpr || this.getPropertyPath(spreadExpr) !== fieldPath)
|
|
2245
|
+
return null;
|
|
2246
|
+
if (elements.length === 2) {
|
|
2247
|
+
return { field: signalName, value: "<<payload>> \\o @" };
|
|
2248
|
+
}
|
|
2249
|
+
const placeholders = Array(elements.length - 1).fill("payload").join(", ");
|
|
2250
|
+
return { field: signalName, value: `<<${placeholders}>> \\o @` };
|
|
2251
|
+
}
|
|
2252
|
+
extractObjectLiteralAssignments(objectLiteral, assignments, signalName) {
|
|
2253
|
+
if (!Node4.isObjectLiteralExpression(objectLiteral))
|
|
2254
|
+
return;
|
|
2255
|
+
for (const prop of objectLiteral.getProperties()) {
|
|
2256
|
+
this.extractPropertyAssignment(prop, assignments, signalName);
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
extractPropertyAssignment(prop, assignments, signalName) {
|
|
2260
|
+
if (Node4.isPropertyAssignment(prop)) {
|
|
2261
|
+
const name = prop.getName();
|
|
2262
|
+
const initializer = prop.getInitializer();
|
|
2263
|
+
if (!name || !initializer)
|
|
2264
|
+
return;
|
|
2265
|
+
const value = this.extractValue(initializer);
|
|
2266
|
+
if (value === undefined)
|
|
2267
|
+
return;
|
|
2268
|
+
const field = signalName ? `${signalName}_${name}` : name;
|
|
2269
|
+
assignments.push({ field, value });
|
|
2270
|
+
return;
|
|
2271
|
+
}
|
|
2272
|
+
if (Node4.isShorthandPropertyAssignment(prop)) {
|
|
2273
|
+
const name = prop.getName();
|
|
2274
|
+
const field = signalName ? `${signalName}_${name}` : name;
|
|
2275
|
+
assignments.push({ field, value: "@" });
|
|
2276
|
+
}
|
|
1937
2277
|
}
|
|
1938
2278
|
extractElementAccessAssignment(left, right, assignments) {
|
|
1939
2279
|
if (!Node4.isElementAccessExpression(left))
|
|
@@ -2946,15 +3286,15 @@ class HandlerExtractor {
|
|
|
2946
3286
|
if (path2 === `${varName}.value`) {
|
|
2947
3287
|
const right = node.getRight();
|
|
2948
3288
|
if (Node4.isObjectLiteralExpression(right)) {
|
|
2949
|
-
this.extractObjectLiteralAssignments(right, mutations);
|
|
3289
|
+
this.extractObjectLiteralAssignments(right, mutations, varName);
|
|
2950
3290
|
}
|
|
2951
3291
|
break;
|
|
2952
3292
|
}
|
|
2953
3293
|
const fieldPrefix = `${varName}.value.`;
|
|
2954
3294
|
if (path2.startsWith(fieldPrefix)) {
|
|
2955
|
-
const
|
|
3295
|
+
const fieldName = path2.substring(fieldPrefix.length);
|
|
2956
3296
|
const value = this.extractValue(node.getRight());
|
|
2957
|
-
mutations.push({ field
|
|
3297
|
+
mutations.push({ field: `${varName}_${fieldName}`, value: value ?? "@" });
|
|
2958
3298
|
break;
|
|
2959
3299
|
}
|
|
2960
3300
|
}
|
|
@@ -5285,4 +5625,4 @@ main().catch((_error) => {
|
|
|
5285
5625
|
process.exit(1);
|
|
5286
5626
|
});
|
|
5287
5627
|
|
|
5288
|
-
//# debugId=
|
|
5628
|
+
//# debugId=3513B8B8E90D943B64756E2164756E21
|