@fairfox/polly 0.1.1 → 0.1.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 (39) hide show
  1. package/cli/polly.ts +9 -3
  2. package/package.json +2 -2
  3. package/vendor/analysis/src/extract/adr.ts +212 -0
  4. package/vendor/analysis/src/extract/architecture.ts +160 -0
  5. package/vendor/analysis/src/extract/contexts.ts +298 -0
  6. package/vendor/analysis/src/extract/flows.ts +309 -0
  7. package/vendor/analysis/src/extract/handlers.ts +321 -0
  8. package/vendor/analysis/src/extract/index.ts +9 -0
  9. package/vendor/analysis/src/extract/integrations.ts +329 -0
  10. package/vendor/analysis/src/extract/manifest.ts +298 -0
  11. package/vendor/analysis/src/extract/types.ts +389 -0
  12. package/vendor/analysis/src/index.ts +7 -0
  13. package/vendor/analysis/src/types/adr.ts +53 -0
  14. package/vendor/analysis/src/types/architecture.ts +245 -0
  15. package/vendor/analysis/src/types/core.ts +210 -0
  16. package/vendor/analysis/src/types/index.ts +18 -0
  17. package/vendor/verify/src/adapters/base.ts +164 -0
  18. package/vendor/verify/src/adapters/detection.ts +281 -0
  19. package/vendor/verify/src/adapters/event-bus/index.ts +480 -0
  20. package/vendor/verify/src/adapters/web-extension/index.ts +508 -0
  21. package/vendor/verify/src/adapters/websocket/index.ts +486 -0
  22. package/vendor/verify/src/cli.ts +430 -0
  23. package/vendor/verify/src/codegen/config.ts +354 -0
  24. package/vendor/verify/src/codegen/tla.ts +719 -0
  25. package/vendor/verify/src/config/parser.ts +303 -0
  26. package/vendor/verify/src/config/types.ts +113 -0
  27. package/vendor/verify/src/core/model.ts +267 -0
  28. package/vendor/verify/src/core/primitives.ts +106 -0
  29. package/vendor/verify/src/extract/handlers.ts +2 -0
  30. package/vendor/verify/src/extract/types.ts +2 -0
  31. package/vendor/verify/src/index.ts +150 -0
  32. package/vendor/verify/src/primitives/index.ts +102 -0
  33. package/vendor/verify/src/runner/docker.ts +283 -0
  34. package/vendor/verify/src/types.ts +51 -0
  35. package/vendor/visualize/src/cli.ts +365 -0
  36. package/vendor/visualize/src/codegen/structurizr.ts +770 -0
  37. package/vendor/visualize/src/index.ts +13 -0
  38. package/vendor/visualize/src/runner/export.ts +235 -0
  39. package/vendor/visualize/src/viewer/server.ts +485 -0
@@ -0,0 +1,298 @@
1
+ // Context analysis - analyze individual execution contexts
2
+
3
+ import { Project, Node, SyntaxKind } from "ts-morph";
4
+ import type { ContextInfo, ComponentInfo } from "../types/architecture";
5
+ import type { MessageHandler } from "../types/core";
6
+
7
+ export class ContextAnalyzer {
8
+ private project: Project;
9
+
10
+ constructor(tsConfigPath: string) {
11
+ this.project = new Project({
12
+ tsConfigFilePath: tsConfigPath,
13
+ });
14
+ }
15
+
16
+ /**
17
+ * Analyze a specific context given its entry point
18
+ */
19
+ analyzeContext(contextType: string, entryPoint: string, handlers: MessageHandler[]): ContextInfo {
20
+ const sourceFile = this.project.getSourceFile(entryPoint);
21
+
22
+ if (!sourceFile) {
23
+ throw new Error(`Could not find source file: ${entryPoint}`);
24
+ }
25
+
26
+ // Extract Chrome API usage
27
+ const chromeAPIs = this.extractChromeAPIs(sourceFile);
28
+
29
+ // Extract dependencies
30
+ const dependencies = this.extractDependencies(sourceFile);
31
+
32
+ // Extract JSDoc description
33
+ const description = this.extractDescription(sourceFile);
34
+
35
+ // Extract components (for UI contexts)
36
+ const components = this.isUIContext(contextType)
37
+ ? this.extractComponents(sourceFile)
38
+ : undefined;
39
+
40
+ // Filter handlers for this context
41
+ const contextHandlers = handlers.filter((h) => h.node === contextType);
42
+
43
+ return {
44
+ type: contextType,
45
+ entryPoint,
46
+ handlers: contextHandlers,
47
+ chromeAPIs,
48
+ externalAPIs: [], // Will be filled by integration analyzer
49
+ components,
50
+ dependencies,
51
+ description,
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Extract Chrome API usage from source file
57
+ */
58
+ private extractChromeAPIs(sourceFile: any): string[] {
59
+ const apis = new Set<string>();
60
+
61
+ sourceFile.forEachDescendant((node: any) => {
62
+ if (Node.isPropertyAccessExpression(node)) {
63
+ const text = node.getText();
64
+
65
+ // Match chrome.* API calls
66
+ if (text.startsWith("chrome.")) {
67
+ // Extract API namespace (e.g., "chrome.tabs", "chrome.storage.local")
68
+ const match = text.match(/^chrome\.([^.(]+(?:\.[^.(]+)?)/);
69
+ if (match) {
70
+ apis.add(match[1]);
71
+ }
72
+ }
73
+
74
+ // Also match browser.* for Firefox compatibility
75
+ if (text.startsWith("browser.")) {
76
+ const match = text.match(/^browser\.([^.(]+(?:\.[^.(]+)?)/);
77
+ if (match) {
78
+ apis.add(match[1]);
79
+ }
80
+ }
81
+
82
+ // Match bus.adapters.* pattern (framework abstraction over Chrome APIs)
83
+ // e.g., bus.adapters.storage.get() -> storage
84
+ // bus.adapters.tabs.query() -> tabs
85
+ if (text.includes("bus.adapters.")) {
86
+ const match = text.match(/bus\.adapters\.([^.(]+)/);
87
+ if (match) {
88
+ apis.add(match[1]);
89
+ }
90
+ }
91
+ }
92
+ });
93
+
94
+ return Array.from(apis).sort();
95
+ }
96
+
97
+ /**
98
+ * Extract import dependencies
99
+ */
100
+ private extractDependencies(sourceFile: any): string[] {
101
+ const deps: string[] = [];
102
+
103
+ for (const importDecl of sourceFile.getImportDeclarations()) {
104
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
105
+ deps.push(moduleSpecifier);
106
+ }
107
+
108
+ return deps;
109
+ }
110
+
111
+ /**
112
+ * Extract JSDoc description from file
113
+ */
114
+ private extractDescription(sourceFile: any): string | undefined {
115
+ // Look for file-level JSDoc comment
116
+ const firstStatement = sourceFile.getStatements()[0];
117
+ if (!firstStatement) return undefined;
118
+
119
+ const leadingComments = firstStatement.getLeadingCommentRanges();
120
+ if (leadingComments.length === 0) return undefined;
121
+
122
+ const comment = leadingComments[0].getText();
123
+
124
+ // Extract description from JSDoc
125
+ const descMatch = comment.match(/@description\s+(.+?)(?:\n|$)/s);
126
+ if (descMatch) {
127
+ return descMatch[1].trim();
128
+ }
129
+
130
+ // Or just take the first line of the comment
131
+ const lines = comment
132
+ .split("\n")
133
+ .map((l: string) => l.replace(/^[\s*]+/, "").trim())
134
+ .filter((l: string) => l && !l.startsWith("@"));
135
+
136
+ return lines[0] || undefined;
137
+ }
138
+
139
+ /**
140
+ * Extract React/Preact components from source file
141
+ */
142
+ private extractComponents(sourceFile: any): ComponentInfo[] {
143
+ const components: ComponentInfo[] = [];
144
+
145
+ sourceFile.forEachDescendant((node: any) => {
146
+ // Function components
147
+ if (Node.isFunctionDeclaration(node)) {
148
+ const name = node.getName();
149
+ if (name && this.looksLikeComponent(name, node)) {
150
+ components.push({
151
+ name,
152
+ type: "function",
153
+ filePath: sourceFile.getFilePath(),
154
+ line: node.getStartLineNumber(),
155
+ props: this.extractProps(node),
156
+ description: this.extractJSDocDescription(node),
157
+ });
158
+ }
159
+ }
160
+
161
+ // Arrow function components (const Foo = () => ...)
162
+ if (Node.isVariableDeclaration(node)) {
163
+ const name = node.getName();
164
+ const initializer = node.getInitializer();
165
+
166
+ if (
167
+ name &&
168
+ initializer &&
169
+ (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer))
170
+ ) {
171
+ if (this.looksLikeComponent(name, initializer)) {
172
+ components.push({
173
+ name,
174
+ type: "function",
175
+ filePath: sourceFile.getFilePath(),
176
+ line: node.getStartLineNumber(),
177
+ props: this.extractProps(initializer),
178
+ description: this.extractJSDocDescription(node),
179
+ });
180
+ }
181
+ }
182
+ }
183
+
184
+ // Class components
185
+ if (Node.isClassDeclaration(node)) {
186
+ const name = node.getName();
187
+ if (name && this.looksLikeClassComponent(node)) {
188
+ components.push({
189
+ name,
190
+ type: "class",
191
+ filePath: sourceFile.getFilePath(),
192
+ line: node.getStartLineNumber(),
193
+ props: this.extractPropsFromClass(node),
194
+ description: this.extractJSDocDescription(node),
195
+ });
196
+ }
197
+ }
198
+ });
199
+
200
+ return components;
201
+ }
202
+
203
+ /**
204
+ * Check if a function looks like a React/Preact component
205
+ */
206
+ private looksLikeComponent(name: string, node: any): boolean {
207
+ // Component names should start with uppercase
208
+ if (!/^[A-Z]/.test(name)) return false;
209
+
210
+ // Check if it returns JSX
211
+ const body = node.getBody();
212
+ if (!body) return false;
213
+
214
+ let hasJSX = false;
215
+
216
+ if (Node.isBlock(body)) {
217
+ body.forEachDescendant((child: any) => {
218
+ if (Node.isJsxElement(child) || Node.isJsxSelfClosingElement(child)) {
219
+ hasJSX = true;
220
+ }
221
+ });
222
+ } else {
223
+ // Arrow function with implicit return
224
+ if (Node.isJsxElement(body) || Node.isJsxSelfClosingElement(body)) {
225
+ hasJSX = true;
226
+ }
227
+ }
228
+
229
+ return hasJSX;
230
+ }
231
+
232
+ /**
233
+ * Check if a class looks like a React component
234
+ */
235
+ private looksLikeClassComponent(node: any): boolean {
236
+ const extendedTypes = node.getExtends();
237
+ if (!extendedTypes) return false;
238
+
239
+ const extendsText = extendedTypes.getText();
240
+ return /Component|PureComponent/.test(extendsText);
241
+ }
242
+
243
+ /**
244
+ * Extract props from function component
245
+ */
246
+ private extractProps(node: any): string[] {
247
+ const params = node.getParameters();
248
+ if (params.length === 0) return [];
249
+
250
+ const propsParam = params[0];
251
+ const type = propsParam.getType();
252
+
253
+ const props: string[] = [];
254
+ for (const prop of type.getProperties()) {
255
+ props.push(prop.getName());
256
+ }
257
+
258
+ return props;
259
+ }
260
+
261
+ /**
262
+ * Extract props from class component
263
+ */
264
+ private extractPropsFromClass(node: any): string[] {
265
+ const extendedTypes = node.getExtends();
266
+ if (!extendedTypes) return [];
267
+
268
+ const typeArgs = extendedTypes.getType().getTypeArguments();
269
+ if (typeArgs.length === 0) return [];
270
+
271
+ const propsType = typeArgs[0];
272
+ const props: string[] = [];
273
+
274
+ for (const prop of propsType.getProperties()) {
275
+ props.push(prop.getName());
276
+ }
277
+
278
+ return props;
279
+ }
280
+
281
+ /**
282
+ * Extract JSDoc description from node
283
+ */
284
+ private extractJSDocDescription(node: any): string | undefined {
285
+ const jsDocs = node.getJsDocs();
286
+ if (jsDocs.length === 0) return undefined;
287
+
288
+ const description = jsDocs[0].getDescription().trim();
289
+ return description || undefined;
290
+ }
291
+
292
+ /**
293
+ * Check if context is a UI context
294
+ */
295
+ private isUIContext(contextType: string): boolean {
296
+ return ["popup", "options", "devtools"].includes(contextType);
297
+ }
298
+ }
@@ -0,0 +1,309 @@
1
+ // Message flow analysis - trace messages between contexts
2
+
3
+ import { Project, Node } from "ts-morph";
4
+ import type { MessageFlow, MessageStep } from "../types/architecture";
5
+ import type { MessageHandler } from "../types/core";
6
+
7
+ export class FlowAnalyzer {
8
+ private project: Project;
9
+ private handlers: MessageHandler[];
10
+
11
+ constructor(tsConfigPath: string, handlers: MessageHandler[]) {
12
+ this.project = new Project({
13
+ tsConfigFilePath: tsConfigPath,
14
+ });
15
+ this.handlers = handlers;
16
+ }
17
+
18
+ /**
19
+ * Analyze message flows between contexts
20
+ */
21
+ analyzeFlows(): MessageFlow[] {
22
+ const flows: MessageFlow[] = [];
23
+
24
+ // Group handlers by message type
25
+ const handlersByType = new Map<string, MessageHandler[]>();
26
+ for (const handler of this.handlers) {
27
+ if (!handlersByType.has(handler.messageType)) {
28
+ handlersByType.set(handler.messageType, []);
29
+ }
30
+ handlersByType.get(handler.messageType)!.push(handler);
31
+ }
32
+
33
+ // For each message type, trace the flow
34
+ for (const [messageType, handlers] of handlersByType) {
35
+ // Find senders
36
+ const senders = this.findMessageSenders(messageType);
37
+
38
+ // For each sender, create a flow
39
+ for (const sender of senders) {
40
+ const recipients = handlers.map((h) => h.node);
41
+
42
+ // Build sequence of steps
43
+ const sequence = this.buildSequence(messageType, sender, handlers);
44
+
45
+ // Extract flow metadata
46
+ const flowMetadata = this.extractFlowMetadata(sender.file, sender.line);
47
+
48
+ flows.push({
49
+ messageType,
50
+ from: sender.context,
51
+ to: recipients,
52
+ trigger: flowMetadata.trigger,
53
+ flowName: flowMetadata.flowName,
54
+ description: flowMetadata.description,
55
+ sequence,
56
+ });
57
+ }
58
+ }
59
+
60
+ return flows;
61
+ }
62
+
63
+ /**
64
+ * Find all places where a message is sent
65
+ */
66
+ private findMessageSenders(messageType: string): Array<{
67
+ context: string;
68
+ file: string;
69
+ line: number;
70
+ }> {
71
+ const senders: Array<{ context: string; file: string; line: number }> = [];
72
+
73
+ for (const sourceFile of this.project.getSourceFiles()) {
74
+ const filePath = sourceFile.getFilePath();
75
+ const context = this.inferContext(filePath);
76
+
77
+ sourceFile.forEachDescendant((node) => {
78
+ if (Node.isCallExpression(node)) {
79
+ const expression = node.getExpression();
80
+
81
+ // Check for .send() or .emit() calls
82
+ if (Node.isPropertyAccessExpression(expression)) {
83
+ const methodName = expression.getName();
84
+
85
+ if (methodName === "send" || methodName === "emit") {
86
+ const args = node.getArguments();
87
+ if (args.length > 0) {
88
+ const firstArg = args[0];
89
+
90
+ let msgType: string | undefined;
91
+
92
+ // Check if first argument is a string literal: send("MESSAGE")
93
+ if (Node.isStringLiteral(firstArg)) {
94
+ msgType = firstArg.getLiteralValue();
95
+ }
96
+ // Check if first argument is an object literal: send({ type: "MESSAGE" })
97
+ else if (Node.isObjectLiteralExpression(firstArg)) {
98
+ const typeProperty = firstArg.getProperty("type");
99
+ if (typeProperty && Node.isPropertyAssignment(typeProperty)) {
100
+ const initializer = typeProperty.getInitializer();
101
+ if (initializer && Node.isStringLiteral(initializer)) {
102
+ msgType = initializer.getLiteralValue();
103
+ }
104
+ }
105
+ }
106
+
107
+ if (msgType === messageType) {
108
+ senders.push({
109
+ context,
110
+ file: filePath,
111
+ line: node.getStartLineNumber(),
112
+ });
113
+ }
114
+ }
115
+ }
116
+ }
117
+ }
118
+ });
119
+ }
120
+
121
+ return senders;
122
+ }
123
+
124
+ /**
125
+ * Build sequence of steps for a message flow
126
+ */
127
+ private buildSequence(
128
+ messageType: string,
129
+ sender: { context: string; file: string; line: number },
130
+ handlers: MessageHandler[]
131
+ ): MessageStep[] {
132
+ const steps: MessageStep[] = [];
133
+ let stepNumber = 1;
134
+
135
+ // Step 1: Send message
136
+ steps.push({
137
+ step: stepNumber++,
138
+ action: `${sender.context}.send(${messageType})`,
139
+ context: sender.context,
140
+ location: {
141
+ file: sender.file,
142
+ line: sender.line,
143
+ },
144
+ });
145
+
146
+ // Step 2+: Handle in each recipient context
147
+ for (const handler of handlers) {
148
+ steps.push({
149
+ step: stepNumber++,
150
+ action: `${handler.node}.handle(${messageType})`,
151
+ context: handler.node,
152
+ location: handler.location,
153
+ });
154
+
155
+ // Add substeps for any messages sent by this handler
156
+ const subsends = this.findMessagesInHandler(handler);
157
+ for (const subsend of subsends) {
158
+ steps.push({
159
+ step: stepNumber++,
160
+ action: `${handler.node}.send(${subsend.messageType})`,
161
+ context: handler.node,
162
+ location: subsend.location,
163
+ });
164
+ }
165
+ }
166
+
167
+ return steps;
168
+ }
169
+
170
+ /**
171
+ * Find messages sent within a handler
172
+ */
173
+ private findMessagesInHandler(handler: MessageHandler): Array<{
174
+ messageType: string;
175
+ location: { file: string; line: number };
176
+ }> {
177
+ const sends: Array<{ messageType: string; location: { file: string; line: number } }> = [];
178
+
179
+ const sourceFile = this.project.getSourceFile(handler.location.file);
180
+ if (!sourceFile) return sends;
181
+
182
+ // Find the handler function at the given line
183
+ const targetLine = handler.location.line;
184
+
185
+ sourceFile.forEachDescendant((node) => {
186
+ if (Node.isCallExpression(node)) {
187
+ const line = node.getStartLineNumber();
188
+
189
+ // Rough heuristic: if it's near the handler line, it's probably in the handler
190
+ if (Math.abs(line - targetLine) < 20) {
191
+ const expression = node.getExpression();
192
+
193
+ if (Node.isPropertyAccessExpression(expression)) {
194
+ const methodName = expression.getName();
195
+
196
+ if (methodName === "send" || methodName === "emit") {
197
+ const args = node.getArguments();
198
+ if (args.length > 0) {
199
+ const firstArg = args[0];
200
+ let messageType: string | undefined;
201
+
202
+ // Check if first argument is a string literal: send("MESSAGE")
203
+ if (Node.isStringLiteral(firstArg)) {
204
+ messageType = firstArg.getLiteralValue();
205
+ }
206
+ // Check if first argument is an object literal: send({ type: "MESSAGE" })
207
+ else if (Node.isObjectLiteralExpression(firstArg)) {
208
+ const typeProperty = firstArg.getProperty("type");
209
+ if (typeProperty && Node.isPropertyAssignment(typeProperty)) {
210
+ const initializer = typeProperty.getInitializer();
211
+ if (initializer && Node.isStringLiteral(initializer)) {
212
+ messageType = initializer.getLiteralValue();
213
+ }
214
+ }
215
+ }
216
+
217
+ if (messageType) {
218
+ sends.push({
219
+ messageType,
220
+ location: {
221
+ file: handler.location.file,
222
+ line,
223
+ },
224
+ });
225
+ }
226
+ }
227
+ }
228
+ }
229
+ }
230
+ }
231
+ });
232
+
233
+ return sends;
234
+ }
235
+
236
+ /**
237
+ * Extract flow metadata from JSDoc annotations
238
+ */
239
+ private extractFlowMetadata(
240
+ filePath: string,
241
+ lineNumber: number
242
+ ): {
243
+ trigger?: string;
244
+ flowName?: string;
245
+ description?: string;
246
+ } {
247
+ const sourceFile = this.project.getSourceFile(filePath);
248
+ if (!sourceFile) return {};
249
+
250
+ // Find the node at this line
251
+ let targetNode: any = null;
252
+
253
+ sourceFile.forEachDescendant((node) => {
254
+ if (node.getStartLineNumber() === lineNumber) {
255
+ targetNode = node;
256
+ }
257
+ });
258
+
259
+ if (!targetNode) return {};
260
+
261
+ // Look for JSDoc comments
262
+ const jsDocs = targetNode.getJsDocs?.() || [];
263
+ if (jsDocs.length === 0) return {};
264
+
265
+ const comment = jsDocs[0].getText();
266
+
267
+ // Extract @flow annotation
268
+ const flowMatch = comment.match(/@flow\s+([^\s]+)/);
269
+ const flowName = flowMatch ? flowMatch[1] : undefined;
270
+
271
+ // Extract @trigger annotation
272
+ const triggerMatch = comment.match(/@trigger\s+(.+?)(?:\n|$)/);
273
+ const trigger = triggerMatch ? triggerMatch[1].trim() : undefined;
274
+
275
+ // Extract @description
276
+ const descMatch = comment.match(/@description\s+(.+?)(?:\n|$)/s);
277
+ const description = descMatch ? descMatch[1].trim() : undefined;
278
+
279
+ return { trigger, flowName, description };
280
+ }
281
+
282
+ /**
283
+ * Infer context from file path
284
+ */
285
+ private inferContext(filePath: string): string {
286
+ const path = filePath.toLowerCase();
287
+
288
+ if (path.includes("/background/") || path.includes("\\background\\")) {
289
+ return "background";
290
+ }
291
+ if (path.includes("/content/") || path.includes("\\content\\")) {
292
+ return "content";
293
+ }
294
+ if (path.includes("/popup/") || path.includes("\\popup\\")) {
295
+ return "popup";
296
+ }
297
+ if (path.includes("/devtools/") || path.includes("\\devtools\\")) {
298
+ return "devtools";
299
+ }
300
+ if (path.includes("/options/") || path.includes("\\options\\")) {
301
+ return "options";
302
+ }
303
+ if (path.includes("/offscreen/") || path.includes("\\offscreen\\")) {
304
+ return "offscreen";
305
+ }
306
+
307
+ return "unknown";
308
+ }
309
+ }