speccore 8.3.121 → 8.3.126
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/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/about.d.ts.map +1 -1
- package/dist/commands/about.js +16 -0
- package/dist/commands/about.js.map +1 -1
- package/dist/commands/analyze.d.ts +1 -0
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +198 -70
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/change.js +14 -7
- package/dist/commands/change.js.map +1 -1
- package/dist/commands/context.js +25 -31
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +6 -1
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/done.js +29 -28
- package/dist/commands/done.js.map +1 -1
- package/dist/commands/execute.d.ts.map +1 -1
- package/dist/commands/execute.js +281 -20
- package/dist/commands/execute.js.map +1 -1
- package/dist/commands/import.js +1 -1
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +512 -28
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/iteration/create.js +1 -1
- package/dist/commands/iteration/split.d.ts.map +1 -1
- package/dist/commands/iteration/split.js +158 -70
- package/dist/commands/iteration/split.js.map +1 -1
- package/dist/commands/iteration-from-global.js +1 -2
- package/dist/commands/iteration-from-global.js.map +1 -1
- package/dist/commands/pattern.js +30 -16
- package/dist/commands/pattern.js.map +1 -1
- package/dist/commands/pipeline.d.ts.map +1 -1
- package/dist/commands/pipeline.js +15 -4
- package/dist/commands/pipeline.js.map +1 -1
- package/dist/commands/retro.d.ts.map +1 -1
- package/dist/commands/retro.js +15 -8
- package/dist/commands/retro.js.map +1 -1
- package/dist/commands/status-panel.js +1 -1
- package/dist/commands/status-panel.js.map +1 -1
- package/dist/commands/sync.js +15 -8
- package/dist/commands/sync.js.map +1 -1
- package/dist/commands/trace.js +27 -24
- package/dist/commands/trace.js.map +1 -1
- package/dist/commands/update-env-configs.d.ts.map +1 -1
- package/dist/commands/update-env-configs.js +33 -5
- package/dist/commands/update-env-configs.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +29 -0
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/verify.d.ts +1 -1
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +25 -0
- package/dist/commands/verify.js.map +1 -1
- package/dist/core/ai-context-generator.js +35 -22
- package/dist/core/ai-context-generator.js.map +1 -1
- package/dist/core/ai-impact-analyzer.d.ts.map +1 -1
- package/dist/core/ai-impact-analyzer.js +1 -1
- package/dist/core/ai-impact-analyzer.js.map +1 -1
- package/dist/core/analyze-engine.d.ts +2 -1
- package/dist/core/analyze-engine.d.ts.map +1 -1
- package/dist/core/analyze-engine.js +217 -53
- package/dist/core/analyze-engine.js.map +1 -1
- package/dist/core/ask-engine.d.ts.map +1 -1
- package/dist/core/ask-engine.js +31 -13
- package/dist/core/ask-engine.js.map +1 -1
- package/dist/core/change-detection.d.ts.map +1 -1
- package/dist/core/change-detection.js +21 -5
- package/dist/core/change-detection.js.map +1 -1
- package/dist/core/code-scanner.d.ts +12 -3
- package/dist/core/code-scanner.d.ts.map +1 -1
- package/dist/core/code-scanner.js +376 -10
- package/dist/core/code-scanner.js.map +1 -1
- package/dist/core/context-builder.d.ts +2 -1
- package/dist/core/context-builder.d.ts.map +1 -1
- package/dist/core/context-builder.js +104 -25
- package/dist/core/context-builder.js.map +1 -1
- package/dist/core/deploy/engine.d.ts.map +1 -1
- package/dist/core/deploy/engine.js +21 -6
- package/dist/core/deploy/engine.js.map +1 -1
- package/dist/core/git-integration.d.ts +5 -1
- package/dist/core/git-integration.d.ts.map +1 -1
- package/dist/core/git-integration.js +52 -15
- package/dist/core/git-integration.js.map +1 -1
- package/dist/core/knowledge-graph.d.ts +2 -0
- package/dist/core/knowledge-graph.d.ts.map +1 -1
- package/dist/core/knowledge-graph.js +246 -232
- package/dist/core/knowledge-graph.js.map +1 -1
- package/dist/core/name-validator.js +1 -1
- package/dist/core/name-validator.js.map +1 -1
- package/dist/core/pipeline-test.d.ts.map +1 -1
- package/dist/core/pipeline-test.js +7 -4
- package/dist/core/pipeline-test.js.map +1 -1
- package/dist/core/prompt-builder.d.ts +3 -0
- package/dist/core/prompt-builder.d.ts.map +1 -1
- package/dist/core/prompt-builder.js +629 -77
- package/dist/core/prompt-builder.js.map +1 -1
- package/dist/core/rag-engine.d.ts +10 -5
- package/dist/core/rag-engine.d.ts.map +1 -1
- package/dist/core/rag-engine.js +134 -16
- package/dist/core/rag-engine.js.map +1 -1
- package/dist/core/spec-paths.d.ts +42 -2
- package/dist/core/spec-paths.d.ts.map +1 -1
- package/dist/core/spec-paths.js +207 -11
- package/dist/core/spec-paths.js.map +1 -1
- package/dist/core/structured-extractor.d.ts +67 -0
- package/dist/core/structured-extractor.d.ts.map +1 -1
- package/dist/core/structured-extractor.js +1298 -23
- package/dist/core/structured-extractor.js.map +1 -1
- package/dist/core/test-config.d.ts +1 -1
- package/dist/core/test-config.d.ts.map +1 -1
- package/dist/core/test-config.js +85 -65
- package/dist/core/test-config.js.map +1 -1
- package/dist/core/ui-verify/report-generator.d.ts.map +1 -1
- package/dist/core/ui-verify/report-generator.js +7 -3
- package/dist/core/ui-verify/report-generator.js.map +1 -1
- package/dist/core/ui-verify/smoke-engine.js +7 -1
- package/dist/core/ui-verify/smoke-engine.js.map +1 -1
- package/dist/core/unified-config.d.ts +43 -3
- package/dist/core/unified-config.d.ts.map +1 -1
- package/dist/core/unified-config.js +133 -13
- package/dist/core/unified-config.js.map +1 -1
- package/dist/core/unified-retrieval.d.ts.map +1 -1
- package/dist/core/unified-retrieval.js +41 -15
- package/dist/core/unified-retrieval.js.map +1 -1
- package/dist/core/unit-context-assembler.js +2 -2
- package/dist/core/unit-context-assembler.js.map +1 -1
- package/dist/core/validator.d.ts.map +1 -1
- package/dist/core/validator.js +59 -44
- package/dist/core/validator.js.map +1 -1
- package/dist/core/verify-engine.d.ts +9 -2
- package/dist/core/verify-engine.d.ts.map +1 -1
- package/dist/core/verify-engine.js +274 -108
- package/dist/core/verify-engine.js.map +1 -1
- package/dist/core/verify-report-aggregator.js +9 -20
- package/dist/core/verify-report-aggregator.js.map +1 -1
- package/dist/utils/synonyms.d.ts +24 -0
- package/dist/utils/synonyms.d.ts.map +1 -0
- package/dist/utils/synonyms.js +114 -0
- package/dist/utils/synonyms.js.map +1 -0
- package/package.json +1 -1
|
@@ -58,6 +58,21 @@ const CACHE_PATH = (0, path_1.join)('.speccore', 'cache', 'structured-data.json'
|
|
|
58
58
|
function isTsOrJs(filePath) {
|
|
59
59
|
return /\.(ts|tsx|js|jsx|mts|cts)$/i.test(filePath);
|
|
60
60
|
}
|
|
61
|
+
// v8.3.125+: 支持 Java / Python / Go
|
|
62
|
+
function isSupportedSource(filePath) {
|
|
63
|
+
return /\.(ts|tsx|js|jsx|mts|cts|java|py|go)$/i.test(filePath);
|
|
64
|
+
}
|
|
65
|
+
function detectLanguage(filePath) {
|
|
66
|
+
if (/\.(ts|tsx|js|jsx|mts|cts)$/i.test(filePath))
|
|
67
|
+
return 'typescript';
|
|
68
|
+
if (/\.java$/i.test(filePath))
|
|
69
|
+
return 'java';
|
|
70
|
+
if (/\.py$/i.test(filePath))
|
|
71
|
+
return 'python';
|
|
72
|
+
if (/\.go$/i.test(filePath))
|
|
73
|
+
return 'go';
|
|
74
|
+
return 'unknown';
|
|
75
|
+
}
|
|
61
76
|
function getScriptKind(filePath) {
|
|
62
77
|
if (/\.tsx$/i.test(filePath))
|
|
63
78
|
return ts.ScriptKind.TSX;
|
|
@@ -81,8 +96,83 @@ function getJSDoc(node, sourceFile) {
|
|
|
81
96
|
.slice(0, 200);
|
|
82
97
|
}
|
|
83
98
|
// ── 提取器实现 ──
|
|
99
|
+
/** v8.3.124+: 从参数节点提取详细信息(类型、装饰器、校验规则) */
|
|
100
|
+
function extractParameterDetail(param, sourceFile) {
|
|
101
|
+
const name = param.name.getText(sourceFile);
|
|
102
|
+
const type = param.type?.getText(sourceFile);
|
|
103
|
+
const required = !param.questionToken && !param.initializer;
|
|
104
|
+
const paramDecs = [];
|
|
105
|
+
const validations = [];
|
|
106
|
+
let dtoRef;
|
|
107
|
+
const decorators = ts.canHaveDecorators(param) ? ts.getDecorators(param) : undefined;
|
|
108
|
+
if (decorators) {
|
|
109
|
+
for (const d of decorators) {
|
|
110
|
+
const decText = d.getText(sourceFile);
|
|
111
|
+
paramDecs.push(decText);
|
|
112
|
+
// 校验规则: @IsString, @MinLength(6), @IsEmail, @IsOptional
|
|
113
|
+
if (/^@(Is|String|Min|Max|Length|Email|UUID|URL|Enum|Array|Optional|Validate)/.test(decText)) {
|
|
114
|
+
validations.push(decText);
|
|
115
|
+
}
|
|
116
|
+
// DTO 引用: @Body() dto: CreateUserDto
|
|
117
|
+
if (decText.includes('@Body') && param.type) {
|
|
118
|
+
const typeText = param.type.getText(sourceFile);
|
|
119
|
+
if (!/^string|^number|^boolean|^Date/.test(typeText)) {
|
|
120
|
+
dtoRef = typeText;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { name, type, required, decorators: paramDecs, validationRules: validations.length > 0 ? validations : undefined };
|
|
126
|
+
}
|
|
127
|
+
/** v8.3.124+: 提取鉴权装饰器 */
|
|
128
|
+
function extractAuthDecorators(decorators, sourceFile) {
|
|
129
|
+
if (!decorators)
|
|
130
|
+
return [];
|
|
131
|
+
const authDecs = [];
|
|
132
|
+
for (const d of decorators) {
|
|
133
|
+
const text = d.getText(sourceFile);
|
|
134
|
+
if (/^@(Auth|Roles|Public|UseGuards|Permissions|Scope|Require)/.test(text)) {
|
|
135
|
+
authDecs.push(text.slice(0, 60));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return authDecs;
|
|
139
|
+
}
|
|
140
|
+
/** v8.3.125+: 从方法体提取 Service 调用链 */
|
|
141
|
+
function extractServiceCalls(body, sourceFile) {
|
|
142
|
+
const calls = [];
|
|
143
|
+
if (!body)
|
|
144
|
+
return calls;
|
|
145
|
+
function visitCall(n) {
|
|
146
|
+
if (ts.isCallExpression(n)) {
|
|
147
|
+
const expr = n.expression;
|
|
148
|
+
// 模式: this.userService.findById(...) / userService.create(...)
|
|
149
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
150
|
+
const serviceName = expr.expression.getText(sourceFile);
|
|
151
|
+
const methodName = expr.name.getText(sourceFile);
|
|
152
|
+
// 过滤掉原生方法和内置对象
|
|
153
|
+
if (!/^(console|Math|Date|JSON|Object|Array|String|Number|Promise|process|require)$/.test(serviceName)) {
|
|
154
|
+
const line = sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile)).line + 1;
|
|
155
|
+
calls.push({ service: serviceName, method: methodName, line });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// 模式: await someAsyncCall(...)
|
|
159
|
+
if (ts.isIdentifier(expr)) {
|
|
160
|
+
const name = expr.getText(sourceFile);
|
|
161
|
+
if (/^(find|get|create|update|delete|save|remove|query|execute|call)/i.test(name)) {
|
|
162
|
+
const line = sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile)).line + 1;
|
|
163
|
+
calls.push({ service: 'unknown', method: name, line });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
ts.forEachChild(n, visitCall);
|
|
168
|
+
}
|
|
169
|
+
visitCall(body);
|
|
170
|
+
return [...new Map(calls.map(c => [`${c.service}.${c.method}`, c])).values()]; // 去重
|
|
171
|
+
}
|
|
84
172
|
/**
|
|
85
173
|
* 从 TypeScript 源码提取 API 端点
|
|
174
|
+
* v8.3.124+: 增强参数详情、鉴权装饰器、DTO 引用
|
|
175
|
+
* v8.3.125+: 增加 Service 调用链
|
|
86
176
|
*/
|
|
87
177
|
function extractApisFromFile(filePath, sourceFile, relPath) {
|
|
88
178
|
const apis = [];
|
|
@@ -102,6 +192,25 @@ function extractApisFromFile(filePath, sourceFile, relPath) {
|
|
|
102
192
|
const handlerName = node.name?.getText(sourceFile) || 'anonymous';
|
|
103
193
|
const params = node.parameters.map(p => p.name.getText(sourceFile));
|
|
104
194
|
const returnType = node.type?.getText(sourceFile);
|
|
195
|
+
// v8.3.124+: 提取详细参数信息
|
|
196
|
+
const paramDetails = node.parameters.map(p => extractParameterDetail(p, sourceFile));
|
|
197
|
+
const authDecs = extractAuthDecorators(decorators, sourceFile);
|
|
198
|
+
// v8.3.124+: 提取 DTO 引用
|
|
199
|
+
let dtoRef;
|
|
200
|
+
for (const pd of paramDetails) {
|
|
201
|
+
if (pd.decorators?.some(d => d.includes('@Body'))) {
|
|
202
|
+
const p = node.parameters.find(param => param.name.getText(sourceFile) === pd.name);
|
|
203
|
+
if (p && p.type) {
|
|
204
|
+
const typeText = p.type.getText(sourceFile);
|
|
205
|
+
if (!/^(string|number|boolean|Date|any|unknown)\b/.test(typeText)) {
|
|
206
|
+
dtoRef = typeText;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// v8.3.125+: 提取 Service 调用链
|
|
213
|
+
const svcCalls = extractServiceCalls(ts.isMethodDeclaration(node) ? node.body : undefined, sourceFile);
|
|
105
214
|
apis.push({
|
|
106
215
|
path: pathArg || '/',
|
|
107
216
|
method: decName.toUpperCase(),
|
|
@@ -109,9 +218,13 @@ function extractApisFromFile(filePath, sourceFile, relPath) {
|
|
|
109
218
|
filePath: relPath,
|
|
110
219
|
line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
|
|
111
220
|
parameters: params,
|
|
221
|
+
parameterDetails: paramDetails.length > 0 ? paramDetails : undefined,
|
|
112
222
|
responseType: returnType,
|
|
113
223
|
decorators: decorators.map(d => d.getText(sourceFile).slice(0, 50)),
|
|
224
|
+
authDecorators: authDecs.length > 0 ? authDecs : undefined,
|
|
114
225
|
description: getJSDoc(node, sourceFile),
|
|
226
|
+
dtoRef,
|
|
227
|
+
serviceCalls: svcCalls.length > 0 ? svcCalls : undefined,
|
|
115
228
|
});
|
|
116
229
|
}
|
|
117
230
|
}
|
|
@@ -142,8 +255,55 @@ function extractApisFromFile(filePath, sourceFile, relPath) {
|
|
|
142
255
|
visit(sourceFile);
|
|
143
256
|
return apis;
|
|
144
257
|
}
|
|
258
|
+
/** v8.3.124+: 从装饰器参数提取数组(用于 @Index(['a','b'])) */
|
|
259
|
+
function extractArrayFromArg(arg, sourceFile) {
|
|
260
|
+
if (ts.isArrayLiteralExpression(arg)) {
|
|
261
|
+
return arg.elements
|
|
262
|
+
.filter(e => ts.isStringLiteral(e))
|
|
263
|
+
.map(e => e.text);
|
|
264
|
+
}
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
/** v8.3.124+: 从类装饰器提取索引和约束 */
|
|
268
|
+
function extractClassIndexesAndConstraints(decorators, sourceFile) {
|
|
269
|
+
const indexes = [];
|
|
270
|
+
const constraints = [];
|
|
271
|
+
if (!decorators)
|
|
272
|
+
return { indexes, constraints };
|
|
273
|
+
for (const d of decorators) {
|
|
274
|
+
const text = d.getText(sourceFile);
|
|
275
|
+
if (!ts.isCallExpression(d.expression))
|
|
276
|
+
continue;
|
|
277
|
+
const decName = d.expression.expression.getText(sourceFile);
|
|
278
|
+
// @Index(['email', 'phone'], { unique: true })
|
|
279
|
+
if (/^Index$/i.test(decName) && d.expression.arguments.length > 0) {
|
|
280
|
+
const fields = extractArrayFromArg(d.expression.arguments[0], sourceFile);
|
|
281
|
+
let unique = false;
|
|
282
|
+
if (d.expression.arguments.length > 1 && ts.isObjectLiteralExpression(d.expression.arguments[1])) {
|
|
283
|
+
const uniqueProp = d.expression.arguments[1].properties.find(p => ts.isPropertyAssignment(p) && p.name.getText(sourceFile) === 'unique');
|
|
284
|
+
if (uniqueProp && uniqueProp.initializer.getText(sourceFile) === 'true')
|
|
285
|
+
unique = true;
|
|
286
|
+
}
|
|
287
|
+
if (fields.length > 0)
|
|
288
|
+
indexes.push({ fields, unique });
|
|
289
|
+
}
|
|
290
|
+
// @Unique(['username'])
|
|
291
|
+
if (/^Unique$/i.test(decName) && d.expression.arguments.length > 0) {
|
|
292
|
+
const fields = extractArrayFromArg(d.expression.arguments[0], sourceFile);
|
|
293
|
+
if (fields.length > 0)
|
|
294
|
+
constraints.push({ type: 'UNIQUE', fields });
|
|
295
|
+
}
|
|
296
|
+
// @Check('age > 0')
|
|
297
|
+
if (/^Check$/i.test(decName) && d.expression.arguments.length > 0) {
|
|
298
|
+
const expr = d.expression.arguments[0].getText(sourceFile).replace(/['"]/g, '');
|
|
299
|
+
constraints.push({ type: 'CHECK', fields: [expr] });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return { indexes, constraints };
|
|
303
|
+
}
|
|
145
304
|
/**
|
|
146
305
|
* 从 TypeScript 源码提取 Entity/Model 定义
|
|
306
|
+
* v8.3.124+: 增强字段装饰器、索引、约束、默认值提取
|
|
147
307
|
*/
|
|
148
308
|
function extractEntitiesFromFile(filePath, sourceFile, relPath) {
|
|
149
309
|
const entities = [];
|
|
@@ -170,13 +330,18 @@ function extractEntitiesFromFile(filePath, sourceFile, relPath) {
|
|
|
170
330
|
const fieldName = member.name.getText(sourceFile);
|
|
171
331
|
const fieldType = member.type?.getText(sourceFile) || 'unknown';
|
|
172
332
|
const isOptional = member.questionToken !== undefined;
|
|
173
|
-
|
|
174
|
-
d.getText(sourceFile).includes('@PrimaryColumn'));
|
|
175
|
-
// 检测关系装饰器
|
|
333
|
+
// 字段装饰器
|
|
176
334
|
const fieldDecs = ts.canHaveDecorators(member) ? ts.getDecorators(member) : undefined;
|
|
335
|
+
const fieldDecoratorTexts = [];
|
|
336
|
+
const fieldValidations = [];
|
|
337
|
+
let isPrimary = false;
|
|
338
|
+
let defaultValue;
|
|
339
|
+
let columnOptions = {};
|
|
177
340
|
if (fieldDecs) {
|
|
178
341
|
for (const d of fieldDecs) {
|
|
179
342
|
const decText = d.getText(sourceFile);
|
|
343
|
+
fieldDecoratorTexts.push(decText.slice(0, 80));
|
|
344
|
+
// 关系装饰器
|
|
180
345
|
const relationMatch = decText.match(/@(ManyToOne|OneToMany|ManyToMany|OneToOne)/);
|
|
181
346
|
if (relationMatch) {
|
|
182
347
|
const targetType = member.type?.getText(sourceFile) || 'unknown';
|
|
@@ -185,6 +350,39 @@ function extractEntitiesFromFile(filePath, sourceFile, relPath) {
|
|
|
185
350
|
type: relationMatch[1],
|
|
186
351
|
field: fieldName,
|
|
187
352
|
});
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
// 主键
|
|
356
|
+
if (/@PrimaryGeneratedColumn|@PrimaryColumn/.test(decText))
|
|
357
|
+
isPrimary = true;
|
|
358
|
+
// 校验规则
|
|
359
|
+
if (/^@(Is|String|Min|Max|Length|Email|UUID|URL|Enum|Array|Optional|Validate|Matches)/.test(decText)) {
|
|
360
|
+
fieldValidations.push(decText.slice(0, 60));
|
|
361
|
+
}
|
|
362
|
+
// @Column 选项提取
|
|
363
|
+
if (/^@Column/.test(decText) && ts.isCallExpression(d.expression)) {
|
|
364
|
+
// 尝试提取 @Column({ length: 20, default: 'active' })
|
|
365
|
+
for (const arg of d.expression.arguments) {
|
|
366
|
+
if (ts.isObjectLiteralExpression(arg)) {
|
|
367
|
+
for (const prop of arg.properties) {
|
|
368
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
369
|
+
const key = prop.name.getText(sourceFile);
|
|
370
|
+
const valText = prop.initializer.getText(sourceFile);
|
|
371
|
+
if (key === 'default')
|
|
372
|
+
defaultValue = valText.replace(/['"]/g, '');
|
|
373
|
+
try {
|
|
374
|
+
columnOptions[key] = JSON.parse(valText);
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
columnOptions[key] = valText;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
else if (ts.isStringLiteral(arg)) {
|
|
383
|
+
columnOptions.type = arg.text;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
188
386
|
}
|
|
189
387
|
}
|
|
190
388
|
}
|
|
@@ -193,7 +391,10 @@ function extractEntitiesFromFile(filePath, sourceFile, relPath) {
|
|
|
193
391
|
type: fieldType,
|
|
194
392
|
nullable: isOptional,
|
|
195
393
|
isPrimaryKey: isPrimary,
|
|
394
|
+
defaultValue,
|
|
196
395
|
description: getJSDoc(member, sourceFile),
|
|
396
|
+
columnOptions: Object.keys(columnOptions).length > 0 ? columnOptions : undefined,
|
|
397
|
+
validationRules: fieldValidations.length > 0 ? fieldValidations : undefined,
|
|
197
398
|
});
|
|
198
399
|
}
|
|
199
400
|
}
|
|
@@ -209,6 +410,8 @@ function extractEntitiesFromFile(filePath, sourceFile, relPath) {
|
|
|
209
410
|
}
|
|
210
411
|
}
|
|
211
412
|
}
|
|
413
|
+
// v8.3.124+: 提取类级索引和约束
|
|
414
|
+
const { indexes = [], constraints = [] } = extractClassIndexesAndConstraints(decorators, sourceFile);
|
|
212
415
|
entities.push({
|
|
213
416
|
name: className,
|
|
214
417
|
tableName: tableName || className,
|
|
@@ -217,6 +420,8 @@ function extractEntitiesFromFile(filePath, sourceFile, relPath) {
|
|
|
217
420
|
fields,
|
|
218
421
|
relations: relations.length > 0 ? relations : undefined,
|
|
219
422
|
description: getJSDoc(node, sourceFile),
|
|
423
|
+
indexes: indexes.length > 0 ? indexes : undefined,
|
|
424
|
+
constraints: constraints.length > 0 ? constraints : undefined,
|
|
220
425
|
});
|
|
221
426
|
}
|
|
222
427
|
ts.forEachChild(node, visit);
|
|
@@ -319,8 +524,10 @@ async function extractStructuredData(projectRoot, sourcePaths) {
|
|
|
319
524
|
generatedAt: new Date().toISOString(),
|
|
320
525
|
projectRoot,
|
|
321
526
|
endpoints: {},
|
|
527
|
+
dtos: [],
|
|
528
|
+
services: [],
|
|
322
529
|
dependencies: [],
|
|
323
|
-
stats: { totalApis: 0, totalEntities: 0, totalRoutes: 0, totalComponents: 0, totalFiles: 0 },
|
|
530
|
+
stats: { totalApis: 0, totalEntities: 0, totalRoutes: 0, totalComponents: 0, totalDtos: 0, totalServices: 0, totalFiles: 0 },
|
|
324
531
|
};
|
|
325
532
|
let totalFiles = 0;
|
|
326
533
|
for (const srcPath of sourcePaths) {
|
|
@@ -332,23 +539,62 @@ async function extractStructuredData(projectRoot, sourcePaths) {
|
|
|
332
539
|
if (!data.endpoints[platform]) {
|
|
333
540
|
data.endpoints[platform] = { apis: [], entities: [], routes: [], components: [] };
|
|
334
541
|
}
|
|
335
|
-
const files = await
|
|
542
|
+
const files = await collectSourceFiles(absPath);
|
|
336
543
|
for (const filePath of files) {
|
|
337
544
|
totalFiles++;
|
|
338
545
|
const relPath = (0, path_1.relative)(projectRoot, filePath);
|
|
339
546
|
const content = await (0, fs_extra_1.readFile)(filePath, 'utf-8');
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
547
|
+
const lang = detectLanguage(filePath);
|
|
548
|
+
let apis = [];
|
|
549
|
+
let entities = [];
|
|
550
|
+
let routes = [];
|
|
551
|
+
let components = [];
|
|
552
|
+
let fileDtos = []; // v8.3.126+
|
|
553
|
+
let fileServices = []; // v8.3.126+
|
|
554
|
+
if (lang === 'typescript') {
|
|
555
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.ES2020, true, getScriptKind(filePath));
|
|
556
|
+
apis = extractApisFromFile(filePath, sourceFile, relPath);
|
|
557
|
+
entities = extractEntitiesFromFile(filePath, sourceFile, relPath);
|
|
558
|
+
routes = extractRoutesFromFile(filePath, sourceFile, relPath);
|
|
559
|
+
components = extractComponentsFromFile(filePath, sourceFile, relPath);
|
|
560
|
+
fileDtos = extractDtosFromTsFile(filePath, sourceFile, relPath); // v8.3.126+
|
|
561
|
+
fileServices = extractServicesFromTsFile(filePath, sourceFile, relPath); // v8.3.126+
|
|
562
|
+
}
|
|
563
|
+
else if (lang === 'java') {
|
|
564
|
+
const result = extractFromJavaFile(content, relPath);
|
|
565
|
+
apis = result.apis;
|
|
566
|
+
entities = result.entities;
|
|
567
|
+
routes = result.routes;
|
|
568
|
+
components = result.components;
|
|
569
|
+
fileDtos = result.dtos || []; // v8.3.126+
|
|
570
|
+
fileServices = result.services || []; // v8.3.126+
|
|
571
|
+
}
|
|
572
|
+
else if (lang === 'python') {
|
|
573
|
+
const result = extractFromPythonFile(content, relPath);
|
|
574
|
+
apis = result.apis;
|
|
575
|
+
entities = result.entities;
|
|
576
|
+
routes = result.routes;
|
|
577
|
+
components = result.components;
|
|
578
|
+
fileDtos = result.dtos || [];
|
|
579
|
+
fileServices = result.services || [];
|
|
580
|
+
}
|
|
581
|
+
else if (lang === 'go') {
|
|
582
|
+
const result = extractFromGoFile(content, relPath);
|
|
583
|
+
apis = result.apis;
|
|
584
|
+
entities = result.entities;
|
|
585
|
+
routes = result.routes;
|
|
586
|
+
components = result.components;
|
|
587
|
+
fileDtos = result.dtos || [];
|
|
588
|
+
fileServices = result.services || [];
|
|
589
|
+
}
|
|
346
590
|
data.endpoints[platform].apis.push(...apis);
|
|
347
591
|
data.endpoints[platform].entities.push(...entities);
|
|
348
592
|
data.endpoints[platform].routes.push(...routes);
|
|
349
593
|
data.endpoints[platform].components.push(...components);
|
|
350
|
-
//
|
|
351
|
-
|
|
594
|
+
data.dtos.push(...fileDtos); // v8.3.126+
|
|
595
|
+
data.services.push(...fileServices); // v8.3.126+
|
|
596
|
+
// 提取 import 依赖(按语言适配)
|
|
597
|
+
const imports = extractImportsByLang(content, relPath, lang);
|
|
352
598
|
data.dependencies.push(...imports);
|
|
353
599
|
}
|
|
354
600
|
}
|
|
@@ -360,12 +606,14 @@ async function extractStructuredData(projectRoot, sourcePaths) {
|
|
|
360
606
|
data.stats.totalRoutes += ep.routes.length;
|
|
361
607
|
data.stats.totalComponents += ep.components.length;
|
|
362
608
|
}
|
|
609
|
+
data.stats.totalDtos = data.dtos.length; // v8.3.126+
|
|
610
|
+
data.stats.totalServices = data.services.length; // v8.3.126+
|
|
363
611
|
data.stats.totalFiles = totalFiles;
|
|
364
612
|
// 写入缓存
|
|
365
613
|
await (0, fs_extra_1.ensureDir)((0, path_1.dirname)(CACHE_PATH));
|
|
366
614
|
await (0, fs_extra_1.writeFile)(CACHE_PATH, JSON.stringify(data, null, 2));
|
|
367
615
|
logger_1.logger.info(`📊 结构化数据提取完成: ${CACHE_PATH}`);
|
|
368
|
-
logger_1.logger.info(` API: ${data.stats.totalApis}, Entity: ${data.stats.totalEntities}, Route: ${data.stats.totalRoutes}, Component: ${data.stats.totalComponents}, Files: ${totalFiles}`);
|
|
616
|
+
logger_1.logger.info(` API: ${data.stats.totalApis}, Entity: ${data.stats.totalEntities}, Route: ${data.stats.totalRoutes}, Component: ${data.stats.totalComponents}, DTO: ${data.stats.totalDtos}, Service: ${data.stats.totalServices}, Files: ${totalFiles}`);
|
|
369
617
|
return data;
|
|
370
618
|
}
|
|
371
619
|
/**
|
|
@@ -383,16 +631,16 @@ async function loadStructuredData() {
|
|
|
383
631
|
}
|
|
384
632
|
}
|
|
385
633
|
// ── 内部辅助 ──
|
|
386
|
-
async function
|
|
634
|
+
async function collectSourceFiles(dir) {
|
|
387
635
|
const results = [];
|
|
388
636
|
try {
|
|
389
637
|
const entries = await (0, fs_extra_1.readdir)(dir, { withFileTypes: true });
|
|
390
638
|
for (const entry of entries) {
|
|
391
639
|
const fullPath = (0, path_1.join)(dir, entry.name);
|
|
392
|
-
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
393
|
-
results.push(...await
|
|
640
|
+
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules' && entry.name !== '__pycache__' && entry.name !== 'venv') {
|
|
641
|
+
results.push(...await collectSourceFiles(fullPath));
|
|
394
642
|
}
|
|
395
|
-
else if (entry.isFile() &&
|
|
643
|
+
else if (entry.isFile() && isSupportedSource(fullPath)) {
|
|
396
644
|
results.push(fullPath);
|
|
397
645
|
}
|
|
398
646
|
}
|
|
@@ -414,15 +662,1042 @@ function inferPlatform(path) {
|
|
|
414
662
|
return 'shared';
|
|
415
663
|
return 'unknown';
|
|
416
664
|
}
|
|
417
|
-
function
|
|
665
|
+
function extractImportsByLang(content, fromPath, lang) {
|
|
418
666
|
const edges = [];
|
|
419
667
|
const lines = content.split('\n');
|
|
420
|
-
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
668
|
+
if (lang === 'typescript' || lang === 'java') {
|
|
669
|
+
for (const line of lines) {
|
|
670
|
+
const match = line.match(/import\s+.*?\s+from\s+['"](\.\/[^'"]+)['"]/);
|
|
671
|
+
if (match)
|
|
672
|
+
edges.push({ from: fromPath, to: match[1], type: 'import' });
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
else if (lang === 'python') {
|
|
676
|
+
for (const line of lines) {
|
|
677
|
+
const match = line.match(/from\s+([.\w]+)\s+import|import\s+([.\w]+)/);
|
|
678
|
+
if (match)
|
|
679
|
+
edges.push({ from: fromPath, to: match[1] || match[2], type: 'import' });
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
else if (lang === 'go') {
|
|
683
|
+
for (const line of lines) {
|
|
684
|
+
const match = line.match(/"([^"]+\/[^"]+)"/);
|
|
685
|
+
if (match)
|
|
686
|
+
edges.push({ from: fromPath, to: match[1], type: 'import' });
|
|
424
687
|
}
|
|
425
688
|
}
|
|
426
689
|
return edges;
|
|
427
690
|
}
|
|
691
|
+
// 保留旧函数兼容
|
|
692
|
+
function extractImports(content, fromPath) {
|
|
693
|
+
return extractImportsByLang(content, fromPath, 'typescript');
|
|
694
|
+
}
|
|
695
|
+
// ── Java (Spring Boot) ──
|
|
696
|
+
function extractFromJavaFile(content, relPath) {
|
|
697
|
+
const apis = [];
|
|
698
|
+
const entities = [];
|
|
699
|
+
const routes = [];
|
|
700
|
+
const components = [];
|
|
701
|
+
const lines = content.split('\n');
|
|
702
|
+
let currentClass = null;
|
|
703
|
+
let currentClassLine = 0;
|
|
704
|
+
let classAnnotations = [];
|
|
705
|
+
let inClass = false;
|
|
706
|
+
let braceDepth = 0;
|
|
707
|
+
for (let i = 0; i < lines.length; i++) {
|
|
708
|
+
const line = lines[i].trim();
|
|
709
|
+
// 类级注解收集
|
|
710
|
+
if (/^@\w+/.test(line)) {
|
|
711
|
+
classAnnotations.push(line);
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
// 类定义
|
|
715
|
+
const classMatch = line.match(/(?:public\s+)?(?:class|interface)\s+(\w+)/);
|
|
716
|
+
if (classMatch) {
|
|
717
|
+
currentClass = classMatch[1];
|
|
718
|
+
currentClassLine = i + 1;
|
|
719
|
+
inClass = true;
|
|
720
|
+
braceDepth = (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length;
|
|
721
|
+
// @Entity 检测
|
|
722
|
+
const isEntity = classAnnotations.some(a => /@Entity/.test(a));
|
|
723
|
+
if (isEntity) {
|
|
724
|
+
const tableMatch = classAnnotations.find(a => /@Table\s*\(/.test(a));
|
|
725
|
+
let tableName;
|
|
726
|
+
if (tableMatch) {
|
|
727
|
+
const tm = tableMatch.match(/name\s*=\s*["'](\w+)["']/);
|
|
728
|
+
if (tm)
|
|
729
|
+
tableName = tm[1];
|
|
730
|
+
}
|
|
731
|
+
const entity = extractJavaEntity(currentClass, tableName, content, relPath, currentClassLine, i);
|
|
732
|
+
if (entity)
|
|
733
|
+
entities.push(entity);
|
|
734
|
+
}
|
|
735
|
+
classAnnotations = [];
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
if (inClass) {
|
|
739
|
+
braceDepth += (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length;
|
|
740
|
+
if (braceDepth <= 0) {
|
|
741
|
+
inClass = false;
|
|
742
|
+
currentClass = null;
|
|
743
|
+
}
|
|
744
|
+
// Spring Boot API 方法检测
|
|
745
|
+
const methodMatch = line.match(/@(GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping|RequestMapping)\s*\(([^)]*)\)/);
|
|
746
|
+
if (methodMatch && currentClass) {
|
|
747
|
+
const httpMethod = methodMatch[1].replace('Mapping', '').toUpperCase();
|
|
748
|
+
const methodArg = methodMatch[2];
|
|
749
|
+
let path = '/';
|
|
750
|
+
const pathMatch = methodArg.match(/["']([^"']+)["']/);
|
|
751
|
+
if (pathMatch)
|
|
752
|
+
path = pathMatch[1];
|
|
753
|
+
// 查找方法签名(下一行或当前行剩余部分)
|
|
754
|
+
let methodLine = i + 1;
|
|
755
|
+
let sig = '';
|
|
756
|
+
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
|
|
757
|
+
sig += lines[j];
|
|
758
|
+
if (lines[j].includes('{') || lines[j].includes(';'))
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
const sigMatch = sig.match(/(?:public\s+)?(\w+(?:<[^>]+>)?)\s+(\w+)\s*\(([^)]*)\)/);
|
|
762
|
+
const returnType = sigMatch ? sigMatch[1] : undefined;
|
|
763
|
+
const methodName = sigMatch ? sigMatch[2] : 'unknown';
|
|
764
|
+
// 参数解析
|
|
765
|
+
const params = [];
|
|
766
|
+
const paramDetails = [];
|
|
767
|
+
if (sigMatch && sigMatch[3]) {
|
|
768
|
+
const paramParts = sigMatch[3].split(',');
|
|
769
|
+
for (const pp of paramParts) {
|
|
770
|
+
const pm = pp.trim().match(/(?:@\w+(?:\([^)]*\))?\s+)*(\w+(?:<[^>]+>)?)\s+(\w+)/);
|
|
771
|
+
if (pm) {
|
|
772
|
+
params.push(pm[2]);
|
|
773
|
+
const pDecs = [];
|
|
774
|
+
const pVals = [];
|
|
775
|
+
const decMatch = pp.match(/@(\w+)(?:\([^)]*\))?/g);
|
|
776
|
+
if (decMatch) {
|
|
777
|
+
for (const d of decMatch) {
|
|
778
|
+
pDecs.push(d);
|
|
779
|
+
if (/RequestBody|Valid|Validated/.test(d))
|
|
780
|
+
pVals.push(d);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
paramDetails.push({ name: pm[2], type: pm[1], decorators: pDecs, validationRules: pVals });
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
// 鉴权注解(方法级)
|
|
788
|
+
const authDecs = [];
|
|
789
|
+
for (let j = Math.max(0, i - 5); j < i; j++) {
|
|
790
|
+
if (/@(PreAuthorize|Secured|RolesAllowed)/.test(lines[j])) {
|
|
791
|
+
authDecs.push(lines[j].trim());
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
// v8.3.125+: 提取方法体内的 Service 调用
|
|
795
|
+
const svcCalls = extractServiceCallsFromLines(lines, i, 40, 'java');
|
|
796
|
+
apis.push({
|
|
797
|
+
path,
|
|
798
|
+
method: httpMethod === 'REQUEST' ? 'GET' : httpMethod,
|
|
799
|
+
handler: `${currentClass}.${methodName}`,
|
|
800
|
+
filePath: relPath,
|
|
801
|
+
line: methodLine,
|
|
802
|
+
parameters: params,
|
|
803
|
+
parameterDetails: paramDetails.length > 0 ? paramDetails : undefined,
|
|
804
|
+
responseType: returnType,
|
|
805
|
+
authDecorators: authDecs.length > 0 ? authDecs : undefined,
|
|
806
|
+
serviceCalls: svcCalls.length > 0 ? svcCalls : undefined,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
const dtos = extractDtosFromJavaFile(content, relPath); // v8.3.126+
|
|
812
|
+
const services = extractServicesFromJavaFile(content, relPath); // v8.3.126+
|
|
813
|
+
return { apis, entities, routes, components, dtos, services };
|
|
814
|
+
}
|
|
815
|
+
function extractJavaEntity(className, tableName, content, relPath, classLine, startIdx) {
|
|
816
|
+
const lines = content.split('\n');
|
|
817
|
+
const fields = [];
|
|
818
|
+
let braceDepth = 0;
|
|
819
|
+
let inClass = false;
|
|
820
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
821
|
+
const line = lines[i];
|
|
822
|
+
if (!inClass) {
|
|
823
|
+
if (line.includes('{'))
|
|
824
|
+
inClass = true;
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
braceDepth += (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length;
|
|
828
|
+
if (braceDepth < 0)
|
|
829
|
+
break;
|
|
830
|
+
const trimmed = line.trim();
|
|
831
|
+
// 字段检测: private Type fieldName;
|
|
832
|
+
const fieldMatch = trimmed.match(/^private\s+(?:@\w+(?:\([^)]*\))?\s+)*(\w+(?:<[^>]+>)?)\s+(\w+)\s*;/);
|
|
833
|
+
if (fieldMatch) {
|
|
834
|
+
const fieldType = fieldMatch[1];
|
|
835
|
+
const fieldName = fieldMatch[2];
|
|
836
|
+
let isPrimary = false;
|
|
837
|
+
let isNullable = true;
|
|
838
|
+
let defaultValue;
|
|
839
|
+
const colOpts = {};
|
|
840
|
+
const validations = [];
|
|
841
|
+
// 检查前面几行的注解
|
|
842
|
+
for (let j = Math.max(0, i - 3); j < i; j++) {
|
|
843
|
+
const al = lines[j].trim();
|
|
844
|
+
if (/@Id|@PrimaryKey/.test(al))
|
|
845
|
+
isPrimary = true;
|
|
846
|
+
if (/@Column\s*\(/.test(al)) {
|
|
847
|
+
const nullableMatch = al.match(/nullable\s*=\s*(false|true)/);
|
|
848
|
+
if (nullableMatch)
|
|
849
|
+
isNullable = nullableMatch[1] === 'true';
|
|
850
|
+
const lenMatch = al.match(/length\s*=\s*(\d+)/);
|
|
851
|
+
if (lenMatch)
|
|
852
|
+
colOpts.length = parseInt(lenMatch[1]);
|
|
853
|
+
const defMatch = al.match(/columnDefinition\s*=\s*["']([^"']+)["']/);
|
|
854
|
+
if (defMatch)
|
|
855
|
+
colOpts.columnDefinition = defMatch[1];
|
|
856
|
+
}
|
|
857
|
+
if (/@NotNull/.test(al))
|
|
858
|
+
isNullable = false;
|
|
859
|
+
if (/^@\w+/.test(al) && !/@Column|@Id|@PrimaryKey/.test(al)) {
|
|
860
|
+
validations.push(al.slice(0, 60));
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
fields.push({
|
|
864
|
+
name: fieldName,
|
|
865
|
+
type: fieldType,
|
|
866
|
+
nullable: isNullable,
|
|
867
|
+
isPrimaryKey: isPrimary,
|
|
868
|
+
defaultValue,
|
|
869
|
+
columnOptions: Object.keys(colOpts).length > 0 ? colOpts : undefined,
|
|
870
|
+
validationRules: validations.length > 0 ? validations : undefined,
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
if (fields.length === 0)
|
|
875
|
+
return null;
|
|
876
|
+
return { name: className, tableName: tableName || className, filePath: relPath, line: classLine, fields };
|
|
877
|
+
}
|
|
878
|
+
// ── Python (FastAPI / Django / Flask) ──
|
|
879
|
+
function extractFromPythonFile(content, relPath) {
|
|
880
|
+
const apis = [];
|
|
881
|
+
const entities = [];
|
|
882
|
+
const routes = [];
|
|
883
|
+
const components = [];
|
|
884
|
+
const lines = content.split('\n');
|
|
885
|
+
for (let i = 0; i < lines.length; i++) {
|
|
886
|
+
const line = lines[i];
|
|
887
|
+
// FastAPI 路由: @app.get("/path") 或 @router.post("/path")
|
|
888
|
+
const apiMatch = line.match(/@(app|router|api)\.(get|post|put|delete|patch)\s*\(\s*["']([^"']+)["']/);
|
|
889
|
+
if (apiMatch) {
|
|
890
|
+
const method = apiMatch[2].toUpperCase();
|
|
891
|
+
const path = apiMatch[3];
|
|
892
|
+
let handler = 'unknown';
|
|
893
|
+
// 查找下一行的 def
|
|
894
|
+
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
|
|
895
|
+
const defMatch = lines[j].match(/def\s+(\w+)\s*\(([^)]*)\)/);
|
|
896
|
+
if (defMatch) {
|
|
897
|
+
handler = defMatch[1];
|
|
898
|
+
const params = [];
|
|
899
|
+
const paramDetails = [];
|
|
900
|
+
const paramStr = defMatch[2];
|
|
901
|
+
// 解析参数(跳过 self, cls, 和依赖注入参数)
|
|
902
|
+
const paramParts = paramStr.split(',');
|
|
903
|
+
for (const pp of paramParts) {
|
|
904
|
+
const pt = pp.trim();
|
|
905
|
+
if (!pt || pt === 'self' || pt === 'cls')
|
|
906
|
+
continue;
|
|
907
|
+
// 支持 type hint: param: Type
|
|
908
|
+
const pm = pt.match(/(\w+)\s*:\s*(.+)/);
|
|
909
|
+
if (pm) {
|
|
910
|
+
params.push(pm[1]);
|
|
911
|
+
paramDetails.push({ name: pm[1], type: pm[2] });
|
|
912
|
+
}
|
|
913
|
+
else {
|
|
914
|
+
const simple = pt.match(/(\w+)/);
|
|
915
|
+
if (simple) {
|
|
916
|
+
params.push(simple[1]);
|
|
917
|
+
paramDetails.push({ name: simple[1] });
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
const pySvcCalls = extractServiceCallsFromLines(lines, j, 30, 'python');
|
|
922
|
+
apis.push({
|
|
923
|
+
path,
|
|
924
|
+
method,
|
|
925
|
+
handler,
|
|
926
|
+
filePath: relPath,
|
|
927
|
+
line: j + 1,
|
|
928
|
+
parameters: params,
|
|
929
|
+
parameterDetails: paramDetails.length > 0 ? paramDetails : undefined,
|
|
930
|
+
serviceCalls: pySvcCalls.length > 0 ? pySvcCalls : undefined,
|
|
931
|
+
});
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
// Django 视图: def view_name(request): 带有 url 注释
|
|
937
|
+
const djangoUrlMatch = line.match(/#\s*url:\s*(\S+)/);
|
|
938
|
+
if (djangoUrlMatch) {
|
|
939
|
+
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
|
|
940
|
+
const defMatch = lines[j].match(/def\s+(\w+)\s*\(/);
|
|
941
|
+
if (defMatch) {
|
|
942
|
+
const djangoSvcCalls = extractServiceCallsFromLines(lines, j, 30, 'python');
|
|
943
|
+
apis.push({
|
|
944
|
+
path: djangoUrlMatch[1],
|
|
945
|
+
method: 'GET',
|
|
946
|
+
handler: defMatch[1],
|
|
947
|
+
filePath: relPath,
|
|
948
|
+
line: j + 1,
|
|
949
|
+
serviceCalls: djangoSvcCalls.length > 0 ? djangoSvcCalls : undefined,
|
|
950
|
+
});
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
// Pydantic / Dataclass Entity
|
|
956
|
+
const classMatch = line.match(/class\s+(\w+)\s*\(([^)]*)\)/);
|
|
957
|
+
if (classMatch) {
|
|
958
|
+
const className = classMatch[1];
|
|
959
|
+
const bases = classMatch[2];
|
|
960
|
+
const isPydantic = /BaseModel| pydantic/.test(bases);
|
|
961
|
+
const isDataclass = /@dataclass/.test(bases) || lines.slice(Math.max(0, i - 2), i).some(l => l.includes('@dataclass'));
|
|
962
|
+
const isDjangoModel = /models\.Model/.test(bases);
|
|
963
|
+
if (isPydantic || isDataclass || isDjangoModel) {
|
|
964
|
+
const entity = extractPythonEntity(className, content, relPath, i + 1, i);
|
|
965
|
+
if (entity)
|
|
966
|
+
entities.push(entity);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
const dtos = extractDtosFromPythonFile(content, relPath); // v8.3.126+
|
|
971
|
+
const services = extractServicesFromPythonFile(content, relPath); // v8.3.126+
|
|
972
|
+
return { apis, entities, routes, components, dtos, services };
|
|
973
|
+
}
|
|
974
|
+
function extractPythonEntity(className, content, relPath, classLine, startIdx) {
|
|
975
|
+
const lines = content.split('\n');
|
|
976
|
+
const fields = [];
|
|
977
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
978
|
+
const line = lines[i];
|
|
979
|
+
// 类结束检测
|
|
980
|
+
if (line.trim() && !line.startsWith(' ') && !line.startsWith('\t') && !line.startsWith('#'))
|
|
981
|
+
break;
|
|
982
|
+
// 字段检测: name: Type = Field(...) 或 name = models.CharField(...)
|
|
983
|
+
const fieldMatch = line.match(/^(\s+)(\w+)\s*:\s*(\w+(?:\[[^\]]+\])?)\s*(?:=\s*(.+))?/);
|
|
984
|
+
if (fieldMatch) {
|
|
985
|
+
const fieldName = fieldMatch[2];
|
|
986
|
+
const fieldType = fieldMatch[3];
|
|
987
|
+
const defaultExpr = fieldMatch[4];
|
|
988
|
+
let defaultValue;
|
|
989
|
+
let isNullable = true;
|
|
990
|
+
const validations = [];
|
|
991
|
+
if (defaultExpr) {
|
|
992
|
+
if (/Field\s*\(/.test(defaultExpr)) {
|
|
993
|
+
const nullMatch = defaultExpr.match(/nullable\s*=\s*(True|False)/);
|
|
994
|
+
if (nullMatch)
|
|
995
|
+
isNullable = nullMatch[1] === 'True';
|
|
996
|
+
const defMatch = defaultExpr.match(/default\s*=\s*([^,)]+)/);
|
|
997
|
+
if (defMatch)
|
|
998
|
+
defaultValue = defMatch[1].trim();
|
|
999
|
+
}
|
|
1000
|
+
else {
|
|
1001
|
+
defaultValue = defaultExpr.trim();
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
fields.push({
|
|
1005
|
+
name: fieldName,
|
|
1006
|
+
type: fieldType,
|
|
1007
|
+
nullable: isNullable,
|
|
1008
|
+
defaultValue,
|
|
1009
|
+
validationRules: validations.length > 0 ? validations : undefined,
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
// Django ORM 字段: name = models.CharField(...)
|
|
1013
|
+
const djangoFieldMatch = line.match(/(\w+)\s*=\s*models\.(\w+)Field\s*\(([^)]*)\)/);
|
|
1014
|
+
if (djangoFieldMatch) {
|
|
1015
|
+
const fieldName = djangoFieldMatch[1];
|
|
1016
|
+
const fieldType = djangoFieldMatch[2];
|
|
1017
|
+
const args = djangoFieldMatch[3];
|
|
1018
|
+
let isPrimary = false;
|
|
1019
|
+
let isNullable = true;
|
|
1020
|
+
let defaultValue;
|
|
1021
|
+
const colOpts = {};
|
|
1022
|
+
if (/primary_key\s*=\s*True/.test(args))
|
|
1023
|
+
isPrimary = true;
|
|
1024
|
+
if (/null\s*=\s*False/.test(args))
|
|
1025
|
+
isNullable = false;
|
|
1026
|
+
const defMatch = args.match(/default\s*=\s*([^,)]+)/);
|
|
1027
|
+
if (defMatch)
|
|
1028
|
+
defaultValue = defMatch[1].trim();
|
|
1029
|
+
const maxMatch = args.match(/max_length\s*=\s*(\d+)/);
|
|
1030
|
+
if (maxMatch)
|
|
1031
|
+
colOpts.maxLength = parseInt(maxMatch[1]);
|
|
1032
|
+
fields.push({
|
|
1033
|
+
name: fieldName,
|
|
1034
|
+
type: fieldType,
|
|
1035
|
+
nullable: isNullable,
|
|
1036
|
+
isPrimaryKey: isPrimary,
|
|
1037
|
+
defaultValue,
|
|
1038
|
+
columnOptions: Object.keys(colOpts).length > 0 ? colOpts : undefined,
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
if (fields.length === 0)
|
|
1043
|
+
return null;
|
|
1044
|
+
return { name: className, filePath: relPath, line: classLine, fields };
|
|
1045
|
+
}
|
|
1046
|
+
// ── Go (Gin / Echo) ──
|
|
1047
|
+
function extractFromGoFile(content, relPath) {
|
|
1048
|
+
const apis = [];
|
|
1049
|
+
const entities = [];
|
|
1050
|
+
const routes = [];
|
|
1051
|
+
const components = [];
|
|
1052
|
+
const lines = content.split('\n');
|
|
1053
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1054
|
+
const line = lines[i];
|
|
1055
|
+
// Gin 路由: r.GET("/path", handler) 或 router.POST("/path", handler)
|
|
1056
|
+
const ginMatch = line.match(/\b(r|router|g|engine|e)\.(GET|POST|PUT|DELETE|PATCH|OPTIONS)\s*\(\s*["']([^"']+)["']\s*,\s*(\w+)/);
|
|
1057
|
+
if (ginMatch) {
|
|
1058
|
+
const handlerLine = findGoHandlerLine(lines, ginMatch[4]);
|
|
1059
|
+
const goSvcCalls = handlerLine >= 0 ? extractServiceCallsFromLines(lines, handlerLine, 30, 'go') : [];
|
|
1060
|
+
apis.push({
|
|
1061
|
+
path: ginMatch[3],
|
|
1062
|
+
method: ginMatch[2],
|
|
1063
|
+
handler: ginMatch[4],
|
|
1064
|
+
filePath: relPath,
|
|
1065
|
+
line: i + 1,
|
|
1066
|
+
serviceCalls: goSvcCalls.length > 0 ? goSvcCalls : undefined,
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
// Echo 路由: e.GET("/path", handler) 或 group.GET(...)
|
|
1070
|
+
const echoMatch = line.match(/\b(\w+)\.(GET|POST|PUT|DELETE|PATCH|OPTIONS)\s*\(\s*["']([^"']+)["']\s*,\s*(\w+)/);
|
|
1071
|
+
if (echoMatch && !ginMatch) {
|
|
1072
|
+
const handlerLine = findGoHandlerLine(lines, echoMatch[4]);
|
|
1073
|
+
const goSvcCalls = handlerLine >= 0 ? extractServiceCallsFromLines(lines, handlerLine, 30, 'go') : [];
|
|
1074
|
+
apis.push({
|
|
1075
|
+
path: echoMatch[3],
|
|
1076
|
+
method: echoMatch[2],
|
|
1077
|
+
handler: echoMatch[4],
|
|
1078
|
+
filePath: relPath,
|
|
1079
|
+
line: i + 1,
|
|
1080
|
+
serviceCalls: goSvcCalls.length > 0 ? goSvcCalls : undefined,
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
// Go struct Entity(带 gorm 标签)
|
|
1084
|
+
const structMatch = line.match(/type\s+(\w+)\s+struct\s*\{/);
|
|
1085
|
+
if (structMatch) {
|
|
1086
|
+
const structName = structMatch[1];
|
|
1087
|
+
const entity = extractGoEntity(structName, content, relPath, i + 1, i);
|
|
1088
|
+
if (entity)
|
|
1089
|
+
entities.push(entity);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
const dtos = extractDtosFromGoFile(content, relPath); // v8.3.126+
|
|
1093
|
+
const services = extractServicesFromGoFile(content, relPath); // v8.3.126+
|
|
1094
|
+
return { apis, entities, routes, components, dtos, services };
|
|
1095
|
+
}
|
|
1096
|
+
function extractGoEntity(structName, content, relPath, structLine, startIdx) {
|
|
1097
|
+
const lines = content.split('\n');
|
|
1098
|
+
const fields = [];
|
|
1099
|
+
let inStruct = false;
|
|
1100
|
+
let braceDepth = 0;
|
|
1101
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
1102
|
+
const line = lines[i];
|
|
1103
|
+
if (!inStruct) {
|
|
1104
|
+
if (line.includes('{'))
|
|
1105
|
+
inStruct = true;
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
braceDepth += (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length;
|
|
1109
|
+
if (braceDepth < 0)
|
|
1110
|
+
break;
|
|
1111
|
+
const trimmed = line.trim();
|
|
1112
|
+
// 字段: Name Type `gorm:"..." json:"..."`
|
|
1113
|
+
const fieldMatch = trimmed.match(/^(\w+)\s+(\S+(?:\[[^\]]+\])?)\s*(?:`([^`]+)`)?/);
|
|
1114
|
+
if (fieldMatch && !trimmed.startsWith('//')) {
|
|
1115
|
+
const fieldName = fieldMatch[1];
|
|
1116
|
+
const fieldType = fieldMatch[2];
|
|
1117
|
+
const tagStr = fieldMatch[3] || '';
|
|
1118
|
+
let isPrimary = false;
|
|
1119
|
+
let isNullable = true;
|
|
1120
|
+
let defaultValue;
|
|
1121
|
+
const colOpts = {};
|
|
1122
|
+
// 解析 gorm 标签
|
|
1123
|
+
const gormMatch = tagStr.match(/gorm:"([^"]+)"/);
|
|
1124
|
+
if (gormMatch) {
|
|
1125
|
+
const gormOpts = gormMatch[1];
|
|
1126
|
+
if (/primaryKey/.test(gormOpts))
|
|
1127
|
+
isPrimary = true;
|
|
1128
|
+
if (/autoIncrement/.test(gormOpts))
|
|
1129
|
+
colOpts.autoIncrement = true;
|
|
1130
|
+
if (/default:/.test(gormOpts)) {
|
|
1131
|
+
const defMatch = gormOpts.match(/default:([^;]+)/);
|
|
1132
|
+
if (defMatch)
|
|
1133
|
+
defaultValue = defMatch[1];
|
|
1134
|
+
}
|
|
1135
|
+
if (/size:/.test(gormOpts)) {
|
|
1136
|
+
const sizeMatch = gormOpts.match(/size:(\d+)/);
|
|
1137
|
+
if (sizeMatch)
|
|
1138
|
+
colOpts.size = parseInt(sizeMatch[1]);
|
|
1139
|
+
}
|
|
1140
|
+
if (/type:/.test(gormOpts)) {
|
|
1141
|
+
const typeMatch = gormOpts.match(/type:([^;]+)/);
|
|
1142
|
+
if (typeMatch)
|
|
1143
|
+
colOpts.type = typeMatch[1];
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
// 解析 json 标签判断 omitempty
|
|
1147
|
+
const jsonMatch = tagStr.match(/json:"([^"]+)"/);
|
|
1148
|
+
if (jsonMatch && jsonMatch[1].includes('omitempty')) {
|
|
1149
|
+
isNullable = true;
|
|
1150
|
+
}
|
|
1151
|
+
fields.push({
|
|
1152
|
+
name: fieldName,
|
|
1153
|
+
type: fieldType,
|
|
1154
|
+
nullable: isNullable,
|
|
1155
|
+
isPrimaryKey: isPrimary,
|
|
1156
|
+
defaultValue,
|
|
1157
|
+
columnOptions: Object.keys(colOpts).length > 0 ? colOpts : undefined,
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (fields.length === 0)
|
|
1162
|
+
return null;
|
|
1163
|
+
return { name: structName, filePath: relPath, line: structLine, fields };
|
|
1164
|
+
}
|
|
1165
|
+
/** v8.3.125+: 在 Go 文件中查找 handler 函数定义行 */
|
|
1166
|
+
function findGoHandlerLine(lines, handlerName) {
|
|
1167
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1168
|
+
if (new RegExp(`func\\s+(?:\\([^)]+\\)\\s+)?${handlerName}\\s*\\(`).test(lines[i])) {
|
|
1169
|
+
return i;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return -1;
|
|
1173
|
+
}
|
|
1174
|
+
/** v8.3.125+: 通用 Service 调用提取(从代码行数组) */
|
|
1175
|
+
function extractServiceCallsFromLines(lines, startIdx, maxLines, lang) {
|
|
1176
|
+
const calls = [];
|
|
1177
|
+
const seen = new Set();
|
|
1178
|
+
const endIdx = Math.min(startIdx + maxLines, lines.length);
|
|
1179
|
+
for (let i = startIdx; i < endIdx; i++) {
|
|
1180
|
+
const line = lines[i].trim();
|
|
1181
|
+
if (line.startsWith('//') || line.startsWith('#') || line.startsWith('*'))
|
|
1182
|
+
continue;
|
|
1183
|
+
let matches = [];
|
|
1184
|
+
if (lang === 'java') {
|
|
1185
|
+
// this.userService.findById(...) 或 userService.create(...)
|
|
1186
|
+
const regex = /(\w+)\.(\w+)\s*\(/g;
|
|
1187
|
+
let m;
|
|
1188
|
+
while ((m = regex.exec(line)) !== null) {
|
|
1189
|
+
if (!/^(if|while|for|switch|return|new|assert|System|Math|String|Integer|Long|Boolean|Object|Arrays|Collections|Stream|Optional|logger|log)\b/.test(m[1])) {
|
|
1190
|
+
matches.push(m);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
else if (lang === 'python') {
|
|
1195
|
+
// self.user_service.find_by_id(...) 或 user_service.create(...)
|
|
1196
|
+
const regex = /(\w+)\.(\w+)\s*\(/g;
|
|
1197
|
+
let m;
|
|
1198
|
+
while ((m = regex.exec(line)) !== null) {
|
|
1199
|
+
if (!/^(if|while|for|return|print|len|range|enumerate|zip|map|filter|super|self).__/.test(m[1] + '.' + m[2])) {
|
|
1200
|
+
matches.push(m);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
else if (lang === 'go') {
|
|
1205
|
+
// svc.FindById(...) 或 service.Create(...)
|
|
1206
|
+
const regex = /(\w+)\.(\w+)\s*\(/g;
|
|
1207
|
+
let m;
|
|
1208
|
+
while ((m = regex.exec(line)) !== null) {
|
|
1209
|
+
if (!/^(if|for|switch|return|defer|go|make|new|append|copy|len|cap|panic|recover|print|println|fmt|log|strings|strconv|time|errors)\b/.test(m[1])) {
|
|
1210
|
+
matches.push(m);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
for (const m of matches) {
|
|
1215
|
+
const key = `${m[1]}.${m[2]}`;
|
|
1216
|
+
if (!seen.has(key)) {
|
|
1217
|
+
seen.add(key);
|
|
1218
|
+
calls.push({ service: m[1], method: m[2], line: i + 1 });
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
return calls;
|
|
1223
|
+
}
|
|
1224
|
+
// ═══════════════════════════════════════════════════════════
|
|
1225
|
+
// v8.3.126+: DTO / Service 独立提取器
|
|
1226
|
+
// ═══════════════════════════════════════════════════════════
|
|
1227
|
+
/** 从 TypeScript 文件提取 DTO 定义(class/interface + class-validator 装饰器) */
|
|
1228
|
+
function extractDtosFromTsFile(filePath, sourceFile, relPath) {
|
|
1229
|
+
const dtos = [];
|
|
1230
|
+
function visit(node) {
|
|
1231
|
+
if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) {
|
|
1232
|
+
const className = node.name?.getText(sourceFile);
|
|
1233
|
+
if (!className)
|
|
1234
|
+
return;
|
|
1235
|
+
// DTO 检测:类名含 DTO/Vo/Input/Output/Query/Command/Request/Response
|
|
1236
|
+
// 或包含 class-validator 装饰器
|
|
1237
|
+
const isDtoLike = /(Dto|VO|Input|Output|Query|Command|Request|Response|Create|Update|Delete|Filter|Search|Sort)$/i.test(className);
|
|
1238
|
+
const decorators = ts.canHaveDecorators(node) ? ts.getDecorators(node) : undefined;
|
|
1239
|
+
const hasValidator = node.members.some(m => {
|
|
1240
|
+
const memberDecs = ts.canHaveDecorators(m) ? ts.getDecorators(m) : undefined;
|
|
1241
|
+
return memberDecs?.some(d => /^@(Is|String|Min|Max|Length|Email|UUID|URL|Enum|Array|Optional|Validate|Matches|Not)/.test(d.getText(sourceFile)));
|
|
1242
|
+
});
|
|
1243
|
+
if (!isDtoLike && !hasValidator)
|
|
1244
|
+
return;
|
|
1245
|
+
const fields = [];
|
|
1246
|
+
for (const member of node.members) {
|
|
1247
|
+
if (ts.isPropertySignature(member) || ts.isPropertyDeclaration(member)) {
|
|
1248
|
+
const fieldName = member.name.getText(sourceFile);
|
|
1249
|
+
const fieldType = member.type?.getText(sourceFile) || 'unknown';
|
|
1250
|
+
const isOptional = member.questionToken !== undefined;
|
|
1251
|
+
const fieldDecs = ts.canHaveDecorators(member) ? ts.getDecorators(member) : undefined;
|
|
1252
|
+
const validations = [];
|
|
1253
|
+
const decorators = [];
|
|
1254
|
+
if (fieldDecs) {
|
|
1255
|
+
for (const d of fieldDecs) {
|
|
1256
|
+
const dt = d.getText(sourceFile);
|
|
1257
|
+
decorators.push(dt.slice(0, 80));
|
|
1258
|
+
if (/^@(Is|String|Min|Max|Length|Email|UUID|URL|Enum|Array|Optional|Validate|Matches|Not)/.test(dt)) {
|
|
1259
|
+
validations.push(dt.slice(0, 60));
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
fields.push({
|
|
1264
|
+
name: fieldName,
|
|
1265
|
+
type: fieldType,
|
|
1266
|
+
required: !isOptional,
|
|
1267
|
+
decorators: decorators.length > 0 ? decorators : undefined,
|
|
1268
|
+
validationRules: validations.length > 0 ? validations : undefined,
|
|
1269
|
+
description: getJSDoc(member, sourceFile),
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
if (fields.length > 0) {
|
|
1274
|
+
dtos.push({
|
|
1275
|
+
name: className,
|
|
1276
|
+
filePath: relPath,
|
|
1277
|
+
line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
|
|
1278
|
+
fields,
|
|
1279
|
+
description: getJSDoc(node, sourceFile),
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
ts.forEachChild(node, visit);
|
|
1284
|
+
}
|
|
1285
|
+
visit(sourceFile);
|
|
1286
|
+
return dtos;
|
|
1287
|
+
}
|
|
1288
|
+
/** 从 TypeScript 文件提取 Service 定义 */
|
|
1289
|
+
function extractServicesFromTsFile(filePath, sourceFile, relPath) {
|
|
1290
|
+
const services = [];
|
|
1291
|
+
function visit(node) {
|
|
1292
|
+
if (ts.isClassDeclaration(node)) {
|
|
1293
|
+
const className = node.name?.getText(sourceFile);
|
|
1294
|
+
if (!className)
|
|
1295
|
+
return;
|
|
1296
|
+
// Service 检测:类名含 Service/Provider/Repository/Manager/Handler
|
|
1297
|
+
const isServiceLike = /(Service|Provider|Repository|Manager|Handler|Facade|Gateway)$/i.test(className);
|
|
1298
|
+
const decorators = ts.canHaveDecorators(node) ? ts.getDecorators(node) : undefined;
|
|
1299
|
+
const hasInjectable = decorators?.some(d => /^@(Injectable|Service|Repository|Component)/.test(d.getText(sourceFile)));
|
|
1300
|
+
if (!isServiceLike && !hasInjectable)
|
|
1301
|
+
return;
|
|
1302
|
+
const methods = [];
|
|
1303
|
+
const injects = [];
|
|
1304
|
+
for (const member of node.members) {
|
|
1305
|
+
// 构造函数注入提取
|
|
1306
|
+
if (ts.isConstructorDeclaration(member)) {
|
|
1307
|
+
for (const param of member.parameters) {
|
|
1308
|
+
const paramType = param.type?.getText(sourceFile);
|
|
1309
|
+
if (paramType && /(Service|Repository|Manager|Provider|Client|Dao|Gateway)/i.test(paramType)) {
|
|
1310
|
+
injects.push(paramType);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
// 方法提取
|
|
1315
|
+
if (ts.isMethodDeclaration(member)) {
|
|
1316
|
+
const methodName = member.name.getText(sourceFile);
|
|
1317
|
+
// 跳过私有方法和生命周期方法
|
|
1318
|
+
if (methodName.startsWith('_') || /^(constructor|onModuleInit|onModuleDestroy|onApplicationBootstrap|beforeApplicationShutdown)$/.test(methodName))
|
|
1319
|
+
continue;
|
|
1320
|
+
const returnType = member.type?.getText(sourceFile);
|
|
1321
|
+
const params = member.parameters.map(p => ({
|
|
1322
|
+
name: p.name.getText(sourceFile),
|
|
1323
|
+
type: p.type?.getText(sourceFile),
|
|
1324
|
+
}));
|
|
1325
|
+
const methodDecs = ts.canHaveDecorators(member) ? ts.getDecorators(member) : undefined;
|
|
1326
|
+
const decorators = methodDecs?.map(d => d.getText(sourceFile).slice(0, 50)) || [];
|
|
1327
|
+
methods.push({
|
|
1328
|
+
name: methodName,
|
|
1329
|
+
returnType,
|
|
1330
|
+
parameters: params,
|
|
1331
|
+
decorators: decorators.length > 0 ? decorators : undefined,
|
|
1332
|
+
description: getJSDoc(member, sourceFile),
|
|
1333
|
+
line: sourceFile.getLineAndCharacterOfPosition(member.getStart(sourceFile)).line + 1,
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
if (methods.length > 0) {
|
|
1338
|
+
services.push({
|
|
1339
|
+
name: className,
|
|
1340
|
+
filePath: relPath,
|
|
1341
|
+
line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
|
|
1342
|
+
methods,
|
|
1343
|
+
description: getJSDoc(node, sourceFile),
|
|
1344
|
+
injects: injects.length > 0 ? injects : undefined,
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
ts.forEachChild(node, visit);
|
|
1349
|
+
}
|
|
1350
|
+
visit(sourceFile);
|
|
1351
|
+
return services;
|
|
1352
|
+
}
|
|
1353
|
+
// ── Java DTO / Service 提取 ──
|
|
1354
|
+
function extractDtosFromJavaFile(content, relPath) {
|
|
1355
|
+
const dtos = [];
|
|
1356
|
+
const lines = content.split('\n');
|
|
1357
|
+
let currentClass = null;
|
|
1358
|
+
let classLine = 0;
|
|
1359
|
+
let classAnnotations = [];
|
|
1360
|
+
let inClass = false;
|
|
1361
|
+
let braceDepth = 0;
|
|
1362
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1363
|
+
const line = lines[i].trim();
|
|
1364
|
+
if (/^@\w+/.test(line)) {
|
|
1365
|
+
classAnnotations.push(line);
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
const classMatch = line.match(/(?:public\s+)?class\s+(\w+)/);
|
|
1369
|
+
if (classMatch) {
|
|
1370
|
+
currentClass = classMatch[1];
|
|
1371
|
+
classLine = i + 1;
|
|
1372
|
+
inClass = true;
|
|
1373
|
+
braceDepth = (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length;
|
|
1374
|
+
const isDtoLike = /(DTO|VO|Request|Response|Input|Output|Query|Command|Form|Param)/i.test(currentClass);
|
|
1375
|
+
if (!isDtoLike) {
|
|
1376
|
+
currentClass = null;
|
|
1377
|
+
inClass = false;
|
|
1378
|
+
classAnnotations = [];
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1381
|
+
classAnnotations = [];
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
if (inClass && currentClass) {
|
|
1385
|
+
braceDepth += (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length;
|
|
1386
|
+
if (braceDepth <= 0) {
|
|
1387
|
+
inClass = false;
|
|
1388
|
+
currentClass = null;
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
const fieldMatch = line.match(/(?:private\s+)?(\w+(?:<[^>]+>)?)\s+(\w+)\s*;/);
|
|
1392
|
+
if (fieldMatch) {
|
|
1393
|
+
const fieldType = fieldMatch[1];
|
|
1394
|
+
const fieldName = fieldMatch[2];
|
|
1395
|
+
let required = true;
|
|
1396
|
+
const validations = [];
|
|
1397
|
+
const decorators = [];
|
|
1398
|
+
for (let j = Math.max(0, i - 3); j < i; j++) {
|
|
1399
|
+
const al = lines[j].trim();
|
|
1400
|
+
if (/^@\w+/.test(al)) {
|
|
1401
|
+
decorators.push(al.slice(0, 80));
|
|
1402
|
+
if (/@(NotNull|NotBlank|NotEmpty|Valid)/.test(al))
|
|
1403
|
+
validations.push(al.slice(0, 60));
|
|
1404
|
+
if (/@(Nullable|Optional)/.test(al))
|
|
1405
|
+
required = false;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
// 查找对应的 getter 是否有 @NotNull
|
|
1409
|
+
for (let j = i + 1; j < Math.min(i + 10, lines.length); j++) {
|
|
1410
|
+
if (lines[j].includes(`get${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)}()`)) {
|
|
1411
|
+
for (let k = Math.max(0, j - 3); k < j; k++) {
|
|
1412
|
+
if (/^@\w+/.test(lines[k].trim())) {
|
|
1413
|
+
decorators.push(lines[k].trim().slice(0, 80));
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
break;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
// 如果已经有这个 DTO,追加字段
|
|
1420
|
+
const existing = dtos.find(d => d.name === currentClass);
|
|
1421
|
+
if (existing) {
|
|
1422
|
+
existing.fields.push({ name: fieldName, type: fieldType, required, decorators: decorators.length > 0 ? decorators : undefined, validationRules: validations.length > 0 ? validations : undefined });
|
|
1423
|
+
}
|
|
1424
|
+
else {
|
|
1425
|
+
dtos.push({
|
|
1426
|
+
name: currentClass,
|
|
1427
|
+
filePath: relPath,
|
|
1428
|
+
line: classLine,
|
|
1429
|
+
fields: [{ name: fieldName, type: fieldType, required, decorators: decorators.length > 0 ? decorators : undefined, validationRules: validations.length > 0 ? validations : undefined }],
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
return dtos.filter(d => d.fields.length > 0);
|
|
1436
|
+
}
|
|
1437
|
+
function extractServicesFromJavaFile(content, relPath) {
|
|
1438
|
+
const services = [];
|
|
1439
|
+
const lines = content.split('\n');
|
|
1440
|
+
let currentClass = null;
|
|
1441
|
+
let classLine = 0;
|
|
1442
|
+
let inClass = false;
|
|
1443
|
+
let braceDepth = 0;
|
|
1444
|
+
let injects = [];
|
|
1445
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1446
|
+
const line = lines[i].trim();
|
|
1447
|
+
const classMatch = line.match(/(?:public\s+)?class\s+(\w+)/);
|
|
1448
|
+
if (classMatch) {
|
|
1449
|
+
currentClass = classMatch[1];
|
|
1450
|
+
classLine = i + 1;
|
|
1451
|
+
inClass = true;
|
|
1452
|
+
braceDepth = (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length;
|
|
1453
|
+
injects = [];
|
|
1454
|
+
const isServiceLike = /(Service|Repository|Manager|Provider|Handler|Facade|Gateway|Impl)/i.test(currentClass);
|
|
1455
|
+
if (!isServiceLike) {
|
|
1456
|
+
currentClass = null;
|
|
1457
|
+
inClass = false;
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
if (inClass && currentClass) {
|
|
1463
|
+
braceDepth += (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length;
|
|
1464
|
+
if (braceDepth <= 0) {
|
|
1465
|
+
if (injects.length > 0 || services.find(s => s.name === currentClass)?.methods.length) {
|
|
1466
|
+
const svc = services.find(s => s.name === currentClass);
|
|
1467
|
+
if (svc)
|
|
1468
|
+
svc.injects = injects.length > 0 ? injects : undefined;
|
|
1469
|
+
}
|
|
1470
|
+
inClass = false;
|
|
1471
|
+
currentClass = null;
|
|
1472
|
+
injects = [];
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
// 构造函数注入
|
|
1476
|
+
const ctorMatch = line.match(/public\s+\w+\s*\(([^)]*)\)/);
|
|
1477
|
+
if (ctorMatch) {
|
|
1478
|
+
const paramStr = ctorMatch[1];
|
|
1479
|
+
const parts = paramStr.split(',');
|
|
1480
|
+
for (const p of parts) {
|
|
1481
|
+
const pm = p.trim().match(/(\w+(?:<[^>]+>)?)\s+(\w+)/);
|
|
1482
|
+
if (pm && /(Service|Repository|Manager|Provider|Client|Dao|Gateway)/i.test(pm[1])) {
|
|
1483
|
+
injects.push(pm[1]);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
// 方法提取
|
|
1488
|
+
const methodMatch = line.match(/(?:public|protected)\s+(?:\w+(?:<[^>]+>)?\s+)?(\w+)\s*\(([^)]*)\)/);
|
|
1489
|
+
if (methodMatch && !/^(if|while|for|switch|return|class|interface|enum)/.test(methodMatch[1])) {
|
|
1490
|
+
const methodName = methodMatch[1];
|
|
1491
|
+
const params = [];
|
|
1492
|
+
const paramStr = methodMatch[2];
|
|
1493
|
+
if (paramStr) {
|
|
1494
|
+
const parts = paramStr.split(',');
|
|
1495
|
+
for (const p of parts) {
|
|
1496
|
+
const pm = p.trim().match(/(?:@\w+(?:\([^)]*\))?\s+)*(\w+(?:<[^>]+>)?)\s+(\w+)/);
|
|
1497
|
+
if (pm)
|
|
1498
|
+
params.push({ name: pm[2], type: pm[1] });
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
const existing = services.find(s => s.name === currentClass);
|
|
1502
|
+
if (existing) {
|
|
1503
|
+
existing.methods.push({ name: methodName, parameters: params, line: i + 1 });
|
|
1504
|
+
}
|
|
1505
|
+
else {
|
|
1506
|
+
services.push({
|
|
1507
|
+
name: currentClass,
|
|
1508
|
+
filePath: relPath,
|
|
1509
|
+
line: classLine,
|
|
1510
|
+
methods: [{ name: methodName, parameters: params, line: i + 1 }],
|
|
1511
|
+
injects: injects.length > 0 ? injects : undefined,
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
return services.filter(s => s.methods.length > 0);
|
|
1518
|
+
}
|
|
1519
|
+
// ── Python DTO / Service 提取 ──
|
|
1520
|
+
function extractDtosFromPythonFile(content, relPath) {
|
|
1521
|
+
const dtos = [];
|
|
1522
|
+
const lines = content.split('\n');
|
|
1523
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1524
|
+
const line = lines[i];
|
|
1525
|
+
const classMatch = line.match(/class\s+(\w+)\s*\(([^)]*)\)/);
|
|
1526
|
+
if (classMatch) {
|
|
1527
|
+
const className = classMatch[1];
|
|
1528
|
+
const bases = classMatch[2];
|
|
1529
|
+
const isDtoLike = /(BaseModel|Schema|Dto|Input|Output|Request|Response|Form)/i.test(bases);
|
|
1530
|
+
if (!isDtoLike)
|
|
1531
|
+
continue;
|
|
1532
|
+
const fields = [];
|
|
1533
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
1534
|
+
const l = lines[j];
|
|
1535
|
+
if (l.trim() && !l.startsWith(' ') && !l.startsWith('\t') && !l.startsWith('#'))
|
|
1536
|
+
break;
|
|
1537
|
+
const fieldMatch = l.match(/^\s+(\w+)\s*:\s*(\w+(?:\[[^\]]+\])?)\s*(?:=\s*(.+))?/);
|
|
1538
|
+
if (fieldMatch) {
|
|
1539
|
+
const fieldName = fieldMatch[1];
|
|
1540
|
+
const fieldType = fieldMatch[2];
|
|
1541
|
+
const defaultExpr = fieldMatch[3];
|
|
1542
|
+
let required = true;
|
|
1543
|
+
const validations = [];
|
|
1544
|
+
if (defaultExpr) {
|
|
1545
|
+
if (/Field\s*\(/.test(defaultExpr)) {
|
|
1546
|
+
if (/default\s*=/.test(defaultExpr))
|
|
1547
|
+
required = false;
|
|
1548
|
+
}
|
|
1549
|
+
else if (defaultExpr.trim() !== '...') {
|
|
1550
|
+
required = false;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
fields.push({ name: fieldName, type: fieldType, required, validationRules: validations.length > 0 ? validations : undefined });
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
if (fields.length > 0) {
|
|
1557
|
+
dtos.push({ name: className, filePath: relPath, line: i + 1, fields });
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
return dtos;
|
|
1562
|
+
}
|
|
1563
|
+
function extractServicesFromPythonFile(content, relPath) {
|
|
1564
|
+
const services = [];
|
|
1565
|
+
const lines = content.split('\n');
|
|
1566
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1567
|
+
const line = lines[i];
|
|
1568
|
+
const classMatch = line.match(/class\s+(\w+)\s*\(/);
|
|
1569
|
+
if (classMatch) {
|
|
1570
|
+
const className = classMatch[1];
|
|
1571
|
+
const isServiceLike = /(Service|Repository|Manager|Provider|Handler|Facade|Gateway)/i.test(className);
|
|
1572
|
+
if (!isServiceLike)
|
|
1573
|
+
continue;
|
|
1574
|
+
const methods = [];
|
|
1575
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
1576
|
+
const l = lines[j];
|
|
1577
|
+
if (l.trim() && !l.startsWith(' ') && !l.startsWith('\t') && !l.startsWith('#'))
|
|
1578
|
+
break;
|
|
1579
|
+
const methodMatch = l.match(/def\s+(\w+)\s*\(([^)]*)\)/);
|
|
1580
|
+
if (methodMatch) {
|
|
1581
|
+
const methodName = methodMatch[1];
|
|
1582
|
+
if (methodName.startsWith('_'))
|
|
1583
|
+
continue;
|
|
1584
|
+
const params = [];
|
|
1585
|
+
const paramStr = methodMatch[2];
|
|
1586
|
+
if (paramStr) {
|
|
1587
|
+
const parts = paramStr.split(',');
|
|
1588
|
+
for (const p of parts) {
|
|
1589
|
+
const pt = p.trim();
|
|
1590
|
+
if (!pt || pt === 'self' || pt === 'cls')
|
|
1591
|
+
continue;
|
|
1592
|
+
const pm = pt.match(/(\w+)\s*:\s*(.+)/);
|
|
1593
|
+
if (pm)
|
|
1594
|
+
params.push({ name: pm[1], type: pm[2] });
|
|
1595
|
+
else {
|
|
1596
|
+
const sm = pt.match(/(\w+)/);
|
|
1597
|
+
if (sm)
|
|
1598
|
+
params.push({ name: sm[1] });
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
methods.push({ name: methodName, parameters: params, line: j + 1 });
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
if (methods.length > 0) {
|
|
1606
|
+
services.push({ name: className, filePath: relPath, line: i + 1, methods });
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
return services;
|
|
1611
|
+
}
|
|
1612
|
+
// ── Go DTO / Service 提取 ──
|
|
1613
|
+
function extractDtosFromGoFile(content, relPath) {
|
|
1614
|
+
const dtos = [];
|
|
1615
|
+
const lines = content.split('\n');
|
|
1616
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1617
|
+
const line = lines[i];
|
|
1618
|
+
const structMatch = line.match(/type\s+(\w+)\s+struct\s*\{/);
|
|
1619
|
+
if (structMatch) {
|
|
1620
|
+
const structName = structMatch[1];
|
|
1621
|
+
const isDtoLike = /(DTO|VO|Request|Response|Input|Output|Query|Command|Form|Param)/i.test(structName);
|
|
1622
|
+
if (!isDtoLike)
|
|
1623
|
+
continue;
|
|
1624
|
+
const fields = [];
|
|
1625
|
+
let inStruct = false;
|
|
1626
|
+
let braceDepth = 0;
|
|
1627
|
+
for (let j = i; j < lines.length; j++) {
|
|
1628
|
+
const l = lines[j];
|
|
1629
|
+
if (!inStruct) {
|
|
1630
|
+
if (l.includes('{'))
|
|
1631
|
+
inStruct = true;
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1634
|
+
braceDepth += (l.match(/\{/g) || []).length - (l.match(/\}/g) || []).length;
|
|
1635
|
+
if (braceDepth < 0)
|
|
1636
|
+
break;
|
|
1637
|
+
const trimmed = l.trim();
|
|
1638
|
+
const fieldMatch = trimmed.match(/^(\w+)\s+(\S+(?:\[[^\]]+\])?)\s*(?:`([^`]+)`)?/);
|
|
1639
|
+
if (fieldMatch && !trimmed.startsWith('//')) {
|
|
1640
|
+
const fieldName = fieldMatch[1];
|
|
1641
|
+
const fieldType = fieldMatch[2];
|
|
1642
|
+
const tagStr = fieldMatch[3] || '';
|
|
1643
|
+
let required = true;
|
|
1644
|
+
const jsonMatch = tagStr.match(/json:"([^"]+)"/);
|
|
1645
|
+
if (jsonMatch && jsonMatch[1].includes('omitempty'))
|
|
1646
|
+
required = false;
|
|
1647
|
+
if (jsonMatch && jsonMatch[1].startsWith('-'))
|
|
1648
|
+
continue; // 跳过不序列化的字段
|
|
1649
|
+
const validateMatch = tagStr.match(/validate:"([^"]+)"/);
|
|
1650
|
+
const validations = validateMatch ? validateMatch[1].split(',').map(s => s.trim()) : [];
|
|
1651
|
+
fields.push({ name: fieldName, type: fieldType, required, validationRules: validations.length > 0 ? validations : undefined });
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
if (fields.length > 0) {
|
|
1655
|
+
dtos.push({ name: structName, filePath: relPath, line: i + 1, fields });
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
return dtos;
|
|
1660
|
+
}
|
|
1661
|
+
function extractServicesFromGoFile(content, relPath) {
|
|
1662
|
+
const services = [];
|
|
1663
|
+
const lines = content.split('\n');
|
|
1664
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1665
|
+
const line = lines[i];
|
|
1666
|
+
const structMatch = line.match(/type\s+(\w+)\s+struct\s*\{/);
|
|
1667
|
+
if (structMatch) {
|
|
1668
|
+
const structName = structMatch[1];
|
|
1669
|
+
const isServiceLike = /(Service|Repository|Manager|Provider|Handler|Facade|Gateway|Impl)/i.test(structName);
|
|
1670
|
+
if (!isServiceLike)
|
|
1671
|
+
continue;
|
|
1672
|
+
const methods = [];
|
|
1673
|
+
for (let j = 0; j < lines.length; j++) {
|
|
1674
|
+
const l = lines[j];
|
|
1675
|
+
const methodMatch = l.match(/func\s+\(\s*\w+\s*\*?\s*\)?\s*\)?\s*(\w+)\s*\(([^)]*)\)/);
|
|
1676
|
+
if (methodMatch) {
|
|
1677
|
+
const methodName = methodMatch[1];
|
|
1678
|
+
if (methodName.startsWith('_'))
|
|
1679
|
+
continue;
|
|
1680
|
+
const params = [];
|
|
1681
|
+
const paramStr = methodMatch[2];
|
|
1682
|
+
if (paramStr) {
|
|
1683
|
+
const parts = paramStr.split(',');
|
|
1684
|
+
for (const p of parts) {
|
|
1685
|
+
const pt = p.trim();
|
|
1686
|
+
if (!pt)
|
|
1687
|
+
continue;
|
|
1688
|
+
const pm = pt.match(/(\w+)\s+(\S+)/);
|
|
1689
|
+
if (pm)
|
|
1690
|
+
params.push({ name: pm[1], type: pm[2] });
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
methods.push({ name: methodName, parameters: params, line: j + 1 });
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
if (methods.length > 0) {
|
|
1697
|
+
services.push({ name: structName, filePath: relPath, line: i + 1, methods });
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
return services;
|
|
1702
|
+
}
|
|
428
1703
|
//# sourceMappingURL=structured-extractor.js.map
|