@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.
- package/cli/polly.ts +9 -3
- package/package.json +2 -2
- package/vendor/analysis/src/extract/adr.ts +212 -0
- package/vendor/analysis/src/extract/architecture.ts +160 -0
- package/vendor/analysis/src/extract/contexts.ts +298 -0
- package/vendor/analysis/src/extract/flows.ts +309 -0
- package/vendor/analysis/src/extract/handlers.ts +321 -0
- package/vendor/analysis/src/extract/index.ts +9 -0
- package/vendor/analysis/src/extract/integrations.ts +329 -0
- package/vendor/analysis/src/extract/manifest.ts +298 -0
- package/vendor/analysis/src/extract/types.ts +389 -0
- package/vendor/analysis/src/index.ts +7 -0
- package/vendor/analysis/src/types/adr.ts +53 -0
- package/vendor/analysis/src/types/architecture.ts +245 -0
- package/vendor/analysis/src/types/core.ts +210 -0
- package/vendor/analysis/src/types/index.ts +18 -0
- package/vendor/verify/src/adapters/base.ts +164 -0
- package/vendor/verify/src/adapters/detection.ts +281 -0
- package/vendor/verify/src/adapters/event-bus/index.ts +480 -0
- package/vendor/verify/src/adapters/web-extension/index.ts +508 -0
- package/vendor/verify/src/adapters/websocket/index.ts +486 -0
- package/vendor/verify/src/cli.ts +430 -0
- package/vendor/verify/src/codegen/config.ts +354 -0
- package/vendor/verify/src/codegen/tla.ts +719 -0
- package/vendor/verify/src/config/parser.ts +303 -0
- package/vendor/verify/src/config/types.ts +113 -0
- package/vendor/verify/src/core/model.ts +267 -0
- package/vendor/verify/src/core/primitives.ts +106 -0
- package/vendor/verify/src/extract/handlers.ts +2 -0
- package/vendor/verify/src/extract/types.ts +2 -0
- package/vendor/verify/src/index.ts +150 -0
- package/vendor/verify/src/primitives/index.ts +102 -0
- package/vendor/verify/src/runner/docker.ts +283 -0
- package/vendor/verify/src/types.ts +51 -0
- package/vendor/visualize/src/cli.ts +365 -0
- package/vendor/visualize/src/codegen/structurizr.ts +770 -0
- package/vendor/visualize/src/index.ts +13 -0
- package/vendor/visualize/src/runner/export.ts +235 -0
- 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
|
+
}
|