skrypt-ai 0.1.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/LICENSE +21 -0
- package/README.md +200 -0
- package/dist/autofix/index.d.ts +46 -0
- package/dist/autofix/index.js +240 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +40 -0
- package/dist/commands/autofix.d.ts +2 -0
- package/dist/commands/autofix.js +143 -0
- package/dist/commands/generate.d.ts +2 -0
- package/dist/commands/generate.js +320 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +56 -0
- package/dist/commands/review-pr.d.ts +2 -0
- package/dist/commands/review-pr.js +117 -0
- package/dist/commands/watch.d.ts +2 -0
- package/dist/commands/watch.js +142 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +2 -0
- package/dist/config/loader.d.ts +9 -0
- package/dist/config/loader.js +82 -0
- package/dist/config/types.d.ts +24 -0
- package/dist/config/types.js +34 -0
- package/dist/generator/generator.d.ts +15 -0
- package/dist/generator/generator.js +144 -0
- package/dist/generator/index.d.ts +4 -0
- package/dist/generator/index.js +4 -0
- package/dist/generator/organizer.d.ts +29 -0
- package/dist/generator/organizer.js +222 -0
- package/dist/generator/types.d.ts +83 -0
- package/dist/generator/types.js +1 -0
- package/dist/generator/writer.d.ts +28 -0
- package/dist/generator/writer.js +320 -0
- package/dist/github/pr-comments.d.ts +40 -0
- package/dist/github/pr-comments.js +308 -0
- package/dist/llm/anthropic-client.d.ts +16 -0
- package/dist/llm/anthropic-client.js +92 -0
- package/dist/llm/index.d.ts +53 -0
- package/dist/llm/index.js +400 -0
- package/dist/llm/llm.manual-test.d.ts +1 -0
- package/dist/llm/llm.manual-test.js +112 -0
- package/dist/llm/llm.mock-test.d.ts +4 -0
- package/dist/llm/llm.mock-test.js +79 -0
- package/dist/llm/openai-client.d.ts +17 -0
- package/dist/llm/openai-client.js +90 -0
- package/dist/llm/types.d.ts +60 -0
- package/dist/llm/types.js +20 -0
- package/dist/scanner/content-type.d.ts +39 -0
- package/dist/scanner/content-type.js +194 -0
- package/dist/scanner/content-type.test.d.ts +1 -0
- package/dist/scanner/content-type.test.js +231 -0
- package/dist/scanner/go.d.ts +20 -0
- package/dist/scanner/go.js +269 -0
- package/dist/scanner/index.d.ts +21 -0
- package/dist/scanner/index.js +137 -0
- package/dist/scanner/python.d.ts +6 -0
- package/dist/scanner/python.js +57 -0
- package/dist/scanner/python_parser.py +230 -0
- package/dist/scanner/rust.d.ts +23 -0
- package/dist/scanner/rust.js +304 -0
- package/dist/scanner/scanner.test.d.ts +1 -0
- package/dist/scanner/scanner.test.js +210 -0
- package/dist/scanner/types.d.ts +50 -0
- package/dist/scanner/types.js +1 -0
- package/dist/scanner/typescript.d.ts +34 -0
- package/dist/scanner/typescript.js +327 -0
- package/dist/scanner/typescript.manual-test.d.ts +1 -0
- package/dist/scanner/typescript.manual-test.js +112 -0
- package/dist/template/docs.json +32 -0
- package/dist/template/mdx-components.tsx +62 -0
- package/dist/template/next-env.d.ts +6 -0
- package/dist/template/next.config.mjs +17 -0
- package/dist/template/package.json +39 -0
- package/dist/template/postcss.config.mjs +5 -0
- package/dist/template/public/search-index.json +1 -0
- package/dist/template/scripts/build-search-index.mjs +120 -0
- package/dist/template/src/app/api/mock/[...path]/route.ts +224 -0
- package/dist/template/src/app/api/openapi/route.ts +48 -0
- package/dist/template/src/app/api/rate-limit/route.ts +84 -0
- package/dist/template/src/app/docs/[...slug]/page.tsx +81 -0
- package/dist/template/src/app/docs/layout.tsx +9 -0
- package/dist/template/src/app/docs/page.mdx +67 -0
- package/dist/template/src/app/error.tsx +63 -0
- package/dist/template/src/app/layout.tsx +71 -0
- package/dist/template/src/app/page.tsx +18 -0
- package/dist/template/src/app/reference/route.ts +36 -0
- package/dist/template/src/app/robots.ts +14 -0
- package/dist/template/src/app/sitemap.ts +64 -0
- package/dist/template/src/components/breadcrumbs.tsx +41 -0
- package/dist/template/src/components/copy-button.tsx +29 -0
- package/dist/template/src/components/docs-layout.tsx +35 -0
- package/dist/template/src/components/edit-link.tsx +39 -0
- package/dist/template/src/components/feedback.tsx +52 -0
- package/dist/template/src/components/header.tsx +66 -0
- package/dist/template/src/components/mdx/accordion.tsx +48 -0
- package/dist/template/src/components/mdx/api-badge.tsx +57 -0
- package/dist/template/src/components/mdx/callout.tsx +111 -0
- package/dist/template/src/components/mdx/card.tsx +62 -0
- package/dist/template/src/components/mdx/changelog.tsx +57 -0
- package/dist/template/src/components/mdx/code-block.tsx +42 -0
- package/dist/template/src/components/mdx/code-group.tsx +125 -0
- package/dist/template/src/components/mdx/code-playground.tsx +322 -0
- package/dist/template/src/components/mdx/go-playground.tsx +235 -0
- package/dist/template/src/components/mdx/heading.tsx +37 -0
- package/dist/template/src/components/mdx/highlighted-code.tsx +89 -0
- package/dist/template/src/components/mdx/index.tsx +15 -0
- package/dist/template/src/components/mdx/param-table.tsx +71 -0
- package/dist/template/src/components/mdx/python-playground.tsx +293 -0
- package/dist/template/src/components/mdx/steps.tsx +43 -0
- package/dist/template/src/components/mdx/tabs.tsx +81 -0
- package/dist/template/src/components/rate-limit-display.tsx +183 -0
- package/dist/template/src/components/search-dialog.tsx +178 -0
- package/dist/template/src/components/sidebar.tsx +129 -0
- package/dist/template/src/components/syntax-theme-selector.tsx +50 -0
- package/dist/template/src/components/table-of-contents.tsx +84 -0
- package/dist/template/src/components/theme-toggle.tsx +46 -0
- package/dist/template/src/components/version-selector.tsx +61 -0
- package/dist/template/src/contexts/syntax-theme.tsx +52 -0
- package/dist/template/src/lib/highlight.ts +83 -0
- package/dist/template/src/lib/search-types.ts +37 -0
- package/dist/template/src/lib/search.ts +125 -0
- package/dist/template/src/lib/utils.ts +6 -0
- package/dist/template/src/styles/globals.css +152 -0
- package/dist/template/tsconfig.json +25 -0
- package/dist/template/tsconfig.tsbuildinfo +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const PARSER_SCRIPT = join(__dirname, 'python_parser.py');
|
|
6
|
+
export class PythonScanner {
|
|
7
|
+
languages = ['python'];
|
|
8
|
+
canHandle(filePath) {
|
|
9
|
+
return filePath.endsWith('.py');
|
|
10
|
+
}
|
|
11
|
+
async scanFile(filePath) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
const proc = spawn('python3', [PARSER_SCRIPT, filePath], {
|
|
14
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
15
|
+
});
|
|
16
|
+
let stdout = '';
|
|
17
|
+
let stderr = '';
|
|
18
|
+
proc.stdout.on('data', (data) => {
|
|
19
|
+
stdout += data.toString();
|
|
20
|
+
});
|
|
21
|
+
proc.stderr.on('data', (data) => {
|
|
22
|
+
stderr += data.toString();
|
|
23
|
+
});
|
|
24
|
+
proc.on('close', (code) => {
|
|
25
|
+
if (code !== 0 || stderr) {
|
|
26
|
+
resolve({
|
|
27
|
+
filePath,
|
|
28
|
+
language: 'python',
|
|
29
|
+
elements: [],
|
|
30
|
+
errors: [stderr || `Python parser exited with code ${code}`]
|
|
31
|
+
});
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const result = JSON.parse(stdout);
|
|
36
|
+
resolve(result);
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
resolve({
|
|
40
|
+
filePath,
|
|
41
|
+
language: 'python',
|
|
42
|
+
elements: [],
|
|
43
|
+
errors: [`Failed to parse output: ${e}`]
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
proc.on('error', (err) => {
|
|
48
|
+
resolve({
|
|
49
|
+
filePath,
|
|
50
|
+
language: 'python',
|
|
51
|
+
elements: [],
|
|
52
|
+
errors: [`Failed to spawn Python: ${err.message}`]
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Extract API elements from Python files using the ast module.
|
|
4
|
+
Called by the Node.js scanner via subprocess.
|
|
5
|
+
Outputs JSON to stdout.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_docstring(node: ast.AST) -> str | None:
|
|
15
|
+
"""Extract docstring from a node if present."""
|
|
16
|
+
return ast.get_docstring(node)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_type_annotation(annotation: ast.AST | None) -> str | None:
|
|
20
|
+
"""Convert type annotation AST to string."""
|
|
21
|
+
if annotation is None:
|
|
22
|
+
return None
|
|
23
|
+
return ast.unparse(annotation)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_default_value(default: ast.AST | None) -> str | None:
|
|
27
|
+
"""Convert default value AST to string."""
|
|
28
|
+
if default is None:
|
|
29
|
+
return None
|
|
30
|
+
return ast.unparse(default)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def extract_parameters(args: ast.arguments) -> list[dict[str, Any]]:
|
|
34
|
+
"""Extract parameters from function arguments."""
|
|
35
|
+
params = []
|
|
36
|
+
|
|
37
|
+
# Calculate defaults offset (defaults align to end of args)
|
|
38
|
+
num_args = len(args.args)
|
|
39
|
+
num_defaults = len(args.defaults)
|
|
40
|
+
defaults_offset = num_args - num_defaults
|
|
41
|
+
|
|
42
|
+
for i, arg in enumerate(args.args):
|
|
43
|
+
# Skip 'self' and 'cls' for methods
|
|
44
|
+
if arg.arg in ('self', 'cls'):
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
param = {
|
|
48
|
+
'name': arg.arg,
|
|
49
|
+
'type': get_type_annotation(arg.annotation),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Check if this arg has a default
|
|
53
|
+
default_idx = i - defaults_offset
|
|
54
|
+
if default_idx >= 0 and default_idx < len(args.defaults):
|
|
55
|
+
param['default'] = get_default_value(args.defaults[default_idx])
|
|
56
|
+
|
|
57
|
+
params.append(param)
|
|
58
|
+
|
|
59
|
+
# Handle *args
|
|
60
|
+
if args.vararg:
|
|
61
|
+
params.append({
|
|
62
|
+
'name': f'*{args.vararg.arg}',
|
|
63
|
+
'type': get_type_annotation(args.vararg.annotation),
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
# Handle keyword-only args
|
|
67
|
+
for i, arg in enumerate(args.kwonlyargs):
|
|
68
|
+
param = {
|
|
69
|
+
'name': arg.arg,
|
|
70
|
+
'type': get_type_annotation(arg.annotation),
|
|
71
|
+
}
|
|
72
|
+
if i < len(args.kw_defaults) and args.kw_defaults[i]:
|
|
73
|
+
param['default'] = get_default_value(args.kw_defaults[i])
|
|
74
|
+
params.append(param)
|
|
75
|
+
|
|
76
|
+
# Handle **kwargs
|
|
77
|
+
if args.kwarg:
|
|
78
|
+
params.append({
|
|
79
|
+
'name': f'**{args.kwarg.arg}',
|
|
80
|
+
'type': get_type_annotation(args.kwarg.annotation),
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
return params
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def build_signature(name: str, args: ast.arguments, returns: ast.AST | None, is_async: bool) -> str:
|
|
87
|
+
"""Build the function signature as a string."""
|
|
88
|
+
params = []
|
|
89
|
+
|
|
90
|
+
for arg in args.args:
|
|
91
|
+
p = arg.arg
|
|
92
|
+
if arg.annotation:
|
|
93
|
+
p += f': {ast.unparse(arg.annotation)}'
|
|
94
|
+
params.append(p)
|
|
95
|
+
|
|
96
|
+
if args.vararg:
|
|
97
|
+
p = f'*{args.vararg.arg}'
|
|
98
|
+
if args.vararg.annotation:
|
|
99
|
+
p += f': {ast.unparse(args.vararg.annotation)}'
|
|
100
|
+
params.append(p)
|
|
101
|
+
|
|
102
|
+
for i, arg in enumerate(args.kwonlyargs):
|
|
103
|
+
p = arg.arg
|
|
104
|
+
if arg.annotation:
|
|
105
|
+
p += f': {ast.unparse(arg.annotation)}'
|
|
106
|
+
if i < len(args.kw_defaults) and args.kw_defaults[i]:
|
|
107
|
+
p += f' = {ast.unparse(args.kw_defaults[i])}'
|
|
108
|
+
params.append(p)
|
|
109
|
+
|
|
110
|
+
if args.kwarg:
|
|
111
|
+
p = f'**{args.kwarg.arg}'
|
|
112
|
+
if args.kwarg.annotation:
|
|
113
|
+
p += f': {ast.unparse(args.kwarg.annotation)}'
|
|
114
|
+
params.append(p)
|
|
115
|
+
|
|
116
|
+
sig = f"{'async ' if is_async else ''}def {name}({', '.join(params)})"
|
|
117
|
+
if returns:
|
|
118
|
+
sig += f' -> {ast.unparse(returns)}'
|
|
119
|
+
|
|
120
|
+
return sig
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def extract_function(node: ast.FunctionDef | ast.AsyncFunctionDef, file_path: str, parent_class: str | None = None) -> dict[str, Any]:
|
|
124
|
+
"""Extract a function or method."""
|
|
125
|
+
is_async = isinstance(node, ast.AsyncFunctionDef)
|
|
126
|
+
is_method = parent_class is not None
|
|
127
|
+
is_public = not node.name.startswith('_') or node.name.startswith('__') and node.name.endswith('__')
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
'kind': 'method' if is_method else 'function',
|
|
131
|
+
'name': node.name,
|
|
132
|
+
'signature': build_signature(node.name, node.args, node.returns, is_async),
|
|
133
|
+
'parameters': extract_parameters(node.args),
|
|
134
|
+
'returnType': get_type_annotation(node.returns),
|
|
135
|
+
'docstring': get_docstring(node),
|
|
136
|
+
'filePath': file_path,
|
|
137
|
+
'lineNumber': node.lineno,
|
|
138
|
+
'parentClass': parent_class,
|
|
139
|
+
'isAsync': is_async,
|
|
140
|
+
'isPublic': is_public,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def extract_class(node: ast.ClassDef, file_path: str) -> list[dict[str, Any]]:
|
|
145
|
+
"""Extract a class and its methods."""
|
|
146
|
+
elements = []
|
|
147
|
+
is_public = not node.name.startswith('_')
|
|
148
|
+
|
|
149
|
+
# Add the class itself
|
|
150
|
+
bases = [ast.unparse(b) for b in node.bases]
|
|
151
|
+
signature = f"class {node.name}"
|
|
152
|
+
if bases:
|
|
153
|
+
signature += f"({', '.join(bases)})"
|
|
154
|
+
|
|
155
|
+
elements.append({
|
|
156
|
+
'kind': 'class',
|
|
157
|
+
'name': node.name,
|
|
158
|
+
'signature': signature,
|
|
159
|
+
'parameters': [], # Classes don't have parameters in signature
|
|
160
|
+
'docstring': get_docstring(node),
|
|
161
|
+
'filePath': file_path,
|
|
162
|
+
'lineNumber': node.lineno,
|
|
163
|
+
'isPublic': is_public,
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
# Extract methods
|
|
167
|
+
for item in node.body:
|
|
168
|
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
169
|
+
# Skip private methods unless they're dunder methods
|
|
170
|
+
if item.name.startswith('_') and not (item.name.startswith('__') and item.name.endswith('__')):
|
|
171
|
+
continue
|
|
172
|
+
elements.append(extract_function(item, file_path, parent_class=node.name))
|
|
173
|
+
|
|
174
|
+
return elements
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def scan_file(file_path: str) -> dict[str, Any]:
|
|
178
|
+
"""Scan a Python file and extract all API elements."""
|
|
179
|
+
try:
|
|
180
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
181
|
+
source = f.read()
|
|
182
|
+
|
|
183
|
+
tree = ast.parse(source, filename=file_path)
|
|
184
|
+
except SyntaxError as e:
|
|
185
|
+
return {
|
|
186
|
+
'filePath': file_path,
|
|
187
|
+
'language': 'python',
|
|
188
|
+
'elements': [],
|
|
189
|
+
'errors': [f'Syntax error: {e}']
|
|
190
|
+
}
|
|
191
|
+
except Exception as e:
|
|
192
|
+
return {
|
|
193
|
+
'filePath': file_path,
|
|
194
|
+
'language': 'python',
|
|
195
|
+
'elements': [],
|
|
196
|
+
'errors': [str(e)]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
elements = []
|
|
200
|
+
|
|
201
|
+
for node in ast.iter_child_nodes(tree):
|
|
202
|
+
# Top-level functions
|
|
203
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
204
|
+
# Skip private functions
|
|
205
|
+
if node.name.startswith('_') and not (node.name.startswith('__') and node.name.endswith('__')):
|
|
206
|
+
continue
|
|
207
|
+
elements.append(extract_function(node, file_path))
|
|
208
|
+
|
|
209
|
+
# Classes
|
|
210
|
+
elif isinstance(node, ast.ClassDef):
|
|
211
|
+
# Skip private classes
|
|
212
|
+
if node.name.startswith('_'):
|
|
213
|
+
continue
|
|
214
|
+
elements.extend(extract_class(node, file_path))
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
'filePath': file_path,
|
|
218
|
+
'language': 'python',
|
|
219
|
+
'elements': elements,
|
|
220
|
+
'errors': []
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
if __name__ == '__main__':
|
|
225
|
+
if len(sys.argv) != 2:
|
|
226
|
+
print(json.dumps({'error': 'Usage: python_parser.py <file_path>'}))
|
|
227
|
+
sys.exit(1)
|
|
228
|
+
|
|
229
|
+
result = scan_file(sys.argv[1])
|
|
230
|
+
print(json.dumps(result, indent=2))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Scanner, ScanResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Scanner for Rust source files
|
|
4
|
+
* Extracts: pub functions, pub structs, pub enums, impl blocks, traits
|
|
5
|
+
*/
|
|
6
|
+
export declare class RustScanner implements Scanner {
|
|
7
|
+
languages: string[];
|
|
8
|
+
canHandle(filePath: string): boolean;
|
|
9
|
+
scanFile(filePath: string): Promise<ScanResult>;
|
|
10
|
+
private inferCrateName;
|
|
11
|
+
private extractUseStatements;
|
|
12
|
+
private extractFunctions;
|
|
13
|
+
private isTopLevelFn;
|
|
14
|
+
private extractStructsAndEnums;
|
|
15
|
+
private extractTraits;
|
|
16
|
+
private extractImplMethods;
|
|
17
|
+
private extractBlock;
|
|
18
|
+
private parseRustParams;
|
|
19
|
+
private splitParams;
|
|
20
|
+
private getLineNumber;
|
|
21
|
+
private getDocComment;
|
|
22
|
+
private getSourceContext;
|
|
23
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
/**
|
|
3
|
+
* Scanner for Rust source files
|
|
4
|
+
* Extracts: pub functions, pub structs, pub enums, impl blocks, traits
|
|
5
|
+
*/
|
|
6
|
+
export class RustScanner {
|
|
7
|
+
languages = ['rust'];
|
|
8
|
+
canHandle(filePath) {
|
|
9
|
+
return /\.rs$/.test(filePath) && !filePath.includes('/tests/');
|
|
10
|
+
}
|
|
11
|
+
async scanFile(filePath) {
|
|
12
|
+
try {
|
|
13
|
+
const source = readFileSync(filePath, 'utf-8');
|
|
14
|
+
const elements = [];
|
|
15
|
+
const errors = [];
|
|
16
|
+
const lines = source.split('\n');
|
|
17
|
+
// Infer crate/module name from path
|
|
18
|
+
const packageName = this.inferCrateName(filePath);
|
|
19
|
+
// Extract use statements for context
|
|
20
|
+
const imports = this.extractUseStatements(source);
|
|
21
|
+
// Find all pub items
|
|
22
|
+
this.extractFunctions(source, lines, filePath, packageName, imports, elements);
|
|
23
|
+
this.extractStructsAndEnums(source, lines, filePath, packageName, imports, elements);
|
|
24
|
+
this.extractTraits(source, lines, filePath, packageName, imports, elements);
|
|
25
|
+
this.extractImplMethods(source, lines, filePath, packageName, imports, elements);
|
|
26
|
+
return {
|
|
27
|
+
filePath,
|
|
28
|
+
language: 'rust',
|
|
29
|
+
elements,
|
|
30
|
+
errors
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
return {
|
|
35
|
+
filePath,
|
|
36
|
+
language: 'rust',
|
|
37
|
+
elements: [],
|
|
38
|
+
errors: [`Failed to parse: ${err}`]
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
inferCrateName(filePath) {
|
|
43
|
+
// Look for Cargo.toml parent or use directory name
|
|
44
|
+
const parts = filePath.split('/');
|
|
45
|
+
const srcIndex = parts.indexOf('src');
|
|
46
|
+
if (srcIndex > 0) {
|
|
47
|
+
return parts[srcIndex - 1];
|
|
48
|
+
}
|
|
49
|
+
return parts[parts.length - 2] || 'unknown';
|
|
50
|
+
}
|
|
51
|
+
extractUseStatements(source) {
|
|
52
|
+
const uses = [];
|
|
53
|
+
const regex = /^use\s+([^;]+);/gm;
|
|
54
|
+
let match;
|
|
55
|
+
while ((match = regex.exec(source)) !== null) {
|
|
56
|
+
uses.push(match[1]);
|
|
57
|
+
}
|
|
58
|
+
return uses;
|
|
59
|
+
}
|
|
60
|
+
extractFunctions(source, lines, filePath, packageName, imports, elements) {
|
|
61
|
+
// Match: pub fn function_name<generics>(params) -> ReturnType
|
|
62
|
+
// Also handles pub async fn, pub const fn, pub unsafe fn
|
|
63
|
+
const funcRegex = /^(\s*)(pub(?:\s+(?:async|const|unsafe))?\s+fn)\s+(\w+)\s*(<[^>]+>)?\s*\(([^)]*)\)\s*(->\s*[^{]+)?/gm;
|
|
64
|
+
let match;
|
|
65
|
+
while ((match = funcRegex.exec(source)) !== null) {
|
|
66
|
+
const indent = match[1];
|
|
67
|
+
// Skip if indented (inside impl block - handled separately)
|
|
68
|
+
if (indent.length > 0 && !this.isTopLevelFn(source, match.index))
|
|
69
|
+
continue;
|
|
70
|
+
const fnKeyword = match[2];
|
|
71
|
+
const name = match[3];
|
|
72
|
+
const generics = match[4] || '';
|
|
73
|
+
const paramsStr = match[5];
|
|
74
|
+
const returnPart = match[6]?.trim() || '';
|
|
75
|
+
const lineNumber = this.getLineNumber(source, match.index);
|
|
76
|
+
const docstring = this.getDocComment(lines, lineNumber - 1);
|
|
77
|
+
const parameters = this.parseRustParams(paramsStr);
|
|
78
|
+
const returnType = returnPart ? returnPart.replace(/^->\s*/, '').trim() : undefined;
|
|
79
|
+
const isAsync = fnKeyword.includes('async');
|
|
80
|
+
const signature = `${fnKeyword} ${name}${generics}(${paramsStr})${returnPart ? ' ' + returnPart : ''}`;
|
|
81
|
+
elements.push({
|
|
82
|
+
kind: 'function',
|
|
83
|
+
name,
|
|
84
|
+
signature,
|
|
85
|
+
parameters,
|
|
86
|
+
returnType,
|
|
87
|
+
docstring,
|
|
88
|
+
filePath,
|
|
89
|
+
lineNumber,
|
|
90
|
+
isAsync,
|
|
91
|
+
isExported: true,
|
|
92
|
+
isPublic: true,
|
|
93
|
+
imports,
|
|
94
|
+
packageName,
|
|
95
|
+
sourceContext: this.getSourceContext(lines, lineNumber)
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
isTopLevelFn(source, index) {
|
|
100
|
+
// Check if we're inside an impl block by counting brace balance
|
|
101
|
+
const before = source.slice(0, index);
|
|
102
|
+
const braceBalance = (before.match(/\{/g) || []).length - (before.match(/\}/g) || []).length;
|
|
103
|
+
return braceBalance === 0;
|
|
104
|
+
}
|
|
105
|
+
extractStructsAndEnums(source, lines, filePath, packageName, imports, elements) {
|
|
106
|
+
// Match: pub struct StructName<generics> or pub enum EnumName
|
|
107
|
+
const typeRegex = /^pub\s+(struct|enum)\s+(\w+)\s*(<[^>]+>)?/gm;
|
|
108
|
+
let match;
|
|
109
|
+
while ((match = typeRegex.exec(source)) !== null) {
|
|
110
|
+
const kind = match[1];
|
|
111
|
+
const name = match[2];
|
|
112
|
+
const generics = match[3] || '';
|
|
113
|
+
const lineNumber = this.getLineNumber(source, match.index);
|
|
114
|
+
const docstring = this.getDocComment(lines, lineNumber - 1);
|
|
115
|
+
const signature = `pub ${kind} ${name}${generics}`;
|
|
116
|
+
elements.push({
|
|
117
|
+
kind: 'class', // Using 'class' for Rust structs/enums
|
|
118
|
+
name,
|
|
119
|
+
signature,
|
|
120
|
+
parameters: [],
|
|
121
|
+
docstring,
|
|
122
|
+
filePath,
|
|
123
|
+
lineNumber,
|
|
124
|
+
isExported: true,
|
|
125
|
+
isPublic: true,
|
|
126
|
+
imports,
|
|
127
|
+
packageName,
|
|
128
|
+
sourceContext: this.getSourceContext(lines, lineNumber, 20)
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
extractTraits(source, lines, filePath, packageName, imports, elements) {
|
|
133
|
+
// Match: pub trait TraitName<generics>
|
|
134
|
+
const traitRegex = /^pub\s+trait\s+(\w+)\s*(<[^>]+>)?/gm;
|
|
135
|
+
let match;
|
|
136
|
+
while ((match = traitRegex.exec(source)) !== null) {
|
|
137
|
+
const name = match[1];
|
|
138
|
+
const generics = match[2] || '';
|
|
139
|
+
const lineNumber = this.getLineNumber(source, match.index);
|
|
140
|
+
const docstring = this.getDocComment(lines, lineNumber - 1);
|
|
141
|
+
const signature = `pub trait ${name}${generics}`;
|
|
142
|
+
elements.push({
|
|
143
|
+
kind: 'class', // Using 'class' for traits
|
|
144
|
+
name,
|
|
145
|
+
signature,
|
|
146
|
+
parameters: [],
|
|
147
|
+
docstring,
|
|
148
|
+
filePath,
|
|
149
|
+
lineNumber,
|
|
150
|
+
isExported: true,
|
|
151
|
+
isPublic: true,
|
|
152
|
+
imports,
|
|
153
|
+
packageName,
|
|
154
|
+
sourceContext: this.getSourceContext(lines, lineNumber, 15)
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
extractImplMethods(source, lines, filePath, packageName, imports, elements) {
|
|
159
|
+
// Find impl blocks and their methods
|
|
160
|
+
// Match: impl<generics> TypeName { ... }
|
|
161
|
+
// Or: impl<generics> Trait for TypeName { ... }
|
|
162
|
+
const implRegex = /impl\s*(<[^>]+>)?\s*(?:(\w+)\s+for\s+)?(\w+)(?:<[^>]+>)?\s*\{/g;
|
|
163
|
+
let implMatch;
|
|
164
|
+
while ((implMatch = implRegex.exec(source)) !== null) {
|
|
165
|
+
// implMatch[2] contains trait name if implementing a trait (for future use)
|
|
166
|
+
const typeName = implMatch[3];
|
|
167
|
+
const implStart = implMatch.index;
|
|
168
|
+
// Find the matching closing brace
|
|
169
|
+
const implBody = this.extractBlock(source, implStart + implMatch[0].length - 1);
|
|
170
|
+
if (!implBody)
|
|
171
|
+
continue;
|
|
172
|
+
// Extract pub fn methods from the impl body
|
|
173
|
+
const methodRegex = /^\s*(pub(?:\s+(?:async|const|unsafe))?\s+fn)\s+(\w+)\s*(<[^>]+>)?\s*\(([^)]*)\)\s*(->\s*[^{]+)?/gm;
|
|
174
|
+
let methodMatch;
|
|
175
|
+
while ((methodMatch = methodRegex.exec(implBody)) !== null) {
|
|
176
|
+
const fnKeyword = methodMatch[1];
|
|
177
|
+
const name = methodMatch[2];
|
|
178
|
+
const generics = methodMatch[3] || '';
|
|
179
|
+
const paramsStr = methodMatch[4];
|
|
180
|
+
const returnPart = methodMatch[5]?.trim() || '';
|
|
181
|
+
const bodyIndex = implBody.indexOf(methodMatch[0]);
|
|
182
|
+
const fullIndex = implStart + implMatch[0].length + bodyIndex;
|
|
183
|
+
const lineNumber = this.getLineNumber(source, fullIndex);
|
|
184
|
+
const docstring = this.getDocComment(lines, lineNumber - 1);
|
|
185
|
+
const parameters = this.parseRustParams(paramsStr);
|
|
186
|
+
const returnType = returnPart ? returnPart.replace(/^->\s*/, '').trim() : undefined;
|
|
187
|
+
const isAsync = fnKeyword.includes('async');
|
|
188
|
+
const signature = `${fnKeyword} ${name}${generics}(${paramsStr})${returnPart ? ' ' + returnPart : ''}`;
|
|
189
|
+
elements.push({
|
|
190
|
+
kind: 'method',
|
|
191
|
+
name,
|
|
192
|
+
signature,
|
|
193
|
+
parameters,
|
|
194
|
+
returnType,
|
|
195
|
+
docstring,
|
|
196
|
+
filePath,
|
|
197
|
+
lineNumber,
|
|
198
|
+
parentClass: typeName,
|
|
199
|
+
isAsync,
|
|
200
|
+
isExported: true,
|
|
201
|
+
isPublic: true,
|
|
202
|
+
imports,
|
|
203
|
+
packageName,
|
|
204
|
+
sourceContext: this.getSourceContext(lines, lineNumber)
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
extractBlock(source, startIndex) {
|
|
210
|
+
let depth = 0;
|
|
211
|
+
let i = startIndex;
|
|
212
|
+
let start = -1;
|
|
213
|
+
while (i < source.length) {
|
|
214
|
+
if (source[i] === '{') {
|
|
215
|
+
if (depth === 0)
|
|
216
|
+
start = i;
|
|
217
|
+
depth++;
|
|
218
|
+
}
|
|
219
|
+
else if (source[i] === '}') {
|
|
220
|
+
depth--;
|
|
221
|
+
if (depth === 0) {
|
|
222
|
+
return source.slice(start + 1, i);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
i++;
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
parseRustParams(paramsStr) {
|
|
230
|
+
if (!paramsStr.trim())
|
|
231
|
+
return [];
|
|
232
|
+
const params = [];
|
|
233
|
+
const parts = this.splitParams(paramsStr);
|
|
234
|
+
for (const part of parts) {
|
|
235
|
+
const trimmed = part.trim();
|
|
236
|
+
if (!trimmed)
|
|
237
|
+
continue;
|
|
238
|
+
// Skip self, &self, &mut self
|
|
239
|
+
if (/^&?(?:mut\s+)?self$/.test(trimmed))
|
|
240
|
+
continue;
|
|
241
|
+
// Format: "name: Type" or "mut name: Type"
|
|
242
|
+
const colonIdx = trimmed.indexOf(':');
|
|
243
|
+
if (colonIdx > 0) {
|
|
244
|
+
let name = trimmed.slice(0, colonIdx).trim();
|
|
245
|
+
const type = trimmed.slice(colonIdx + 1).trim();
|
|
246
|
+
// Remove mut prefix from name
|
|
247
|
+
name = name.replace(/^mut\s+/, '');
|
|
248
|
+
params.push({ name, type });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return params;
|
|
252
|
+
}
|
|
253
|
+
splitParams(str) {
|
|
254
|
+
const parts = [];
|
|
255
|
+
let depth = 0;
|
|
256
|
+
let current = '';
|
|
257
|
+
for (const char of str) {
|
|
258
|
+
if (char === '<' || char === '(' || char === '[' || char === '{')
|
|
259
|
+
depth++;
|
|
260
|
+
else if (char === '>' || char === ')' || char === ']' || char === '}')
|
|
261
|
+
depth--;
|
|
262
|
+
else if (char === ',' && depth === 0) {
|
|
263
|
+
parts.push(current);
|
|
264
|
+
current = '';
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
current += char;
|
|
268
|
+
}
|
|
269
|
+
if (current.trim())
|
|
270
|
+
parts.push(current);
|
|
271
|
+
return parts;
|
|
272
|
+
}
|
|
273
|
+
getLineNumber(source, index) {
|
|
274
|
+
return source.slice(0, index).split('\n').length;
|
|
275
|
+
}
|
|
276
|
+
getDocComment(lines, lineIndex) {
|
|
277
|
+
const comments = [];
|
|
278
|
+
let i = lineIndex - 1;
|
|
279
|
+
while (i >= 0) {
|
|
280
|
+
const line = lines[i].trim();
|
|
281
|
+
// Rust doc comments: /// or //!
|
|
282
|
+
if (line.startsWith('///') || line.startsWith('//!')) {
|
|
283
|
+
comments.unshift(line.replace(/^\/\/[/!]\s?/, ''));
|
|
284
|
+
i--;
|
|
285
|
+
}
|
|
286
|
+
else if (line.startsWith('#[')) {
|
|
287
|
+
// Skip attributes
|
|
288
|
+
i--;
|
|
289
|
+
}
|
|
290
|
+
else if (line === '') {
|
|
291
|
+
i--;
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return comments.length > 0 ? comments.join('\n') : undefined;
|
|
298
|
+
}
|
|
299
|
+
getSourceContext(lines, lineNumber, context = 5) {
|
|
300
|
+
const start = Math.max(0, lineNumber - context - 1);
|
|
301
|
+
const end = Math.min(lines.length, lineNumber + context);
|
|
302
|
+
return lines.slice(start, end).join('\n');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|