tywrap 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -32
- package/dist/cli.js +70 -5
- package/dist/cli.js.map +1 -1
- package/dist/config/index.d.ts +11 -7
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +62 -20
- package/dist/config/index.js.map +1 -1
- package/dist/core/analyzer.d.ts +1 -0
- package/dist/core/analyzer.d.ts.map +1 -1
- package/dist/core/analyzer.js +46 -12
- package/dist/core/analyzer.js.map +1 -1
- package/dist/core/annotation-parser.d.ts +8 -0
- package/dist/core/annotation-parser.d.ts.map +1 -0
- package/dist/core/annotation-parser.js +409 -0
- package/dist/core/annotation-parser.js.map +1 -0
- package/dist/core/discovery.d.ts +1 -0
- package/dist/core/discovery.d.ts.map +1 -1
- package/dist/core/discovery.js +9 -9
- package/dist/core/discovery.js.map +1 -1
- package/dist/core/generator.d.ts +9 -1
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +738 -104
- package/dist/core/generator.js.map +1 -1
- package/dist/core/mapper.d.ts +9 -7
- package/dist/core/mapper.d.ts.map +1 -1
- package/dist/core/mapper.js +129 -44
- package/dist/core/mapper.js.map +1 -1
- package/dist/dev.d.ts +57 -0
- package/dist/dev.d.ts.map +1 -0
- package/dist/dev.js +743 -0
- package/dist/dev.js.map +1 -0
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/runtime/bridge-core.d.ts +1 -0
- package/dist/runtime/bridge-core.d.ts.map +1 -1
- package/dist/runtime/bridge-core.js +52 -17
- package/dist/runtime/bridge-core.js.map +1 -1
- package/dist/runtime/bridge-protocol.d.ts +17 -0
- package/dist/runtime/bridge-protocol.d.ts.map +1 -1
- package/dist/runtime/bridge-protocol.js +97 -0
- package/dist/runtime/bridge-protocol.js.map +1 -1
- package/dist/runtime/errors.d.ts +10 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/errors.js +9 -0
- package/dist/runtime/errors.js.map +1 -1
- package/dist/runtime/http-io.d.ts.map +1 -1
- package/dist/runtime/http-io.js.map +1 -1
- package/dist/runtime/http.d.ts +1 -0
- package/dist/runtime/http.d.ts.map +1 -1
- package/dist/runtime/http.js +6 -0
- package/dist/runtime/http.js.map +1 -1
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +206 -32
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/optimized-node.d.ts +6 -5
- package/dist/runtime/optimized-node.d.ts.map +1 -1
- package/dist/runtime/optimized-node.js +6 -5
- package/dist/runtime/optimized-node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +4 -0
- package/dist/runtime/pooled-transport.d.ts.map +1 -1
- package/dist/runtime/pooled-transport.js +2 -0
- package/dist/runtime/pooled-transport.js.map +1 -1
- package/dist/runtime/process-io.d.ts +5 -0
- package/dist/runtime/process-io.d.ts.map +1 -1
- package/dist/runtime/process-io.js +64 -30
- package/dist/runtime/process-io.js.map +1 -1
- package/dist/runtime/pyodide-io.d.ts +4 -5
- package/dist/runtime/pyodide-io.d.ts.map +1 -1
- package/dist/runtime/pyodide-io.js +99 -37
- package/dist/runtime/pyodide-io.js.map +1 -1
- package/dist/runtime/safe-codec.d.ts +25 -3
- package/dist/runtime/safe-codec.d.ts.map +1 -1
- package/dist/runtime/safe-codec.js +231 -57
- package/dist/runtime/safe-codec.js.map +1 -1
- package/dist/runtime/transport.d.ts +6 -1
- package/dist/runtime/transport.d.ts.map +1 -1
- package/dist/runtime/transport.js.map +1 -1
- package/dist/runtime/validators.d.ts +0 -11
- package/dist/runtime/validators.d.ts.map +1 -1
- package/dist/runtime/validators.js +13 -3
- package/dist/runtime/validators.js.map +1 -1
- package/dist/runtime/worker-pool.d.ts +17 -0
- package/dist/runtime/worker-pool.d.ts.map +1 -1
- package/dist/runtime/worker-pool.js +97 -22
- package/dist/runtime/worker-pool.js.map +1 -1
- package/dist/types/index.d.ts +51 -8
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts +6 -0
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +237 -331
- package/dist/tywrap.js.map +1 -1
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/cache.js +53 -5
- package/dist/utils/cache.js.map +1 -1
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +10 -16
- package/dist/utils/codec.js.map +1 -1
- package/dist/utils/ir-cache.d.ts +28 -0
- package/dist/utils/ir-cache.d.ts.map +1 -0
- package/dist/utils/ir-cache.js +29 -0
- package/dist/utils/ir-cache.js.map +1 -0
- package/dist/utils/parallel-processor.d.ts +1 -1
- package/dist/utils/parallel-processor.d.ts.map +1 -1
- package/dist/utils/parallel-processor.js +111 -40
- package/dist/utils/parallel-processor.js.map +1 -1
- package/dist/utils/runtime.d.ts +5 -1
- package/dist/utils/runtime.d.ts.map +1 -1
- package/dist/utils/runtime.js +112 -18
- package/dist/utils/runtime.js.map +1 -1
- package/package.json +47 -21
- package/runtime/python_bridge.py +78 -12
- package/src/cli.ts +85 -5
- package/src/config/index.ts +91 -32
- package/src/core/analyzer.ts +51 -15
- package/src/core/annotation-parser.ts +500 -0
- package/src/core/discovery.ts +26 -12
- package/src/core/generator.ts +956 -116
- package/src/core/mapper.ts +158 -51
- package/src/dev.ts +983 -0
- package/src/index.ts +35 -6
- package/src/runtime/bridge-core.ts +58 -18
- package/src/runtime/bridge-protocol.ts +164 -0
- package/src/runtime/errors.ts +14 -0
- package/src/runtime/http-io.ts +2 -1
- package/src/runtime/http.ts +7 -0
- package/src/runtime/node.ts +312 -46
- package/src/runtime/optimized-node.ts +9 -5
- package/src/runtime/pooled-transport.ts +12 -1
- package/src/runtime/process-io.ts +83 -55
- package/src/runtime/pyodide-io.ts +110 -44
- package/src/runtime/safe-codec.ts +290 -82
- package/src/runtime/transport.ts +6 -1
- package/src/runtime/validators.ts +14 -3
- package/src/runtime/worker-pool.ts +120 -24
- package/src/types/global.d.ts +11 -1
- package/src/types/index.ts +63 -8
- package/src/tywrap.ts +296 -361
- package/src/utils/cache.ts +59 -5
- package/src/utils/codec.ts +11 -15
- package/src/utils/ir-cache.ts +51 -0
- package/src/utils/parallel-processor.ts +119 -43
- package/src/utils/runtime.ts +129 -18
package/src/config/index.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import { randomUUID } from 'node:crypto';
|
|
9
9
|
import { existsSync } from 'node:fs';
|
|
10
|
-
import {
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { readFile, rm, writeFile, mkdtemp, symlink } from 'node:fs/promises';
|
|
11
12
|
import { createRequire } from 'node:module';
|
|
12
13
|
import { dirname, extname, resolve } from 'node:path';
|
|
13
14
|
import { pathToFileURL } from 'node:url';
|
|
@@ -16,15 +17,9 @@ import type {
|
|
|
16
17
|
OutputConfig,
|
|
17
18
|
RuntimeConfig,
|
|
18
19
|
PerformanceConfig,
|
|
19
|
-
DevelopmentConfig,
|
|
20
20
|
} from '../types/index.js';
|
|
21
21
|
import { getDefaultPythonPath } from '../utils/python.js';
|
|
22
22
|
|
|
23
|
-
/**
|
|
24
|
-
* Public configuration type. Currently identical to {@link TywrapOptions}.
|
|
25
|
-
*/
|
|
26
|
-
export type TywrapConfig = TywrapOptions;
|
|
27
|
-
|
|
28
23
|
type DeepPartial<T> = {
|
|
29
24
|
[K in keyof T]?: T[K] extends Array<infer U>
|
|
30
25
|
? Array<U>
|
|
@@ -33,19 +28,35 @@ type DeepPartial<T> = {
|
|
|
33
28
|
: T[K];
|
|
34
29
|
};
|
|
35
30
|
|
|
31
|
+
/**
|
|
32
|
+
* User-authored configuration shape accepted by defineConfig() and config files.
|
|
33
|
+
*/
|
|
34
|
+
export type TywrapConfig = DeepPartial<TywrapOptions>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Fully resolved configuration shape returned by createConfig() and resolveConfig().
|
|
38
|
+
*/
|
|
39
|
+
export type ResolvedTywrapConfig = TywrapOptions;
|
|
40
|
+
|
|
36
41
|
/**
|
|
37
42
|
* Default configuration values used when options are not supplied.
|
|
38
43
|
*/
|
|
39
|
-
const DEFAULT_CONFIG:
|
|
44
|
+
const DEFAULT_CONFIG: ResolvedTywrapConfig = {
|
|
40
45
|
pythonModules: {},
|
|
46
|
+
pythonImportPath: [],
|
|
41
47
|
output: { dir: './generated', format: 'esm', declaration: false, sourceMap: false },
|
|
42
48
|
runtime: { node: { pythonPath: getDefaultPythonPath(), timeout: 30000 } },
|
|
43
49
|
performance: { caching: false, batching: false, compression: 'none' },
|
|
44
|
-
development: { hotReload: false, sourceMap: false, validation: 'none' },
|
|
45
50
|
types: { presets: [] },
|
|
46
51
|
debug: false,
|
|
47
52
|
};
|
|
48
53
|
|
|
54
|
+
const LEGACY_DEVELOPMENT_MESSAGE =
|
|
55
|
+
'Legacy config field "development" is no longer supported. Use createBridgeReloader() or startNodeWatchSession() from "tywrap/dev" instead.';
|
|
56
|
+
|
|
57
|
+
const LEGACY_MODULE_WATCH_MESSAGE =
|
|
58
|
+
'Legacy config field "pythonModules.<module>.watch" is no longer supported. Use startNodeWatchSession() from "tywrap/dev" instead.';
|
|
59
|
+
|
|
49
60
|
/**
|
|
50
61
|
* Recursively merge two configuration objects. Arrays are overwritten by
|
|
51
62
|
* overrides instead of concatenated.
|
|
@@ -82,16 +93,38 @@ function safeExists(path: string): boolean {
|
|
|
82
93
|
return existsSync(path);
|
|
83
94
|
}
|
|
84
95
|
|
|
96
|
+
function detectLegacyFields(config: TywrapConfig): void {
|
|
97
|
+
const rawConfig = config as unknown as Record<string, unknown>;
|
|
98
|
+
if (Object.prototype.hasOwnProperty.call(rawConfig, 'development')) {
|
|
99
|
+
throw new Error(LEGACY_DEVELOPMENT_MESSAGE);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const pythonModules = rawConfig.pythonModules;
|
|
103
|
+
if (!isPlainObject(pythonModules)) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const moduleConfig of Object.values(pythonModules)) {
|
|
108
|
+
if (
|
|
109
|
+
isPlainObject(moduleConfig) &&
|
|
110
|
+
Object.prototype.hasOwnProperty.call(moduleConfig, 'watch')
|
|
111
|
+
) {
|
|
112
|
+
throw new Error(LEGACY_MODULE_WATCH_MESSAGE);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
85
117
|
/**
|
|
86
118
|
* Validate configuration values and throw user-friendly errors when invalid.
|
|
87
119
|
*/
|
|
88
|
-
function validateConfig(config:
|
|
120
|
+
function validateConfig(config: ResolvedTywrapConfig): void {
|
|
121
|
+
detectLegacyFields(config);
|
|
89
122
|
const allowedTopLevel = new Set([
|
|
90
123
|
'pythonModules',
|
|
124
|
+
'pythonImportPath',
|
|
91
125
|
'output',
|
|
92
126
|
'runtime',
|
|
93
127
|
'performance',
|
|
94
|
-
'development',
|
|
95
128
|
'types',
|
|
96
129
|
'debug',
|
|
97
130
|
]);
|
|
@@ -105,6 +138,15 @@ function validateConfig(config: TywrapConfig): void {
|
|
|
105
138
|
if (typeof out.dir !== 'string') {
|
|
106
139
|
throw new Error('output.dir must be a string');
|
|
107
140
|
}
|
|
141
|
+
|
|
142
|
+
if (config.pythonImportPath !== undefined) {
|
|
143
|
+
if (
|
|
144
|
+
!Array.isArray(config.pythonImportPath) ||
|
|
145
|
+
config.pythonImportPath.some(p => typeof p !== 'string')
|
|
146
|
+
) {
|
|
147
|
+
throw new Error('pythonImportPath must be an array of strings');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
108
150
|
if (!['esm', 'cjs', 'both'].includes(out.format)) {
|
|
109
151
|
throw new Error('output.format must be one of "esm", "cjs" or "both"');
|
|
110
152
|
}
|
|
@@ -136,18 +178,6 @@ function validateConfig(config: TywrapConfig): void {
|
|
|
136
178
|
throw new Error(`performance.compression must be one of ${validCompression.join(', ')}`);
|
|
137
179
|
}
|
|
138
180
|
|
|
139
|
-
const dev: DevelopmentConfig = config.development;
|
|
140
|
-
if (typeof dev.hotReload !== 'boolean') {
|
|
141
|
-
throw new Error('development.hotReload must be a boolean');
|
|
142
|
-
}
|
|
143
|
-
if (typeof dev.sourceMap !== 'boolean') {
|
|
144
|
-
throw new Error('development.sourceMap must be a boolean');
|
|
145
|
-
}
|
|
146
|
-
const validValidation = ['runtime', 'compile', 'both', 'none'];
|
|
147
|
-
if (!validValidation.includes(dev.validation)) {
|
|
148
|
-
throw new Error(`development.validation must be one of ${validValidation.join(', ')}`);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
181
|
if (config.types) {
|
|
152
182
|
const presets = config.types.presets;
|
|
153
183
|
const validPresets = new Set([
|
|
@@ -183,7 +213,7 @@ function validateConfig(config: TywrapConfig): void {
|
|
|
183
213
|
*
|
|
184
214
|
* @param overrides CLI or programmatic overrides
|
|
185
215
|
*/
|
|
186
|
-
export function createConfig(overrides: DeepPartial<TywrapOptions> = {}):
|
|
216
|
+
export function createConfig(overrides: DeepPartial<TywrapOptions> = {}): ResolvedTywrapConfig {
|
|
187
217
|
const merged = merge(DEFAULT_CONFIG, overrides);
|
|
188
218
|
validateConfig(merged);
|
|
189
219
|
return merged;
|
|
@@ -200,7 +230,9 @@ export interface ResolveConfigOptions {
|
|
|
200
230
|
* Resolve configuration by loading a config file (JSON/JS/TS) and merging
|
|
201
231
|
* defaults with any overrides.
|
|
202
232
|
*/
|
|
203
|
-
export async function resolveConfig(
|
|
233
|
+
export async function resolveConfig(
|
|
234
|
+
options: ResolveConfigOptions = {}
|
|
235
|
+
): Promise<ResolvedTywrapConfig> {
|
|
204
236
|
const cwd = options.cwd ?? process.cwd();
|
|
205
237
|
const overrides = options.overrides ?? {};
|
|
206
238
|
const configFile = options.configFile ? resolve(cwd, options.configFile) : undefined;
|
|
@@ -272,6 +304,20 @@ export async function loadConfigFile(configFile: string): Promise<Partial<Tywrap
|
|
|
272
304
|
fileName: resolved,
|
|
273
305
|
});
|
|
274
306
|
|
|
307
|
+
let transpiledOutput = output.outputText;
|
|
308
|
+
if (!emitCommonJs) {
|
|
309
|
+
try {
|
|
310
|
+
// Preserve support for `import { defineConfig } from 'tywrap'` when ESM config
|
|
311
|
+
// is evaluated from an OS temp directory outside the package scope.
|
|
312
|
+
const tywrapEntryHref = await import.meta.resolve('tywrap');
|
|
313
|
+
transpiledOutput = transpiledOutput
|
|
314
|
+
.replaceAll("'tywrap'", `'${tywrapEntryHref}'`)
|
|
315
|
+
.replaceAll('\"tywrap\"', `\"${tywrapEntryHref}\"`);
|
|
316
|
+
} catch {
|
|
317
|
+
// Best-effort: leave source as-is when tywrap cannot be resolved.
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
275
321
|
if (emitCommonJs) {
|
|
276
322
|
// Why: `.cts` is explicitly CommonJS. We evaluate the transpiled output in-memory using
|
|
277
323
|
// Node's Module internals (`Module._compile` / `_nodeModulePaths`) to avoid writing an extra
|
|
@@ -309,18 +355,31 @@ export async function loadConfigFile(configFile: string): Promise<Partial<Tywrap
|
|
|
309
355
|
return ensureConfigObject(loaded ?? {}, resolved);
|
|
310
356
|
}
|
|
311
357
|
|
|
312
|
-
// Why: Node can't import ESM from a string without a custom loader. We write
|
|
313
|
-
// output to a temporary `.mjs` file
|
|
314
|
-
//
|
|
315
|
-
const
|
|
358
|
+
// Why: Node can't import ESM from a string without a custom loader. We write transpiled
|
|
359
|
+
// output to a temporary `.mjs` file under the OS temp directory (not next to user config),
|
|
360
|
+
// then clean up both file and temporary directory after loading.
|
|
361
|
+
const tempDir = await mkdtemp(resolve(tmpdir(), 'tywrap-config-'));
|
|
362
|
+
const tmpPath = resolve(tempDir, `.tywrap.config.${randomUUID()}.mjs`);
|
|
316
363
|
try {
|
|
317
|
-
|
|
318
|
-
|
|
364
|
+
const localNodeModules = resolve(process.cwd(), 'node_modules');
|
|
365
|
+
if (safeExists(localNodeModules)) {
|
|
366
|
+
const tempNodeModules = resolve(tempDir, 'node_modules');
|
|
367
|
+
try {
|
|
368
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- temp path is derived from OS temp directory
|
|
369
|
+
await symlink(localNodeModules, tempNodeModules, 'dir');
|
|
370
|
+
} catch {
|
|
371
|
+
// Best-effort only; regular resolution may still succeed without a symlink.
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- temp path is derived from OS temp directory
|
|
376
|
+
await writeFile(tmpPath, transpiledOutput, 'utf-8');
|
|
319
377
|
const mod = (await import(pathToFileURL(tmpPath).href)) as Record<string, unknown>;
|
|
320
378
|
const loaded = mod.default ?? mod;
|
|
321
379
|
return ensureConfigObject(loaded ?? {}, resolved);
|
|
322
380
|
} finally {
|
|
323
381
|
await rm(tmpPath, { force: true });
|
|
382
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
324
383
|
}
|
|
325
384
|
}
|
|
326
385
|
|
|
@@ -335,6 +394,6 @@ async function safeReadFileAsync(path: string): Promise<string> {
|
|
|
335
394
|
/**
|
|
336
395
|
* Type helper for authoring tywrap configs with full type inference.
|
|
337
396
|
*/
|
|
338
|
-
export function defineConfig(config:
|
|
397
|
+
export function defineConfig<T extends TywrapConfig>(config: T): T {
|
|
339
398
|
return config;
|
|
340
399
|
}
|
package/src/core/analyzer.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { getComponentLogger } from '../utils/logger.js';
|
|
|
26
26
|
const log = getComponentLogger('Analyzer');
|
|
27
27
|
|
|
28
28
|
const UNKNOWN_TYPE: PythonType = { kind: 'custom', name: 'Any', module: 'typing' };
|
|
29
|
+
const PYTHON_LANGUAGE = Python as unknown as Parameters<Parser['setLanguage']>[0];
|
|
29
30
|
|
|
30
31
|
export class PyAnalyzer {
|
|
31
32
|
private parser: Parser;
|
|
@@ -44,7 +45,7 @@ export class PyAnalyzer {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
try {
|
|
47
|
-
await this.parser.setLanguage(
|
|
48
|
+
await this.parser.setLanguage(PYTHON_LANGUAGE);
|
|
48
49
|
this.initialized = true;
|
|
49
50
|
} catch (error) {
|
|
50
51
|
throw new Error(`Failed to initialize Python parser: ${error}`);
|
|
@@ -141,7 +142,7 @@ export class PyAnalyzer {
|
|
|
141
142
|
async extractFunctions(node: SyntaxNode): Promise<PythonFunction[]> {
|
|
142
143
|
const functions: PythonFunction[] = [];
|
|
143
144
|
|
|
144
|
-
const functionNodes = this.
|
|
145
|
+
const functionNodes = this.findDirectDefinitions(node, 'function_definition');
|
|
145
146
|
|
|
146
147
|
for (const funcNode of functionNodes) {
|
|
147
148
|
try {
|
|
@@ -161,7 +162,7 @@ export class PyAnalyzer {
|
|
|
161
162
|
async extractClasses(node: SyntaxNode): Promise<PythonClass[]> {
|
|
162
163
|
const classes: PythonClass[] = [];
|
|
163
164
|
|
|
164
|
-
const classNodes = this.
|
|
165
|
+
const classNodes = this.findDirectDefinitions(node, 'class_definition');
|
|
165
166
|
|
|
166
167
|
for (const classNode of classNodes) {
|
|
167
168
|
try {
|
|
@@ -238,7 +239,7 @@ export class PyAnalyzer {
|
|
|
238
239
|
const properties: Property[] = [];
|
|
239
240
|
|
|
240
241
|
if (bodyNode) {
|
|
241
|
-
const methodNodes = this.
|
|
242
|
+
const methodNodes = this.findDirectDefinitions(bodyNode, 'function_definition');
|
|
242
243
|
for (const methodNode of methodNodes) {
|
|
243
244
|
try {
|
|
244
245
|
const method = await this.extractFunction(methodNode);
|
|
@@ -249,9 +250,18 @@ export class PyAnalyzer {
|
|
|
249
250
|
}
|
|
250
251
|
|
|
251
252
|
// Extract properties (assignments with type annotations or @property decorators)
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
const
|
|
253
|
+
for (const stmt of bodyNode.namedChildren) {
|
|
254
|
+
// Only consider class-body statements, never descend into method bodies.
|
|
255
|
+
const assignmentNode =
|
|
256
|
+
stmt.type === 'assignment'
|
|
257
|
+
? stmt
|
|
258
|
+
: stmt.type === 'expression_statement'
|
|
259
|
+
? (stmt.namedChildren.find(c => c.type === 'assignment') ?? null)
|
|
260
|
+
: null;
|
|
261
|
+
if (!assignmentNode) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const prop = this.extractProperty(assignmentNode);
|
|
255
265
|
if (prop) {
|
|
256
266
|
properties.push(prop);
|
|
257
267
|
}
|
|
@@ -641,21 +651,45 @@ export class PyAnalyzer {
|
|
|
641
651
|
*/
|
|
642
652
|
private findNodesByType(node: SyntaxNode, type: string): SyntaxNode[] {
|
|
643
653
|
const results: SyntaxNode[] = [];
|
|
644
|
-
|
|
645
|
-
|
|
654
|
+
const stack: SyntaxNode[] = [node];
|
|
655
|
+
while (stack.length > 0) {
|
|
656
|
+
const n = stack.pop();
|
|
657
|
+
if (!n) {
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
646
660
|
if (n.type === type) {
|
|
647
661
|
results.push(n);
|
|
648
662
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
663
|
+
for (let i = n.children.length - 1; i >= 0; i--) {
|
|
664
|
+
const child = n.children[i];
|
|
665
|
+
if (child) {
|
|
666
|
+
stack.push(child);
|
|
667
|
+
}
|
|
652
668
|
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
traverse(node);
|
|
669
|
+
}
|
|
656
670
|
return results;
|
|
657
671
|
}
|
|
658
672
|
|
|
673
|
+
private findDirectDefinitions(
|
|
674
|
+
node: SyntaxNode,
|
|
675
|
+
type: 'function_definition' | 'class_definition'
|
|
676
|
+
): SyntaxNode[] {
|
|
677
|
+
const out: SyntaxNode[] = [];
|
|
678
|
+
for (const child of node.namedChildren) {
|
|
679
|
+
if (child.type === type) {
|
|
680
|
+
out.push(child);
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
if (child.type === 'decorated_definition') {
|
|
684
|
+
const def = child.namedChildren.find(c => c.type === type) ?? null;
|
|
685
|
+
if (def) {
|
|
686
|
+
out.push(def);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return out;
|
|
691
|
+
}
|
|
692
|
+
|
|
659
693
|
private extractModuleName(path?: string): string {
|
|
660
694
|
if (!path) {
|
|
661
695
|
return 'unknown';
|
|
@@ -683,6 +717,8 @@ export class PyAnalyzer {
|
|
|
683
717
|
const expr = firstChild.child(0);
|
|
684
718
|
if (expr?.type === 'string') {
|
|
685
719
|
let docstring = expr.text;
|
|
720
|
+
// Strip docstring prefixes like r/u/f/b and combos (rf/fr/br/...).
|
|
721
|
+
docstring = docstring.replace(/^(?:[rRuUbBfF]{1,2})(?=(\"\"\"|'''|\"|'))/, '');
|
|
686
722
|
// Remove outer quotes and any remaining inner quotes
|
|
687
723
|
if (docstring.startsWith('"""') && docstring.endsWith('"""')) {
|
|
688
724
|
docstring = docstring.slice(3, -3);
|