@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
|
+
// Manifest.json parser for Chrome extensions
|
|
2
|
+
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import type { ManifestInfo, ContextInfo } from "../types/architecture";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parse manifest.json and extract context information
|
|
9
|
+
*/
|
|
10
|
+
export class ManifestParser {
|
|
11
|
+
private manifestPath: string;
|
|
12
|
+
private manifestData: any;
|
|
13
|
+
private baseDir: string;
|
|
14
|
+
|
|
15
|
+
constructor(projectRoot: string) {
|
|
16
|
+
this.baseDir = projectRoot;
|
|
17
|
+
this.manifestPath = path.join(projectRoot, "manifest.json");
|
|
18
|
+
|
|
19
|
+
if (!fs.existsSync(this.manifestPath)) {
|
|
20
|
+
throw new Error(`manifest.json not found at ${this.manifestPath}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const content = fs.readFileSync(this.manifestPath, "utf-8");
|
|
25
|
+
this.manifestData = JSON.parse(content);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw new Error(`Failed to parse manifest.json: ${error}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse manifest and extract all information
|
|
33
|
+
*/
|
|
34
|
+
parse(): ManifestInfo {
|
|
35
|
+
const manifest = this.manifestData;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
name: manifest.name || "Unknown Extension",
|
|
39
|
+
version: manifest.version || "0.0.0",
|
|
40
|
+
description: manifest.description,
|
|
41
|
+
manifestVersion: manifest.manifest_version || 2,
|
|
42
|
+
background: this.parseBackground(),
|
|
43
|
+
contentScripts: this.parseContentScripts(),
|
|
44
|
+
popup: this.parsePopup(),
|
|
45
|
+
options: this.parseOptions(),
|
|
46
|
+
devtools: this.parseDevtools(),
|
|
47
|
+
permissions: manifest.permissions || [],
|
|
48
|
+
hostPermissions: manifest.host_permissions || [],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get context entry points from manifest
|
|
54
|
+
*/
|
|
55
|
+
getContextEntryPoints(): Record<string, string> {
|
|
56
|
+
const entryPoints: Record<string, string> = {};
|
|
57
|
+
|
|
58
|
+
// Background
|
|
59
|
+
const background = this.parseBackground();
|
|
60
|
+
if (background) {
|
|
61
|
+
// Take first file as entry point
|
|
62
|
+
const entryFile = background.files[0];
|
|
63
|
+
if (entryFile) {
|
|
64
|
+
entryPoints.background = this.findSourceFile(entryFile);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Content scripts
|
|
69
|
+
const contentScripts = this.parseContentScripts();
|
|
70
|
+
if (contentScripts && contentScripts.length > 0) {
|
|
71
|
+
const firstScript = contentScripts[0].js[0];
|
|
72
|
+
if (firstScript) {
|
|
73
|
+
entryPoints.content = this.findSourceFile(firstScript);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Popup
|
|
78
|
+
const popup = this.parsePopup();
|
|
79
|
+
if (popup) {
|
|
80
|
+
// For HTML, we need to find the associated JS/TS file
|
|
81
|
+
const htmlPath = path.join(this.baseDir, popup.html);
|
|
82
|
+
const jsPath = this.findAssociatedJS(htmlPath);
|
|
83
|
+
if (jsPath) {
|
|
84
|
+
entryPoints.popup = jsPath;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Options
|
|
89
|
+
const options = this.parseOptions();
|
|
90
|
+
if (options) {
|
|
91
|
+
const htmlPath = path.join(this.baseDir, options.page);
|
|
92
|
+
const jsPath = this.findAssociatedJS(htmlPath);
|
|
93
|
+
if (jsPath) {
|
|
94
|
+
entryPoints.options = jsPath;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// DevTools
|
|
99
|
+
const devtools = this.parseDevtools();
|
|
100
|
+
if (devtools) {
|
|
101
|
+
const htmlPath = path.join(this.baseDir, devtools.page);
|
|
102
|
+
const jsPath = this.findAssociatedJS(htmlPath);
|
|
103
|
+
if (jsPath) {
|
|
104
|
+
entryPoints.devtools = jsPath;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return entryPoints;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Parse background configuration
|
|
113
|
+
*/
|
|
114
|
+
private parseBackground(): ManifestInfo["background"] {
|
|
115
|
+
const bg = this.manifestData.background;
|
|
116
|
+
if (!bg) return undefined;
|
|
117
|
+
|
|
118
|
+
// Manifest V3 - service worker
|
|
119
|
+
if (bg.service_worker) {
|
|
120
|
+
return {
|
|
121
|
+
type: "service_worker",
|
|
122
|
+
files: [bg.service_worker],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Manifest V2 - scripts
|
|
127
|
+
if (bg.scripts) {
|
|
128
|
+
return {
|
|
129
|
+
type: "script",
|
|
130
|
+
files: bg.scripts,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Manifest V2 - page
|
|
135
|
+
if (bg.page) {
|
|
136
|
+
return {
|
|
137
|
+
type: "script",
|
|
138
|
+
files: [bg.page],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Parse content scripts configuration
|
|
147
|
+
*/
|
|
148
|
+
private parseContentScripts(): ManifestInfo["contentScripts"] {
|
|
149
|
+
const cs = this.manifestData.content_scripts;
|
|
150
|
+
if (!cs || !Array.isArray(cs)) return undefined;
|
|
151
|
+
|
|
152
|
+
return cs.map((script) => ({
|
|
153
|
+
matches: script.matches || [],
|
|
154
|
+
js: script.js || [],
|
|
155
|
+
css: script.css,
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Parse popup configuration
|
|
161
|
+
*/
|
|
162
|
+
private parsePopup(): ManifestInfo["popup"] {
|
|
163
|
+
const action = this.manifestData.action || this.manifestData.browser_action;
|
|
164
|
+
if (!action) return undefined;
|
|
165
|
+
|
|
166
|
+
if (action.default_popup) {
|
|
167
|
+
return {
|
|
168
|
+
html: action.default_popup,
|
|
169
|
+
default: true,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Parse options configuration
|
|
178
|
+
*/
|
|
179
|
+
private parseOptions(): ManifestInfo["options"] {
|
|
180
|
+
const options = this.manifestData.options_ui || this.manifestData.options_page;
|
|
181
|
+
if (!options) return undefined;
|
|
182
|
+
|
|
183
|
+
if (typeof options === "string") {
|
|
184
|
+
return {
|
|
185
|
+
page: options,
|
|
186
|
+
openInTab: false,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
page: options.page,
|
|
192
|
+
openInTab: options.open_in_tab,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Parse devtools configuration
|
|
198
|
+
*/
|
|
199
|
+
private parseDevtools(): ManifestInfo["devtools"] {
|
|
200
|
+
const devtools = this.manifestData.devtools_page;
|
|
201
|
+
if (!devtools) return undefined;
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
page: devtools,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Find source file from manifest reference
|
|
210
|
+
* Tries multiple locations: exact path, src/ directory, .ts extension
|
|
211
|
+
*/
|
|
212
|
+
private findSourceFile(manifestPath: string): string {
|
|
213
|
+
const candidates = [
|
|
214
|
+
// Exact path from manifest
|
|
215
|
+
path.join(this.baseDir, manifestPath),
|
|
216
|
+
// Same path with .ts extension
|
|
217
|
+
path.join(this.baseDir, manifestPath.replace(/\.js$/, ".ts")),
|
|
218
|
+
// In src/ directory
|
|
219
|
+
path.join(this.baseDir, "src", manifestPath),
|
|
220
|
+
path.join(this.baseDir, "src", manifestPath.replace(/\.js$/, ".ts")),
|
|
221
|
+
// In src/ with .tsx extension
|
|
222
|
+
path.join(this.baseDir, "src", manifestPath.replace(/\.js$/, ".tsx")),
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
for (const candidate of candidates) {
|
|
226
|
+
if (fs.existsSync(candidate)) {
|
|
227
|
+
return candidate;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Fallback to manifest path (will error later if not found)
|
|
232
|
+
return path.join(this.baseDir, manifestPath);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Find associated JavaScript/TypeScript file for an HTML file
|
|
237
|
+
*/
|
|
238
|
+
private findAssociatedJS(htmlPath: string): string | null {
|
|
239
|
+
if (!fs.existsSync(htmlPath)) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Read HTML and look for script tags
|
|
244
|
+
const html = fs.readFileSync(htmlPath, "utf-8");
|
|
245
|
+
const scriptMatch = html.match(/<script[^>]+src=["']([^"']+)["']/i);
|
|
246
|
+
|
|
247
|
+
if (scriptMatch && scriptMatch[1]) {
|
|
248
|
+
const scriptPath = scriptMatch[1];
|
|
249
|
+
const fullPath = path.resolve(path.dirname(htmlPath), scriptPath);
|
|
250
|
+
|
|
251
|
+
// Try with and without .js/.ts extension
|
|
252
|
+
if (fs.existsSync(fullPath)) return fullPath;
|
|
253
|
+
if (fs.existsSync(fullPath.replace(/\.js$/, ".ts"))) {
|
|
254
|
+
return fullPath.replace(/\.js$/, ".ts");
|
|
255
|
+
}
|
|
256
|
+
if (fs.existsSync(fullPath.replace(/\.js$/, ".tsx"))) {
|
|
257
|
+
return fullPath.replace(/\.js$/, ".tsx");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Fallback: look for convention-based files
|
|
262
|
+
const baseName = path.basename(htmlPath, ".html");
|
|
263
|
+
const dir = path.dirname(htmlPath);
|
|
264
|
+
|
|
265
|
+
const candidates = [
|
|
266
|
+
path.join(dir, `${baseName}.ts`),
|
|
267
|
+
path.join(dir, `${baseName}.tsx`),
|
|
268
|
+
path.join(dir, `${baseName}.js`),
|
|
269
|
+
path.join(dir, "index.ts"),
|
|
270
|
+
path.join(dir, "index.tsx"),
|
|
271
|
+
path.join(dir, "index.js"),
|
|
272
|
+
];
|
|
273
|
+
|
|
274
|
+
for (const candidate of candidates) {
|
|
275
|
+
if (fs.existsSync(candidate)) {
|
|
276
|
+
return candidate;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Parse manifest.json and extract information
|
|
286
|
+
*/
|
|
287
|
+
export function parseManifest(projectRoot: string): ManifestInfo {
|
|
288
|
+
const parser = new ManifestParser(projectRoot);
|
|
289
|
+
return parser.parse();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Get context entry points from manifest.json
|
|
294
|
+
*/
|
|
295
|
+
export function getContextEntryPoints(projectRoot: string): Record<string, string> {
|
|
296
|
+
const parser = new ManifestParser(projectRoot);
|
|
297
|
+
return parser.getContextEntryPoints();
|
|
298
|
+
}
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// Type extraction from TypeScript using ts-morph
|
|
2
|
+
|
|
3
|
+
import { Project, SourceFile, Type, TypeFormatFlags } from "ts-morph";
|
|
4
|
+
import type { TypeInfo, TypeKind, FieldAnalysis, Confidence, CodebaseAnalysis } from "../types";
|
|
5
|
+
import { HandlerExtractor } from "./handlers";
|
|
6
|
+
|
|
7
|
+
export class TypeExtractor {
|
|
8
|
+
private project: Project;
|
|
9
|
+
|
|
10
|
+
constructor(tsConfigPath: string) {
|
|
11
|
+
this.project = new Project({
|
|
12
|
+
tsConfigFilePath: tsConfigPath,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Analyze the codebase and extract state types and message types
|
|
18
|
+
*/
|
|
19
|
+
async analyzeCodebase(stateFilePath?: string): Promise<CodebaseAnalysis> {
|
|
20
|
+
// Find state type
|
|
21
|
+
const stateType = stateFilePath ? this.extractStateType(stateFilePath) : this.findStateType();
|
|
22
|
+
|
|
23
|
+
// Find message types
|
|
24
|
+
const messageTypes = this.findMessageTypes();
|
|
25
|
+
|
|
26
|
+
// Analyze fields
|
|
27
|
+
const fields = stateType ? this.analyzeFields(stateType) : [];
|
|
28
|
+
|
|
29
|
+
// Extract message handlers
|
|
30
|
+
const configFilePath = this.project.getCompilerOptions().configFilePath;
|
|
31
|
+
const tsConfigPath = typeof configFilePath === "string" ? configFilePath : "tsconfig.json";
|
|
32
|
+
const handlerExtractor = new HandlerExtractor(tsConfigPath);
|
|
33
|
+
const handlerAnalysis = handlerExtractor.extractHandlers();
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
stateType,
|
|
37
|
+
messageTypes: Array.from(new Set([...messageTypes, ...handlerAnalysis.messageTypes])),
|
|
38
|
+
fields,
|
|
39
|
+
handlers: handlerAnalysis.handlers,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extract state type from a specific file
|
|
45
|
+
*/
|
|
46
|
+
private extractStateType(filePath: string): TypeInfo | null {
|
|
47
|
+
const sourceFile = this.project.getSourceFile(filePath);
|
|
48
|
+
if (!sourceFile) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Look for type alias named "AppState" or similar
|
|
53
|
+
const typeAlias =
|
|
54
|
+
sourceFile.getTypeAlias("AppState") ||
|
|
55
|
+
sourceFile.getTypeAlias("State") ||
|
|
56
|
+
sourceFile.getTypeAliases()[0];
|
|
57
|
+
|
|
58
|
+
if (!typeAlias) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const type = typeAlias.getType();
|
|
63
|
+
return this.convertType(type, typeAlias.getName());
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Find state type by searching common patterns
|
|
68
|
+
*/
|
|
69
|
+
private findStateType(): TypeInfo | null {
|
|
70
|
+
// Search for files with "state" in the name
|
|
71
|
+
const stateFiles = this.project.getSourceFiles("**/state*.ts");
|
|
72
|
+
|
|
73
|
+
for (const file of stateFiles) {
|
|
74
|
+
const typeAlias = file.getTypeAlias("AppState") || file.getTypeAlias("State");
|
|
75
|
+
|
|
76
|
+
if (typeAlias) {
|
|
77
|
+
const type = typeAlias.getType();
|
|
78
|
+
return this.convertType(type, typeAlias.getName());
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Find message types by searching for type unions
|
|
87
|
+
*/
|
|
88
|
+
private findMessageTypes(): string[] {
|
|
89
|
+
const messageTypes: string[] = [];
|
|
90
|
+
|
|
91
|
+
// Search for files with "message" in the name
|
|
92
|
+
const messageFiles = this.project.getSourceFiles("**/message*.ts");
|
|
93
|
+
|
|
94
|
+
for (const file of messageFiles) {
|
|
95
|
+
// Look for type aliases that are unions
|
|
96
|
+
for (const typeAlias of file.getTypeAliases()) {
|
|
97
|
+
const type = typeAlias.getType();
|
|
98
|
+
if (type.isUnion()) {
|
|
99
|
+
// Extract message type literals
|
|
100
|
+
for (const unionType of type.getUnionTypes()) {
|
|
101
|
+
if (unionType.isObject()) {
|
|
102
|
+
const typeProperty = unionType.getProperty("type");
|
|
103
|
+
if (typeProperty) {
|
|
104
|
+
const typeType = typeProperty.getTypeAtLocation(file);
|
|
105
|
+
if (typeType.isStringLiteral()) {
|
|
106
|
+
messageTypes.push(typeType.getLiteralValue() as string);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return [...new Set(messageTypes)]; // Dedupe
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Convert ts-morph Type to our TypeInfo
|
|
120
|
+
*/
|
|
121
|
+
private convertType(type: Type, name: string): TypeInfo {
|
|
122
|
+
// Check for null/undefined
|
|
123
|
+
const nullable = type.isNullable();
|
|
124
|
+
|
|
125
|
+
// Boolean
|
|
126
|
+
if (type.isBoolean() || type.isBooleanLiteral()) {
|
|
127
|
+
return { name, kind: "boolean", nullable };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Union types
|
|
131
|
+
if (type.isUnion()) {
|
|
132
|
+
const unionTypes = type.getUnionTypes();
|
|
133
|
+
|
|
134
|
+
// Check for string literal union (enum)
|
|
135
|
+
const allStringLiterals = unionTypes.every((t) => t.isStringLiteral());
|
|
136
|
+
if (allStringLiterals) {
|
|
137
|
+
const enumValues = unionTypes.map((t) => t.getLiteralValue() as string);
|
|
138
|
+
return {
|
|
139
|
+
name,
|
|
140
|
+
kind: "enum",
|
|
141
|
+
nullable,
|
|
142
|
+
enumValues,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check for nullable type (T | null | undefined)
|
|
147
|
+
const nonNullTypes = unionTypes.filter((t) => !t.isNull() && !t.isUndefined());
|
|
148
|
+
|
|
149
|
+
if (nonNullTypes.length === 1) {
|
|
150
|
+
// This is a nullable type: T | null or T | undefined
|
|
151
|
+
const baseType = this.convertType(nonNullTypes[0], name);
|
|
152
|
+
return {
|
|
153
|
+
...baseType,
|
|
154
|
+
nullable: true,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Generic union - keep as-is
|
|
159
|
+
return {
|
|
160
|
+
name,
|
|
161
|
+
kind: "union",
|
|
162
|
+
nullable,
|
|
163
|
+
unionTypes: unionTypes.map((t, i) => this.convertType(t, `${name}_${i}`)),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// String
|
|
168
|
+
if (type.isString() || type.isStringLiteral()) {
|
|
169
|
+
return { name, kind: "string", nullable };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Number
|
|
173
|
+
if (type.isNumber() || type.isNumberLiteral()) {
|
|
174
|
+
return { name, kind: "number", nullable };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Array
|
|
178
|
+
if (type.isArray()) {
|
|
179
|
+
const elementType = type.getArrayElementType();
|
|
180
|
+
return {
|
|
181
|
+
name,
|
|
182
|
+
kind: "array",
|
|
183
|
+
nullable,
|
|
184
|
+
elementType: elementType
|
|
185
|
+
? this.convertType(elementType, `${name}_element`)
|
|
186
|
+
: { name: "unknown", kind: "unknown", nullable: false },
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Map/Set detection - must come before generic object handling
|
|
191
|
+
const symbol = type.getSymbol();
|
|
192
|
+
if (symbol) {
|
|
193
|
+
const symbolName = symbol.getName();
|
|
194
|
+
|
|
195
|
+
// Map<K, V>
|
|
196
|
+
if (symbolName === "Map") {
|
|
197
|
+
const typeArgs = type.getTypeArguments();
|
|
198
|
+
return {
|
|
199
|
+
name,
|
|
200
|
+
kind: "map",
|
|
201
|
+
nullable,
|
|
202
|
+
// Extract value type from Map<K, V>
|
|
203
|
+
valueType:
|
|
204
|
+
typeArgs && typeArgs[1] ? this.convertType(typeArgs[1], `${name}_value`) : undefined,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Set<T>
|
|
209
|
+
if (symbolName === "Set") {
|
|
210
|
+
const typeArgs = type.getTypeArguments();
|
|
211
|
+
return {
|
|
212
|
+
name,
|
|
213
|
+
kind: "set",
|
|
214
|
+
nullable,
|
|
215
|
+
elementType:
|
|
216
|
+
typeArgs && typeArgs[0] ? this.convertType(typeArgs[0], `${name}_element`) : undefined,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Object
|
|
222
|
+
if (type.isObject()) {
|
|
223
|
+
const properties: Record<string, TypeInfo> = {};
|
|
224
|
+
|
|
225
|
+
for (const prop of type.getProperties()) {
|
|
226
|
+
const propName = prop.getName();
|
|
227
|
+
const propType = prop.getTypeAtLocation(this.project.getSourceFiles()[0]);
|
|
228
|
+
properties[propName] = this.convertType(propType, propName);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
name,
|
|
233
|
+
kind: "object",
|
|
234
|
+
nullable,
|
|
235
|
+
properties,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Null
|
|
240
|
+
if (type.isNull()) {
|
|
241
|
+
return { name, kind: "null", nullable: true };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Unknown/Any
|
|
245
|
+
return { name, kind: "unknown", nullable };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Analyze fields and determine confidence/bounds
|
|
250
|
+
*/
|
|
251
|
+
private analyzeFields(stateType: TypeInfo, prefix = ""): FieldAnalysis[] {
|
|
252
|
+
const fields: FieldAnalysis[] = [];
|
|
253
|
+
|
|
254
|
+
if (stateType.kind === "object" && stateType.properties) {
|
|
255
|
+
for (const [key, propType] of Object.entries(stateType.properties)) {
|
|
256
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
257
|
+
|
|
258
|
+
// Recursively analyze nested objects (but not Map/Set - they're leaf nodes)
|
|
259
|
+
if (propType.kind === "object") {
|
|
260
|
+
// Don't add intermediate objects as fields, just recurse into them
|
|
261
|
+
fields.push(...this.analyzeFields(propType, path));
|
|
262
|
+
} else {
|
|
263
|
+
// This is a leaf field (or Map/Set), add it for configuration
|
|
264
|
+
const analysis = this.analyzeField(path, propType);
|
|
265
|
+
fields.push(analysis);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return fields;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Analyze a single field and determine configuration needs
|
|
275
|
+
*/
|
|
276
|
+
private analyzeField(path: string, type: TypeInfo): FieldAnalysis {
|
|
277
|
+
const analysis: FieldAnalysis = {
|
|
278
|
+
path,
|
|
279
|
+
type,
|
|
280
|
+
confidence: "low",
|
|
281
|
+
evidence: [],
|
|
282
|
+
suggestions: [],
|
|
283
|
+
bounds: {},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// Boolean - high confidence, no config needed
|
|
287
|
+
if (type.kind === "boolean") {
|
|
288
|
+
analysis.confidence = "high";
|
|
289
|
+
analysis.evidence.push("Boolean type - auto-configured");
|
|
290
|
+
return analysis;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Enum - high confidence
|
|
294
|
+
if (type.kind === "enum" && type.enumValues) {
|
|
295
|
+
analysis.confidence = "high";
|
|
296
|
+
analysis.evidence.push(`Enum with ${type.enumValues.length} values`);
|
|
297
|
+
analysis.bounds!.values = type.enumValues;
|
|
298
|
+
return analysis;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Array - needs manual configuration
|
|
302
|
+
if (type.kind === "array") {
|
|
303
|
+
analysis.confidence = "low";
|
|
304
|
+
analysis.suggestions.push("Choose maxLength: 5 (fast), 10 (balanced), or 20 (thorough)");
|
|
305
|
+
analysis.bounds!.maxLength = undefined;
|
|
306
|
+
|
|
307
|
+
// Try to find bounds in code
|
|
308
|
+
const foundBound = this.findArrayBound(path);
|
|
309
|
+
if (foundBound) {
|
|
310
|
+
analysis.confidence = "medium";
|
|
311
|
+
analysis.evidence.push(`Found array check: ${foundBound.evidence}`);
|
|
312
|
+
analysis.bounds!.maxLength = foundBound.value;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return analysis;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Number - needs manual configuration
|
|
319
|
+
if (type.kind === "number") {
|
|
320
|
+
analysis.confidence = "low";
|
|
321
|
+
analysis.suggestions.push("Provide min and max values based on your application logic");
|
|
322
|
+
analysis.bounds!.min = undefined;
|
|
323
|
+
analysis.bounds!.max = undefined;
|
|
324
|
+
|
|
325
|
+
// Try to find bounds in code
|
|
326
|
+
const foundBound = this.findNumberBound(path);
|
|
327
|
+
if (foundBound) {
|
|
328
|
+
analysis.confidence = "high";
|
|
329
|
+
analysis.evidence.push(`Found comparison: ${foundBound.evidence}`);
|
|
330
|
+
analysis.bounds = { ...analysis.bounds!, ...foundBound.bounds };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return analysis;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// String - needs manual configuration
|
|
337
|
+
if (type.kind === "string") {
|
|
338
|
+
analysis.confidence = "low";
|
|
339
|
+
analysis.suggestions.push(
|
|
340
|
+
'Provide 2-3 example values: ["value1", "value2", "value3"]',
|
|
341
|
+
"Or use { abstract: true } for symbolic verification"
|
|
342
|
+
);
|
|
343
|
+
analysis.bounds!.values = undefined;
|
|
344
|
+
return analysis;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Map/Set - needs manual configuration
|
|
348
|
+
if (type.kind === "map" || type.kind === "set") {
|
|
349
|
+
analysis.confidence = "low";
|
|
350
|
+
analysis.suggestions.push("Provide maxSize (recommended: 3-5)");
|
|
351
|
+
analysis.bounds!.maxSize = undefined;
|
|
352
|
+
return analysis;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return analysis;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Try to find array bounds by searching for length checks
|
|
360
|
+
*/
|
|
361
|
+
private findArrayBound(path: string): { value: number; evidence: string } | null {
|
|
362
|
+
// TODO: Search source code for patterns like:
|
|
363
|
+
// - if (array.length < N)
|
|
364
|
+
// - array.slice(0, N)
|
|
365
|
+
// This would require analyzing the actual usage in code
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Try to find number bounds by searching for comparisons
|
|
371
|
+
*/
|
|
372
|
+
private findNumberBound(
|
|
373
|
+
path: string
|
|
374
|
+
): { bounds: { min?: number; max?: number }; evidence: string } | null {
|
|
375
|
+
// TODO: Search source code for patterns like:
|
|
376
|
+
// - if (counter < 100)
|
|
377
|
+
// - if (value >= 0 && value <= 100)
|
|
378
|
+
// This would require analyzing the actual usage in code
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export async function analyzeCodebase(options: {
|
|
384
|
+
tsConfigPath: string;
|
|
385
|
+
stateFilePath?: string;
|
|
386
|
+
}): Promise<CodebaseAnalysis> {
|
|
387
|
+
const extractor = new TypeExtractor(options.tsConfigPath);
|
|
388
|
+
return extractor.analyzeCodebase(options.stateFilePath);
|
|
389
|
+
}
|