linguclaw 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +161 -0
- package/dist/agent-system.d.ts +196 -0
- package/dist/agent-system.d.ts.map +1 -0
- package/dist/agent-system.js +738 -0
- package/dist/agent-system.js.map +1 -0
- package/dist/alphabeta.d.ts +54 -0
- package/dist/alphabeta.d.ts.map +1 -0
- package/dist/alphabeta.js +193 -0
- package/dist/alphabeta.js.map +1 -0
- package/dist/browser.d.ts +62 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +224 -0
- package/dist/browser.js.map +1 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +565 -0
- package/dist/cli.js.map +1 -0
- package/dist/code-parser.d.ts +39 -0
- package/dist/code-parser.d.ts.map +1 -0
- package/dist/code-parser.js +385 -0
- package/dist/code-parser.js.map +1 -0
- package/dist/config.d.ts +66 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +232 -0
- package/dist/config.js.map +1 -0
- package/dist/core/engine.d.ts +359 -0
- package/dist/core/engine.d.ts.map +1 -0
- package/dist/core/engine.js +127 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/daemon.d.ts +29 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +212 -0
- package/dist/daemon.js.map +1 -0
- package/dist/email-receiver.d.ts +63 -0
- package/dist/email-receiver.d.ts.map +1 -0
- package/dist/email-receiver.js +553 -0
- package/dist/email-receiver.js.map +1 -0
- package/dist/git-integration.d.ts +180 -0
- package/dist/git-integration.d.ts.map +1 -0
- package/dist/git-integration.js +850 -0
- package/dist/git-integration.js.map +1 -0
- package/dist/inbox.d.ts +84 -0
- package/dist/inbox.d.ts.map +1 -0
- package/dist/inbox.js +198 -0
- package/dist/inbox.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/languages/cpp.d.ts +51 -0
- package/dist/languages/cpp.d.ts.map +1 -0
- package/dist/languages/cpp.js +930 -0
- package/dist/languages/cpp.js.map +1 -0
- package/dist/languages/csharp.d.ts +79 -0
- package/dist/languages/csharp.d.ts.map +1 -0
- package/dist/languages/csharp.js +1776 -0
- package/dist/languages/csharp.js.map +1 -0
- package/dist/languages/go.d.ts +50 -0
- package/dist/languages/go.d.ts.map +1 -0
- package/dist/languages/go.js +882 -0
- package/dist/languages/go.js.map +1 -0
- package/dist/languages/java.d.ts +47 -0
- package/dist/languages/java.d.ts.map +1 -0
- package/dist/languages/java.js +649 -0
- package/dist/languages/java.js.map +1 -0
- package/dist/languages/python.d.ts +47 -0
- package/dist/languages/python.d.ts.map +1 -0
- package/dist/languages/python.js +655 -0
- package/dist/languages/python.js.map +1 -0
- package/dist/languages/rust.d.ts +61 -0
- package/dist/languages/rust.d.ts.map +1 -0
- package/dist/languages/rust.js +1064 -0
- package/dist/languages/rust.js.map +1 -0
- package/dist/logger.d.ts +20 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +133 -0
- package/dist/logger.js.map +1 -0
- package/dist/longterm-memory.d.ts +47 -0
- package/dist/longterm-memory.d.ts.map +1 -0
- package/dist/longterm-memory.js +300 -0
- package/dist/longterm-memory.js.map +1 -0
- package/dist/memory.d.ts +42 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +274 -0
- package/dist/memory.js.map +1 -0
- package/dist/messaging.d.ts +103 -0
- package/dist/messaging.d.ts.map +1 -0
- package/dist/messaging.js +645 -0
- package/dist/messaging.js.map +1 -0
- package/dist/multi-provider.d.ts +69 -0
- package/dist/multi-provider.d.ts.map +1 -0
- package/dist/multi-provider.js +484 -0
- package/dist/multi-provider.js.map +1 -0
- package/dist/orchestrator.d.ts +65 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +441 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/plugins.d.ts +52 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +215 -0
- package/dist/plugins.js.map +1 -0
- package/dist/prism-orchestrator.d.ts +26 -0
- package/dist/prism-orchestrator.d.ts.map +1 -0
- package/dist/prism-orchestrator.js +191 -0
- package/dist/prism-orchestrator.js.map +1 -0
- package/dist/prism.d.ts +46 -0
- package/dist/prism.d.ts.map +1 -0
- package/dist/prism.js +188 -0
- package/dist/prism.js.map +1 -0
- package/dist/privacy.d.ts +23 -0
- package/dist/privacy.d.ts.map +1 -0
- package/dist/privacy.js +220 -0
- package/dist/privacy.js.map +1 -0
- package/dist/proactive.d.ts +30 -0
- package/dist/proactive.d.ts.map +1 -0
- package/dist/proactive.js +260 -0
- package/dist/proactive.js.map +1 -0
- package/dist/refactoring-engine.d.ts +100 -0
- package/dist/refactoring-engine.d.ts.map +1 -0
- package/dist/refactoring-engine.js +717 -0
- package/dist/refactoring-engine.js.map +1 -0
- package/dist/resilience.d.ts +43 -0
- package/dist/resilience.d.ts.map +1 -0
- package/dist/resilience.js +200 -0
- package/dist/resilience.js.map +1 -0
- package/dist/safety.d.ts +40 -0
- package/dist/safety.d.ts.map +1 -0
- package/dist/safety.js +133 -0
- package/dist/safety.js.map +1 -0
- package/dist/sandbox.d.ts +33 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +173 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/scheduler.d.ts +72 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +374 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/semantic-memory.d.ts +70 -0
- package/dist/semantic-memory.d.ts.map +1 -0
- package/dist/semantic-memory.js +430 -0
- package/dist/semantic-memory.js.map +1 -0
- package/dist/skills.d.ts +97 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +575 -0
- package/dist/skills.js.map +1 -0
- package/dist/static/dashboard.html +853 -0
- package/dist/static/hub.html +772 -0
- package/dist/static/index.html +818 -0
- package/dist/static/logo.svg +24 -0
- package/dist/static/workflow-editor.html +913 -0
- package/dist/tools.d.ts +67 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +303 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +295 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +90 -0
- package/dist/types.js.map +1 -0
- package/dist/web.d.ts +76 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +2139 -0
- package/dist/web.js.map +1 -0
- package/dist/workflow-engine.d.ts +114 -0
- package/dist/workflow-engine.d.ts.map +1 -0
- package/dist/workflow-engine.js +855 -0
- package/dist/workflow-engine.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,1776 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* C# Language Support for LinguClaw
|
|
4
|
+
* Advanced parser with LINQ, async/await, and .NET ecosystem analysis
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.CSharpLanguageSupport = exports.CSharpAnalyzer = exports.CSharpParser = void 0;
|
|
8
|
+
class CSharpParser {
|
|
9
|
+
source = '';
|
|
10
|
+
lines = [];
|
|
11
|
+
currentLine = 0;
|
|
12
|
+
parse(source, filePath) {
|
|
13
|
+
this.source = source;
|
|
14
|
+
this.lines = source.split('\n');
|
|
15
|
+
this.currentLine = 0;
|
|
16
|
+
try {
|
|
17
|
+
const ast = this.parseCSharp(source, filePath);
|
|
18
|
+
return {
|
|
19
|
+
ast,
|
|
20
|
+
errors: this.findSyntaxErrors(),
|
|
21
|
+
warnings: this.findPreprocessorIssues(),
|
|
22
|
+
tokens: [],
|
|
23
|
+
comments: this.extractComments(),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
return this.fallbackParse(filePath);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
parseCSharp(source, filePath) {
|
|
31
|
+
const root = {
|
|
32
|
+
type: 'CompilationUnit',
|
|
33
|
+
id: `${filePath}:0:0`,
|
|
34
|
+
location: this.createLocation(filePath, 0, 0, this.lines.length, 0),
|
|
35
|
+
children: [],
|
|
36
|
+
metadata: { language: 'csharp' },
|
|
37
|
+
};
|
|
38
|
+
// Parse using directives
|
|
39
|
+
const usings = this.parseUsingDirectives(source, filePath);
|
|
40
|
+
root.children.push(...usings);
|
|
41
|
+
// Parse namespace declarations
|
|
42
|
+
const namespaces = this.parseNamespaces(source, filePath);
|
|
43
|
+
root.children.push(...namespaces);
|
|
44
|
+
// Parse top-level statements (C# 9.0+)
|
|
45
|
+
const topLevel = this.parseTopLevelStatements(source, filePath);
|
|
46
|
+
if (topLevel.length > 0) {
|
|
47
|
+
root.children.push(...topLevel);
|
|
48
|
+
}
|
|
49
|
+
return root;
|
|
50
|
+
}
|
|
51
|
+
parseUsingDirectives(source, filePath) {
|
|
52
|
+
const directives = [];
|
|
53
|
+
const usingRegex = /^(global\s+)?using\s+(static\s+)?([\w.]+)\s*(?:=\s*([\w.<>]+))?\s*;/gm;
|
|
54
|
+
let match;
|
|
55
|
+
while ((match = usingRegex.exec(source)) !== null) {
|
|
56
|
+
const isGlobal = !!match[1];
|
|
57
|
+
const isStatic = !!match[2];
|
|
58
|
+
const namespace = match[3];
|
|
59
|
+
const alias = match[4];
|
|
60
|
+
const lineNum = source.substring(0, match.index).split('\n').length;
|
|
61
|
+
const node = {
|
|
62
|
+
type: 'UsingDirective',
|
|
63
|
+
id: `${filePath}:${lineNum}:0`,
|
|
64
|
+
location: this.createLocation(filePath, lineNum, 0, lineNum, match[0].length),
|
|
65
|
+
children: [],
|
|
66
|
+
metadata: {
|
|
67
|
+
isGlobal,
|
|
68
|
+
isStatic,
|
|
69
|
+
namespace,
|
|
70
|
+
alias,
|
|
71
|
+
isAlias: !!alias,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
directives.push(node);
|
|
75
|
+
}
|
|
76
|
+
return directives;
|
|
77
|
+
}
|
|
78
|
+
parseNamespaces(source, filePath) {
|
|
79
|
+
const namespaces = [];
|
|
80
|
+
const lines = source.split('\n');
|
|
81
|
+
let i = 0;
|
|
82
|
+
while (i < lines.length) {
|
|
83
|
+
const line = lines[i].trim();
|
|
84
|
+
if (line.startsWith('namespace ')) {
|
|
85
|
+
const nsDecl = this.parseNamespace(lines, i, filePath);
|
|
86
|
+
if (nsDecl) {
|
|
87
|
+
namespaces.push(nsDecl.node);
|
|
88
|
+
i = nsDecl.endIndex + 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else if (line.startsWith('file ')) {
|
|
93
|
+
// File-scoped namespace (C# 10.0+)
|
|
94
|
+
const fileNsDecl = this.parseFileScopedNamespace(lines, i, filePath);
|
|
95
|
+
if (fileNsDecl) {
|
|
96
|
+
namespaces.push(fileNsDecl.node);
|
|
97
|
+
i = fileNsDecl.endIndex + 1;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
i++;
|
|
102
|
+
}
|
|
103
|
+
return namespaces;
|
|
104
|
+
}
|
|
105
|
+
parseNamespace(lines, startIdx, filePath) {
|
|
106
|
+
const line = lines[startIdx].trim();
|
|
107
|
+
const match = line.match(/namespace\s+([\w.]+)\s*{/);
|
|
108
|
+
if (!match)
|
|
109
|
+
return null;
|
|
110
|
+
const name = match[1];
|
|
111
|
+
let braceCount = 1;
|
|
112
|
+
let endIdx = startIdx;
|
|
113
|
+
for (let i = startIdx + 1; i < lines.length && braceCount > 0; i++) {
|
|
114
|
+
for (const char of lines[i]) {
|
|
115
|
+
if (char === '{')
|
|
116
|
+
braceCount++;
|
|
117
|
+
if (char === '}')
|
|
118
|
+
braceCount--;
|
|
119
|
+
}
|
|
120
|
+
if (braceCount === 0) {
|
|
121
|
+
endIdx = i;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const body = lines.slice(startIdx + 1, endIdx);
|
|
126
|
+
const members = this.parseTypeDeclarations(body.join('\n'), filePath);
|
|
127
|
+
const node = {
|
|
128
|
+
type: 'NamespaceDeclaration',
|
|
129
|
+
id: `${filePath}:${startIdx + 1}:0`,
|
|
130
|
+
location: this.createLocation(filePath, startIdx + 1, 0, endIdx + 1, lines[endIdx]?.length || 0),
|
|
131
|
+
children: members,
|
|
132
|
+
metadata: {
|
|
133
|
+
name,
|
|
134
|
+
isFileScoped: false,
|
|
135
|
+
memberCount: members.length,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
return { node, endIndex: endIdx };
|
|
139
|
+
}
|
|
140
|
+
parseFileScopedNamespace(lines, startIdx, filePath) {
|
|
141
|
+
const line = lines[startIdx].trim();
|
|
142
|
+
// File-scoped namespace ends with semicolon, not braces
|
|
143
|
+
const match = line.match(/namespace\s+([\w.]+)\s*;/);
|
|
144
|
+
if (!match)
|
|
145
|
+
return null;
|
|
146
|
+
const name = match[1];
|
|
147
|
+
// All remaining declarations belong to this namespace
|
|
148
|
+
const remainingLines = lines.slice(startIdx + 1);
|
|
149
|
+
const members = this.parseTypeDeclarations(remainingLines.join('\n'), filePath);
|
|
150
|
+
const node = {
|
|
151
|
+
type: 'NamespaceDeclaration',
|
|
152
|
+
id: `${filePath}:${startIdx + 1}:0`,
|
|
153
|
+
location: this.createLocation(filePath, startIdx + 1, 0, lines.length, 0),
|
|
154
|
+
children: members,
|
|
155
|
+
metadata: {
|
|
156
|
+
name,
|
|
157
|
+
isFileScoped: true,
|
|
158
|
+
memberCount: members.length,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
return { node, endIndex: lines.length - 1 };
|
|
162
|
+
}
|
|
163
|
+
parseTypeDeclarations(source, filePath) {
|
|
164
|
+
const declarations = [];
|
|
165
|
+
const lines = source.split('\n');
|
|
166
|
+
let i = 0;
|
|
167
|
+
while (i < lines.length) {
|
|
168
|
+
const line = lines[i].trim();
|
|
169
|
+
// Skip empty lines, comments, attributes
|
|
170
|
+
if (!line || line.startsWith('//') || line.startsWith('#') || line.startsWith('[')) {
|
|
171
|
+
if (line.startsWith('[')) {
|
|
172
|
+
// Parse attributes
|
|
173
|
+
const attrDecl = this.parseAttribute(lines, i, filePath);
|
|
174
|
+
if (attrDecl) {
|
|
175
|
+
i = attrDecl.endIndex + 1;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
i++;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
// Class
|
|
183
|
+
if (this.isClassDeclaration(line)) {
|
|
184
|
+
const classDecl = this.parseClass(lines, i, filePath);
|
|
185
|
+
if (classDecl) {
|
|
186
|
+
declarations.push(classDecl.node);
|
|
187
|
+
i = classDecl.endIndex + 1;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Interface
|
|
192
|
+
if (this.isInterfaceDeclaration(line)) {
|
|
193
|
+
const ifaceDecl = this.parseInterface(lines, i, filePath);
|
|
194
|
+
if (ifaceDecl) {
|
|
195
|
+
declarations.push(ifaceDecl.node);
|
|
196
|
+
i = ifaceDecl.endIndex + 1;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Struct
|
|
201
|
+
if (this.isStructDeclaration(line)) {
|
|
202
|
+
const structDecl = this.parseStruct(lines, i, filePath);
|
|
203
|
+
if (structDecl) {
|
|
204
|
+
declarations.push(structDecl.node);
|
|
205
|
+
i = structDecl.endIndex + 1;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Enum
|
|
210
|
+
if (this.isEnumDeclaration(line)) {
|
|
211
|
+
const enumDecl = this.parseEnum(lines, i, filePath);
|
|
212
|
+
if (enumDecl) {
|
|
213
|
+
declarations.push(enumDecl.node);
|
|
214
|
+
i = enumDecl.endIndex + 1;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Record
|
|
219
|
+
if (this.isRecordDeclaration(line)) {
|
|
220
|
+
const recordDecl = this.parseRecord(lines, i, filePath);
|
|
221
|
+
if (recordDecl) {
|
|
222
|
+
declarations.push(recordDecl.node);
|
|
223
|
+
i = recordDecl.endIndex + 1;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Delegate
|
|
228
|
+
if (this.isDelegateDeclaration(line)) {
|
|
229
|
+
const delegateDecl = this.parseDelegate(lines, i, filePath);
|
|
230
|
+
if (delegateDecl) {
|
|
231
|
+
declarations.push(delegateDecl.node);
|
|
232
|
+
i = delegateDecl.endIndex + 1;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
i++;
|
|
237
|
+
}
|
|
238
|
+
return declarations;
|
|
239
|
+
}
|
|
240
|
+
isClassDeclaration(line) {
|
|
241
|
+
return /^(?:public|private|protected|internal|abstract|sealed|static|partial|unsafe)?\s*(?:class)\s+\w+/.test(line);
|
|
242
|
+
}
|
|
243
|
+
isInterfaceDeclaration(line) {
|
|
244
|
+
return /^(?:public|private|protected|internal|partial)?\s*interface\s+\w+/.test(line);
|
|
245
|
+
}
|
|
246
|
+
isStructDeclaration(line) {
|
|
247
|
+
return /^(?:public|private|protected|internal|readonly|ref|partial|unsafe)?\s*struct\s+\w+/.test(line);
|
|
248
|
+
}
|
|
249
|
+
isEnumDeclaration(line) {
|
|
250
|
+
return /^(?:public|private|protected|internal)?\s*enum\s+\w+/.test(line);
|
|
251
|
+
}
|
|
252
|
+
isRecordDeclaration(line) {
|
|
253
|
+
return /^(?:public|private|protected|internal|abstract|sealed|readonly|ref|partial)?\s*record\s+(?:class|struct\s+)?\w+/.test(line);
|
|
254
|
+
}
|
|
255
|
+
isDelegateDeclaration(line) {
|
|
256
|
+
return /^(?:public|private|protected|internal)?\s*delegate\s+/.test(line);
|
|
257
|
+
}
|
|
258
|
+
parseClass(lines, startIdx, filePath) {
|
|
259
|
+
const line = lines[startIdx].trim();
|
|
260
|
+
// Extract modifiers and class name
|
|
261
|
+
const match = line.match(/^(.*?)class\s+(\w+)(?:<([^>]+)>)?(?:\s*:\s*([^{]+))?/);
|
|
262
|
+
if (!match)
|
|
263
|
+
return null;
|
|
264
|
+
const modifiers = match[1].trim().split(/\s+/).filter(m => m);
|
|
265
|
+
const name = match[2];
|
|
266
|
+
const typeParams = match[3];
|
|
267
|
+
const inheritance = match[4];
|
|
268
|
+
let endIdx = startIdx;
|
|
269
|
+
let braceCount = 0;
|
|
270
|
+
let foundOpenBrace = false;
|
|
271
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
272
|
+
for (const char of lines[i]) {
|
|
273
|
+
if (char === '{') {
|
|
274
|
+
braceCount++;
|
|
275
|
+
foundOpenBrace = true;
|
|
276
|
+
}
|
|
277
|
+
if (char === '}')
|
|
278
|
+
braceCount--;
|
|
279
|
+
}
|
|
280
|
+
if (foundOpenBrace && braceCount === 0) {
|
|
281
|
+
endIdx = i;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const body = lines.slice(startIdx, endIdx + 1).join('\n');
|
|
286
|
+
const members = this.parseClassMembers(body, filePath);
|
|
287
|
+
const node = {
|
|
288
|
+
type: 'ClassDeclaration',
|
|
289
|
+
id: `${filePath}:${startIdx + 1}:0`,
|
|
290
|
+
location: this.createLocation(filePath, startIdx + 1, 0, endIdx + 1, lines[endIdx]?.length || 0),
|
|
291
|
+
children: members,
|
|
292
|
+
metadata: {
|
|
293
|
+
name,
|
|
294
|
+
modifiers,
|
|
295
|
+
typeParameters: typeParams ? typeParams.split(',').map(p => p.trim()) : [],
|
|
296
|
+
isGeneric: !!typeParams,
|
|
297
|
+
isAbstract: modifiers.includes('abstract'),
|
|
298
|
+
isSealed: modifiers.includes('sealed'),
|
|
299
|
+
isStatic: modifiers.includes('static'),
|
|
300
|
+
isPartial: modifiers.includes('partial'),
|
|
301
|
+
isUnsafe: modifiers.includes('unsafe'),
|
|
302
|
+
inheritance: this.parseInheritance(inheritance),
|
|
303
|
+
baseClass: inheritance?.split(',')[0]?.trim() || null,
|
|
304
|
+
implementedInterfaces: inheritance?.split(',').slice(1).map(i => i.trim()) || [],
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
return { node, endIndex: endIdx };
|
|
308
|
+
}
|
|
309
|
+
parseInterface(lines, startIdx, filePath) {
|
|
310
|
+
const line = lines[startIdx].trim();
|
|
311
|
+
const match = line.match(/^(.*?)interface\s+(\w+)(?:<([^>]+)>)?(?:\s*:\s*([^{]+))?/);
|
|
312
|
+
if (!match)
|
|
313
|
+
return null;
|
|
314
|
+
const modifiers = match[1].trim().split(/\s+/).filter(m => m);
|
|
315
|
+
const name = match[2];
|
|
316
|
+
const typeParams = match[3];
|
|
317
|
+
const inheritance = match[4];
|
|
318
|
+
let endIdx = startIdx;
|
|
319
|
+
let braceCount = 0;
|
|
320
|
+
let foundOpenBrace = false;
|
|
321
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
322
|
+
for (const char of lines[i]) {
|
|
323
|
+
if (char === '{') {
|
|
324
|
+
braceCount++;
|
|
325
|
+
foundOpenBrace = true;
|
|
326
|
+
}
|
|
327
|
+
if (char === '}')
|
|
328
|
+
braceCount--;
|
|
329
|
+
}
|
|
330
|
+
if (foundOpenBrace && braceCount === 0) {
|
|
331
|
+
endIdx = i;
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const body = lines.slice(startIdx, endIdx + 1).join('\n');
|
|
336
|
+
const members = this.parseInterfaceMembers(body, filePath);
|
|
337
|
+
const node = {
|
|
338
|
+
type: 'InterfaceDeclaration',
|
|
339
|
+
id: `${filePath}:${startIdx + 1}:0`,
|
|
340
|
+
location: this.createLocation(filePath, startIdx + 1, 0, endIdx + 1, lines[endIdx]?.length || 0),
|
|
341
|
+
children: members,
|
|
342
|
+
metadata: {
|
|
343
|
+
name,
|
|
344
|
+
modifiers,
|
|
345
|
+
typeParameters: typeParams ? typeParams.split(',').map(p => p.trim()) : [],
|
|
346
|
+
isGeneric: !!typeParams,
|
|
347
|
+
isPartial: modifiers.includes('partial'),
|
|
348
|
+
extendedInterfaces: inheritance?.split(',').map(i => i.trim()) || [],
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
return { node, endIndex: endIdx };
|
|
352
|
+
}
|
|
353
|
+
parseStruct(lines, startIdx, filePath) {
|
|
354
|
+
const line = lines[startIdx].trim();
|
|
355
|
+
const match = line.match(/^(.*?)struct\s+(\w+)(?:<([^>]+)>)?(?:\s*:\s*([^{]+))?/);
|
|
356
|
+
if (!match)
|
|
357
|
+
return null;
|
|
358
|
+
const modifiers = match[1].trim().split(/\s+/).filter(m => m);
|
|
359
|
+
const name = match[2];
|
|
360
|
+
const typeParams = match[3];
|
|
361
|
+
const inheritance = match[4];
|
|
362
|
+
let endIdx = startIdx;
|
|
363
|
+
let braceCount = 0;
|
|
364
|
+
let foundOpenBrace = false;
|
|
365
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
366
|
+
for (const char of lines[i]) {
|
|
367
|
+
if (char === '{') {
|
|
368
|
+
braceCount++;
|
|
369
|
+
foundOpenBrace = true;
|
|
370
|
+
}
|
|
371
|
+
if (char === '}')
|
|
372
|
+
braceCount--;
|
|
373
|
+
}
|
|
374
|
+
if (foundOpenBrace && braceCount === 0) {
|
|
375
|
+
endIdx = i;
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const body = lines.slice(startIdx, endIdx + 1).join('\n');
|
|
380
|
+
const members = this.parseStructMembers(body, filePath);
|
|
381
|
+
const node = {
|
|
382
|
+
type: 'StructDeclaration',
|
|
383
|
+
id: `${filePath}:${startIdx + 1}:0`,
|
|
384
|
+
location: this.createLocation(filePath, startIdx + 1, 0, endIdx + 1, lines[endIdx]?.length || 0),
|
|
385
|
+
children: members,
|
|
386
|
+
metadata: {
|
|
387
|
+
name,
|
|
388
|
+
modifiers,
|
|
389
|
+
typeParameters: typeParams ? typeParams.split(',').map(p => p.trim()) : [],
|
|
390
|
+
isGeneric: !!typeParams,
|
|
391
|
+
isReadonly: modifiers.includes('readonly'),
|
|
392
|
+
isRef: modifiers.includes('ref'),
|
|
393
|
+
isPartial: modifiers.includes('partial'),
|
|
394
|
+
isUnsafe: modifiers.includes('unsafe'),
|
|
395
|
+
implementedInterfaces: inheritance?.split(',').map(i => i.trim()) || [],
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
return { node, endIndex: endIdx };
|
|
399
|
+
}
|
|
400
|
+
parseRecord(lines, startIdx, filePath) {
|
|
401
|
+
const line = lines[startIdx].trim();
|
|
402
|
+
const match = line.match(/^(.*?)record\s+(?:class|struct\s+)?(\w+)(?:<([^>]+)>)?(?:\s*\(([^)]*)\))?(?:\s*:\s*([^{]+))?/);
|
|
403
|
+
if (!match)
|
|
404
|
+
return null;
|
|
405
|
+
const modifiers = match[1].trim().split(/\s+/).filter(m => m);
|
|
406
|
+
const name = match[2];
|
|
407
|
+
const typeParams = match[3];
|
|
408
|
+
const positionalParams = match[4];
|
|
409
|
+
const inheritance = match[5];
|
|
410
|
+
let endIdx = startIdx;
|
|
411
|
+
let braceCount = 0;
|
|
412
|
+
let foundOpenBrace = false;
|
|
413
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
414
|
+
for (const char of lines[i]) {
|
|
415
|
+
if (char === '{') {
|
|
416
|
+
braceCount++;
|
|
417
|
+
foundOpenBrace = true;
|
|
418
|
+
}
|
|
419
|
+
if (char === '}')
|
|
420
|
+
braceCount--;
|
|
421
|
+
}
|
|
422
|
+
if ((foundOpenBrace && braceCount === 0) || (positionalParams && !inheritance && lines[i].includes(';'))) {
|
|
423
|
+
endIdx = i;
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const node = {
|
|
428
|
+
type: 'RecordDeclaration',
|
|
429
|
+
id: `${filePath}:${startIdx + 1}:0`,
|
|
430
|
+
location: this.createLocation(filePath, startIdx + 1, 0, endIdx + 1, lines[endIdx]?.length || 0),
|
|
431
|
+
children: [],
|
|
432
|
+
metadata: {
|
|
433
|
+
name,
|
|
434
|
+
modifiers,
|
|
435
|
+
typeParameters: typeParams ? typeParams.split(',').map(p => p.trim()) : [],
|
|
436
|
+
isGeneric: !!typeParams,
|
|
437
|
+
isClass: !modifiers.includes('struct') && !line.includes('record struct'),
|
|
438
|
+
isStruct: modifiers.includes('struct') || line.includes('record struct'),
|
|
439
|
+
positionalParameters: positionalParams ? positionalParams.split(',').map(p => p.trim()) : [],
|
|
440
|
+
inheritance: this.parseInheritance(inheritance),
|
|
441
|
+
isAbstract: modifiers.includes('abstract'),
|
|
442
|
+
isSealed: modifiers.includes('sealed'),
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
return { node, endIndex: endIdx };
|
|
446
|
+
}
|
|
447
|
+
parseEnum(lines, startIdx, filePath) {
|
|
448
|
+
const line = lines[startIdx].trim();
|
|
449
|
+
const match = line.match(/^(.*?)enum\s+(\w+)(?:\s*:\s*(\w+))?/);
|
|
450
|
+
if (!match)
|
|
451
|
+
return null;
|
|
452
|
+
const modifiers = match[1].trim().split(/\s+/).filter(m => m);
|
|
453
|
+
const name = match[2];
|
|
454
|
+
const underlyingType = match[3] || 'int';
|
|
455
|
+
let endIdx = startIdx;
|
|
456
|
+
let braceCount = 0;
|
|
457
|
+
let foundOpenBrace = false;
|
|
458
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
459
|
+
for (const char of lines[i]) {
|
|
460
|
+
if (char === '{') {
|
|
461
|
+
braceCount++;
|
|
462
|
+
foundOpenBrace = true;
|
|
463
|
+
}
|
|
464
|
+
if (char === '}')
|
|
465
|
+
braceCount--;
|
|
466
|
+
}
|
|
467
|
+
if (foundOpenBrace && braceCount === 0) {
|
|
468
|
+
endIdx = i;
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const body = lines.slice(startIdx, endIdx + 1).join('\n');
|
|
473
|
+
const values = this.extractEnumValues(body);
|
|
474
|
+
const node = {
|
|
475
|
+
type: 'EnumDeclaration',
|
|
476
|
+
id: `${filePath}:${startIdx + 1}:0`,
|
|
477
|
+
location: this.createLocation(filePath, startIdx + 1, 0, endIdx + 1, lines[endIdx]?.length || 0),
|
|
478
|
+
children: [],
|
|
479
|
+
metadata: {
|
|
480
|
+
name,
|
|
481
|
+
modifiers,
|
|
482
|
+
underlyingType,
|
|
483
|
+
values,
|
|
484
|
+
hasFlagsAttribute: modifiers.includes('[Flags]') || body.includes('[Flags]'),
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
return { node, endIndex: endIdx };
|
|
488
|
+
}
|
|
489
|
+
parseDelegate(lines, startIdx, filePath) {
|
|
490
|
+
const line = lines[startIdx].trim();
|
|
491
|
+
const match = line.match(/^(.*?)delegate\s+([\w<>\[\],\s]+)\s+(\w+)\s*\(([^)]*)\)\s*;/);
|
|
492
|
+
if (!match)
|
|
493
|
+
return null;
|
|
494
|
+
const modifiers = match[1].trim().split(/\s+/).filter(m => m);
|
|
495
|
+
const returnType = match[2].trim();
|
|
496
|
+
const name = match[3];
|
|
497
|
+
const parameters = match[4];
|
|
498
|
+
const node = {
|
|
499
|
+
type: 'DelegateDeclaration',
|
|
500
|
+
id: `${filePath}:${startIdx + 1}:0`,
|
|
501
|
+
location: this.createLocation(filePath, startIdx + 1, 0, startIdx + 1, line.length),
|
|
502
|
+
children: [],
|
|
503
|
+
metadata: {
|
|
504
|
+
name,
|
|
505
|
+
modifiers,
|
|
506
|
+
returnType,
|
|
507
|
+
parameters: parameters ? parameters.split(',').map(p => p.trim()) : [],
|
|
508
|
+
},
|
|
509
|
+
};
|
|
510
|
+
return { node, endIndex: startIdx };
|
|
511
|
+
}
|
|
512
|
+
parseAttribute(lines, startIdx, filePath) {
|
|
513
|
+
const line = lines[startIdx].trim();
|
|
514
|
+
const match = line.match(/^\[([\w]+)(?:\(([^)]*)\))?\]\s*(?:$|\/\/)/);
|
|
515
|
+
if (!match)
|
|
516
|
+
return null;
|
|
517
|
+
const name = match[1];
|
|
518
|
+
const arguments_ = match[2];
|
|
519
|
+
// Check if it's a target-specific attribute [target: Attribute]
|
|
520
|
+
const targetMatch = line.match(/^\[(\w+):\s*([\w]+)/);
|
|
521
|
+
const node = {
|
|
522
|
+
type: 'Attribute',
|
|
523
|
+
id: `${filePath}:${startIdx + 1}:0`,
|
|
524
|
+
location: this.createLocation(filePath, startIdx + 1, 0, startIdx + 1, line.length),
|
|
525
|
+
children: [],
|
|
526
|
+
metadata: {
|
|
527
|
+
name: targetMatch ? targetMatch[2] : name,
|
|
528
|
+
target: targetMatch ? targetMatch[1] : null,
|
|
529
|
+
arguments: arguments_ || null,
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
return { node, endIndex: startIdx };
|
|
533
|
+
}
|
|
534
|
+
parseClassMembers(body, filePath) {
|
|
535
|
+
const members = [];
|
|
536
|
+
const lines = body.split('\n');
|
|
537
|
+
for (let i = 0; i < lines.length; i++) {
|
|
538
|
+
const line = lines[i].trim();
|
|
539
|
+
// Skip empty lines and comments
|
|
540
|
+
if (!line || line.startsWith('//') || line.startsWith('/*'))
|
|
541
|
+
continue;
|
|
542
|
+
// Method
|
|
543
|
+
if (this.isMethodDeclaration(line)) {
|
|
544
|
+
const methodDecl = this.parseMethod(lines, i, filePath);
|
|
545
|
+
if (methodDecl) {
|
|
546
|
+
members.push(methodDecl.node);
|
|
547
|
+
i = methodDecl.endIndex;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
// Property
|
|
551
|
+
if (this.isPropertyDeclaration(line)) {
|
|
552
|
+
const propDecl = this.parseProperty(lines, i, filePath);
|
|
553
|
+
if (propDecl) {
|
|
554
|
+
members.push(propDecl.node);
|
|
555
|
+
i = propDecl.endIndex;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
// Field
|
|
559
|
+
if (this.isFieldDeclaration(line)) {
|
|
560
|
+
const fieldDecl = this.parseField(lines, i, filePath);
|
|
561
|
+
if (fieldDecl) {
|
|
562
|
+
members.push(fieldDecl.node);
|
|
563
|
+
i = fieldDecl.endIndex;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
// Constructor
|
|
567
|
+
if (this.isConstructorDeclaration(line)) {
|
|
568
|
+
const ctorDecl = this.parseConstructor(lines, i, filePath);
|
|
569
|
+
if (ctorDecl) {
|
|
570
|
+
members.push(ctorDecl.node);
|
|
571
|
+
i = ctorDecl.endIndex;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
// Destructor/Finalizer
|
|
575
|
+
if (line.includes('~' + this.extractClassName(body))) {
|
|
576
|
+
const dtorDecl = this.parseDestructor(lines, i, filePath);
|
|
577
|
+
if (dtorDecl) {
|
|
578
|
+
members.push(dtorDecl.node);
|
|
579
|
+
i = dtorDecl.endIndex;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
// Event
|
|
583
|
+
if (this.isEventDeclaration(line)) {
|
|
584
|
+
const eventDecl = this.parseEvent(lines, i, filePath);
|
|
585
|
+
if (eventDecl) {
|
|
586
|
+
members.push(eventDecl.node);
|
|
587
|
+
i = eventDecl.endIndex;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// Indexer
|
|
591
|
+
if (this.isIndexerDeclaration(line)) {
|
|
592
|
+
const indexerDecl = this.parseIndexer(lines, i, filePath);
|
|
593
|
+
if (indexerDecl) {
|
|
594
|
+
members.push(indexerDecl.node);
|
|
595
|
+
i = indexerDecl.endIndex;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// Operator
|
|
599
|
+
if (this.isOperatorDeclaration(line)) {
|
|
600
|
+
const operatorDecl = this.parseOperator(lines, i, filePath);
|
|
601
|
+
if (operatorDecl) {
|
|
602
|
+
members.push(operatorDecl.node);
|
|
603
|
+
i = operatorDecl.endIndex;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
// Nested type
|
|
607
|
+
if (this.isNestedTypeDeclaration(line)) {
|
|
608
|
+
const nestedDecl = this.parseNestedType(lines, i, filePath);
|
|
609
|
+
if (nestedDecl) {
|
|
610
|
+
members.push(nestedDecl.node);
|
|
611
|
+
i = nestedDecl.endIndex;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return members;
|
|
616
|
+
}
|
|
617
|
+
parseInterfaceMembers(body, filePath) {
|
|
618
|
+
const members = [];
|
|
619
|
+
const lines = body.split('\n');
|
|
620
|
+
for (let i = 0; i < lines.length; i++) {
|
|
621
|
+
const line = lines[i].trim();
|
|
622
|
+
// Method signature
|
|
623
|
+
const methodMatch = line.match(/([\w<>\[\],\s]+)\s+(\w+)\s*\(([^)]*)\)\s*;/);
|
|
624
|
+
if (methodMatch) {
|
|
625
|
+
members.push({
|
|
626
|
+
type: 'MethodDeclaration',
|
|
627
|
+
id: `${filePath}:${i + 1}:0`,
|
|
628
|
+
location: this.createLocation(filePath, i + 1, 0, i + 1, line.length),
|
|
629
|
+
children: [],
|
|
630
|
+
metadata: {
|
|
631
|
+
name: methodMatch[2],
|
|
632
|
+
returnType: methodMatch[1].trim(),
|
|
633
|
+
parameters: methodMatch[3],
|
|
634
|
+
isAbstract: true,
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
// Property signature
|
|
639
|
+
const propMatch = line.match(/([\w<>\[\],\s]+)\s+(\w+)\s*\{\s*(get|set)\s*;\s*(?:get|set)?\s*;?\s*\}/);
|
|
640
|
+
if (propMatch) {
|
|
641
|
+
members.push({
|
|
642
|
+
type: 'PropertyDeclaration',
|
|
643
|
+
id: `${filePath}:${i + 1}:0`,
|
|
644
|
+
location: this.createLocation(filePath, i + 1, 0, i + 1, line.length),
|
|
645
|
+
children: [],
|
|
646
|
+
metadata: {
|
|
647
|
+
name: propMatch[2],
|
|
648
|
+
type: propMatch[1].trim(),
|
|
649
|
+
hasGetter: line.includes('get'),
|
|
650
|
+
hasSetter: line.includes('set'),
|
|
651
|
+
},
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
// Indexer signature
|
|
655
|
+
const indexerMatch = line.match(/([\w<>\[\],\s]+)\s+this\s*\[([^\]]+)\]\s*\{/);
|
|
656
|
+
if (indexerMatch) {
|
|
657
|
+
members.push({
|
|
658
|
+
type: 'IndexerDeclaration',
|
|
659
|
+
id: `${filePath}:${i + 1}:0`,
|
|
660
|
+
location: this.createLocation(filePath, i + 1, 0, i + 1, line.length),
|
|
661
|
+
children: [],
|
|
662
|
+
metadata: {
|
|
663
|
+
type: indexerMatch[1].trim(),
|
|
664
|
+
parameters: indexerMatch[2],
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
// Event signature
|
|
669
|
+
const eventMatch = line.match(/event\s+([\w<>]+)\s+(\w+)\s*;/);
|
|
670
|
+
if (eventMatch) {
|
|
671
|
+
members.push({
|
|
672
|
+
type: 'EventDeclaration',
|
|
673
|
+
id: `${filePath}:${i + 1}:0`,
|
|
674
|
+
location: this.createLocation(filePath, i + 1, 0, i + 1, line.length),
|
|
675
|
+
children: [],
|
|
676
|
+
metadata: {
|
|
677
|
+
name: eventMatch[2],
|
|
678
|
+
type: eventMatch[1].trim(),
|
|
679
|
+
},
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return members;
|
|
684
|
+
}
|
|
685
|
+
parseStructMembers(body, filePath) {
|
|
686
|
+
// Structs have same members as classes
|
|
687
|
+
return this.parseClassMembers(body, filePath);
|
|
688
|
+
}
|
|
689
|
+
isMethodDeclaration(line) {
|
|
690
|
+
return /(?:public|private|protected|internal|static|virtual|abstract|override|sealed|extern|async|partial|unsafe)?\s*(?:[\w<>\[\],\s]+)\s+\w+\s*\(/.test(line) &&
|
|
691
|
+
!line.includes('class ') &&
|
|
692
|
+
!line.includes('struct ') &&
|
|
693
|
+
!line.includes('interface ') &&
|
|
694
|
+
!line.includes('enum ') &&
|
|
695
|
+
!line.includes('delegate ') &&
|
|
696
|
+
!line.includes('record ');
|
|
697
|
+
}
|
|
698
|
+
isPropertyDeclaration(line) {
|
|
699
|
+
return /(?:public|private|protected|internal|static|virtual|abstract|override|sealed|extern)?\s*(?:[\w<>\[\],\s]+)\s+\w+\s*\{/.test(line) &&
|
|
700
|
+
!line.includes('class ') &&
|
|
701
|
+
!line.includes('new ') &&
|
|
702
|
+
!line.includes('(');
|
|
703
|
+
}
|
|
704
|
+
isFieldDeclaration(line) {
|
|
705
|
+
return /(?:public|private|protected|internal|static|readonly|volatile|const|fixed|unsafe)?\s*(?:[\w<>\[\],\s]+)\s+\w+\s*(?:=|;)/.test(line) &&
|
|
706
|
+
!line.includes('(') &&
|
|
707
|
+
!line.includes('{') &&
|
|
708
|
+
!line.includes('class ') &&
|
|
709
|
+
!line.includes('struct ') &&
|
|
710
|
+
!line.includes('interface ');
|
|
711
|
+
}
|
|
712
|
+
isConstructorDeclaration(line) {
|
|
713
|
+
return /(?:public|private|protected|internal|static|extern|unsafe)?\s*\w+\s*\([^)]*\)\s*(?::\s*base\s*\(|:\s*this\s*\()?\s*\{/.test(line);
|
|
714
|
+
}
|
|
715
|
+
isEventDeclaration(line) {
|
|
716
|
+
return /(?:public|private|protected|internal|static|virtual|abstract|override|sealed|extern)?\s*event\s+/.test(line);
|
|
717
|
+
}
|
|
718
|
+
isIndexerDeclaration(line) {
|
|
719
|
+
return /(?:public|private|protected|internal|static|virtual|abstract|override|sealed|extern)?\s*(?:[\w<>\[\],\s]+)\s+this\s*\[/.test(line);
|
|
720
|
+
}
|
|
721
|
+
isOperatorDeclaration(line) {
|
|
722
|
+
return /(?:public|static|extern)?\s*(?:[\w<>\[\],\s]+)\s+operator\s+/.test(line);
|
|
723
|
+
}
|
|
724
|
+
isNestedTypeDeclaration(line) {
|
|
725
|
+
return /(?:public|private|protected|internal)?\s*(?:class|struct|interface|enum|record)\s+/.test(line);
|
|
726
|
+
}
|
|
727
|
+
parseMethod(lines, startIdx, filePath) {
|
|
728
|
+
const line = lines[startIdx].trim();
|
|
729
|
+
// Match method signature with modifiers
|
|
730
|
+
const match = line.match(/^(.*?)\s*([\w<>\[\],\s]+)\s+(\w+)\s*\(([^)]*)\)(?:\s*where\s+[^;]+)?\s*\{/);
|
|
731
|
+
if (!match) {
|
|
732
|
+
// Check for expression-bodied method
|
|
733
|
+
const exprMatch = line.match(/^(.*?)\s*([\w<>\[\],\s]+)\s+(\w+)\s*\(([^)]*)\)\s*=>\s*(.+);/);
|
|
734
|
+
if (exprMatch) {
|
|
735
|
+
return this.parseExpressionBodiedMethod(lines, startIdx, filePath, exprMatch);
|
|
736
|
+
}
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
const modifiers = match[1].trim().split(/\s+/).filter(m => m);
|
|
740
|
+
const returnType = match[2].trim();
|
|
741
|
+
const name = match[3];
|
|
742
|
+
const parameters = match[4];
|
|
743
|
+
// Find method body end
|
|
744
|
+
let endIdx = startIdx;
|
|
745
|
+
let braceCount = 0;
|
|
746
|
+
let foundOpenBrace = false;
|
|
747
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
748
|
+
for (const char of lines[i]) {
|
|
749
|
+
if (char === '{') {
|
|
750
|
+
braceCount++;
|
|
751
|
+
foundOpenBrace = true;
|
|
752
|
+
}
|
|
753
|
+
if (char === '}')
|
|
754
|
+
braceCount--;
|
|
755
|
+
}
|
|
756
|
+
if (foundOpenBrace && braceCount === 0) {
|
|
757
|
+
endIdx = i;
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
const node = {
|
|
762
|
+
type: 'MethodDeclaration',
|
|
763
|
+
id: `${filePath}:${startIdx + 1}:0`,
|
|
764
|
+
location: this.createLocation(filePath, startIdx + 1, 0, endIdx + 1, lines[endIdx]?.length || 0),
|
|
765
|
+
children: [],
|
|
766
|
+
metadata: {
|
|
767
|
+
name,
|
|
768
|
+
returnType,
|
|
769
|
+
parameters: parameters ? parameters.split(',').map(p => p.trim()) : [],
|
|
770
|
+
modifiers,
|
|
771
|
+
isPublic: modifiers.includes('public'),
|
|
772
|
+
isPrivate: modifiers.includes('private'),
|
|
773
|
+
isProtected: modifiers.includes('protected'),
|
|
774
|
+
isInternal: modifiers.includes('internal'),
|
|
775
|
+
isStatic: modifiers.includes('static'),
|
|
776
|
+
isVirtual: modifiers.includes('virtual'),
|
|
777
|
+
isAbstract: modifiers.includes('abstract'),
|
|
778
|
+
isOverride: modifiers.includes('override'),
|
|
779
|
+
isSealed: modifiers.includes('sealed'),
|
|
780
|
+
isExtern: modifiers.includes('extern'),
|
|
781
|
+
isAsync: modifiers.includes('async'),
|
|
782
|
+
isPartial: modifiers.includes('partial'),
|
|
783
|
+
isUnsafe: modifiers.includes('unsafe'),
|
|
784
|
+
isExpressionBodied: false,
|
|
785
|
+
hasBody: true,
|
|
786
|
+
},
|
|
787
|
+
};
|
|
788
|
+
return { node, endIndex: endIdx };
|
|
789
|
+
}
|
|
790
|
+
parseExpressionBodiedMethod(lines, startIdx, filePath, match) {
|
|
791
|
+
const modifiers = match[1].trim().split(/\s+/).filter(m => m);
|
|
792
|
+
const returnType = match[2].trim();
|
|
793
|
+
const name = match[3];
|
|
794
|
+
const parameters = match[4];
|
|
795
|
+
const node = {
|
|
796
|
+
type: 'MethodDeclaration',
|
|
797
|
+
id: `${filePath}:${startIdx + 1}:0`,
|
|
798
|
+
location: this.createLocation(filePath, startIdx + 1, 0, startIdx + 1, lines[startIdx].length),
|
|
799
|
+
children: [],
|
|
800
|
+
metadata: {
|
|
801
|
+
name,
|
|
802
|
+
returnType,
|
|
803
|
+
parameters: parameters ? parameters.split(',').map(p => p.trim()) : [],
|
|
804
|
+
modifiers,
|
|
805
|
+
isPublic: modifiers.includes('public'),
|
|
806
|
+
isStatic: modifiers.includes('static'),
|
|
807
|
+
isVirtual: modifiers.includes('virtual'),
|
|
808
|
+
isOverride: modifiers.includes('override'),
|
|
809
|
+
isAsync: modifiers.includes('async'),
|
|
810
|
+
isExpressionBodied: true,
|
|
811
|
+
hasBody: true,
|
|
812
|
+
},
|
|
813
|
+
};
|
|
814
|
+
return { node, endIndex: startIdx };
|
|
815
|
+
}
|
|
816
|
+
parseProperty(lines, startIdx, filePath) {
|
|
817
|
+
const line = lines[startIdx].trim();
|
|
818
|
+
// Auto-property or full property
|
|
819
|
+
const match = line.match(/^(.*?)\s*([\w<>\[\],\s]+)\s+(\w+)\s*\{/);
|
|
820
|
+
if (!match)
|
|
821
|
+
return null;
|
|
822
|
+
const modifiers = match[1].trim().split(/\s+/).filter(m => m);
|
|
823
|
+
const type = match[2].trim();
|
|
824
|
+
const name = match[3];
|
|
825
|
+
// Find property end
|
|
826
|
+
let endIdx = startIdx;
|
|
827
|
+
let braceCount = 0;
|
|
828
|
+
let foundOpenBrace = false;
|
|
829
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
830
|
+
const currentLine = lines[i];
|
|
831
|
+
for (const char of currentLine) {
|
|
832
|
+
if (char === '{') {
|
|
833
|
+
braceCount++;
|
|
834
|
+
foundOpenBrace = true;
|
|
835
|
+
}
|
|
836
|
+
if (char === '}')
|
|
837
|
+
braceCount--;
|
|
838
|
+
}
|
|
839
|
+
if (foundOpenBrace && braceCount === 0) {
|
|
840
|
+
endIdx = i;
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
// Expression-bodied property
|
|
844
|
+
if (currentLine.includes('=>') && !foundOpenBrace) {
|
|
845
|
+
if (currentLine.includes(';')) {
|
|
846
|
+
endIdx = i;
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
const body = lines.slice(startIdx, endIdx + 1).join('\n');
|
|
852
|
+
const node = {
|
|
853
|
+
type: 'PropertyDeclaration',
|
|
854
|
+
id: `${filePath}:${startIdx + 1}:0`,
|
|
855
|
+
location: this.createLocation(filePath, startIdx + 1, 0, endIdx + 1, lines[endIdx]?.length || 0),
|
|
856
|
+
children: [],
|
|
857
|
+
metadata: {
|
|
858
|
+
name,
|
|
859
|
+
type,
|
|
860
|
+
modifiers,
|
|
861
|
+
hasGetter: body.includes('get'),
|
|
862
|
+
hasSetter: body.includes('set'),
|
|
863
|
+
isAutoProperty: body.includes('get;') && body.includes('set;') && !body.includes('{ get {'),
|
|
864
|
+
isInitOnly: body.includes('init;'),
|
|
865
|
+
isExpressionBodied: body.includes('=>'),
|
|
866
|
+
isRequired: modifiers.includes('required'),
|
|
867
|
+
},
|
|
868
|
+
};
|
|
869
|
+
return { node, endIndex: endIdx };
|
|
870
|
+
}
|
|
871
|
+
parseField(lines, startIdx, filePath) {
|
|
872
|
+
const line = lines[startIdx].trim();
|
|
873
|
+
const match = line.match(/^(.*?)\s*([\w<>\[\],\s]+)\s+(\w+)\s*(?:=\s*([^;]+))?;/);
|
|
874
|
+
if (!match)
|
|
875
|
+
return null;
|
|
876
|
+
const modifiers = match[1].trim().split(/\s+/).filter(m => m);
|
|
877
|
+
const type = match[2].trim();
|
|
878
|
+
const name = match[3];
|
|
879
|
+
const initializer = match[4];
|
|
880
|
+
const node = {
|
|
881
|
+
type: 'FieldDeclaration',
|
|
882
|
+
id: `${filePath}:${startIdx + 1}:0`,
|
|
883
|
+
location: this.createLocation(filePath, startIdx + 1, 0, startIdx + 1, line.length),
|
|
884
|
+
children: [],
|
|
885
|
+
metadata: {
|
|
886
|
+
name,
|
|
887
|
+
type,
|
|
888
|
+
modifiers,
|
|
889
|
+
isPublic: modifiers.includes('public'),
|
|
890
|
+
isPrivate: modifiers.includes('private'),
|
|
891
|
+
isProtected: modifiers.includes('protected'),
|
|
892
|
+
isInternal: modifiers.includes('internal'),
|
|
893
|
+
isStatic: modifiers.includes('static'),
|
|
894
|
+
isReadonly: modifiers.includes('readonly'),
|
|
895
|
+
isVolatile: modifiers.includes('volatile'),
|
|
896
|
+
isConst: modifiers.includes('const'),
|
|
897
|
+
isFixed: modifiers.includes('fixed'),
|
|
898
|
+
initializer: initializer?.trim(),
|
|
899
|
+
},
|
|
900
|
+
};
|
|
901
|
+
return { node, endIndex: startIdx };
|
|
902
|
+
}
|
|
903
|
+
parseConstructor(lines, startIdx, filePath) {
|
|
904
|
+
const line = lines[startIdx].trim();
|
|
905
|
+
const match = line.match(/^(.*?)\s*(\w+)\s*\(([^)]*)\)(?:\s*:\s*(base|this)\s*\(([^)]*)\))?\s*\{/);
|
|
906
|
+
if (!match)
|
|
907
|
+
return null;
|
|
908
|
+
const modifiers = match[1].trim().split(/\s+/).filter(m => m);
|
|
909
|
+
const name = match[2];
|
|
910
|
+
const parameters = match[3];
|
|
911
|
+
const initializerType = match[4]; // base or this
|
|
912
|
+
const initializerArgs = match[5];
|
|
913
|
+
// Find constructor body end
|
|
914
|
+
let endIdx = startIdx;
|
|
915
|
+
let braceCount = 0;
|
|
916
|
+
let foundOpenBrace = false;
|
|
917
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
918
|
+
for (const char of lines[i]) {
|
|
919
|
+
if (char === '{') {
|
|
920
|
+
braceCount++;
|
|
921
|
+
foundOpenBrace = true;
|
|
922
|
+
}
|
|
923
|
+
if (char === '}')
|
|
924
|
+
braceCount--;
|
|
925
|
+
}
|
|
926
|
+
if (foundOpenBrace && braceCount === 0) {
|
|
927
|
+
endIdx = i;
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
const node = {
|
|
932
|
+
type: 'ConstructorDeclaration',
|
|
933
|
+
id: `${filePath}:${startIdx + 1}:0`,
|
|
934
|
+
location: this.createLocation(filePath, startIdx + 1, 0, endIdx + 1, lines[endIdx]?.length || 0),
|
|
935
|
+
children: [],
|
|
936
|
+
metadata: {
|
|
937
|
+
name,
|
|
938
|
+
parameters: parameters ? parameters.split(',').map(p => p.trim()) : [],
|
|
939
|
+
modifiers,
|
|
940
|
+
isPublic: modifiers.includes('public'),
|
|
941
|
+
isPrivate: modifiers.includes('private'),
|
|
942
|
+
isProtected: modifiers.includes('protected'),
|
|
943
|
+
isInternal: modifiers.includes('internal'),
|
|
944
|
+
isStatic: modifiers.includes('static'), // Static constructor
|
|
945
|
+
initializerType,
|
|
946
|
+
initializerArgs,
|
|
947
|
+
},
|
|
948
|
+
};
|
|
949
|
+
return { node, endIndex: endIdx };
|
|
950
|
+
}
|
|
951
|
+
parseDestructor(lines, startIdx, filePath) {
|
|
952
|
+
const line = lines[startIdx].trim();
|
|
953
|
+
const match = line.match(/^(.*?)\s*~(\w+)\s*\(\s*\)\s*\{/);
|
|
954
|
+
if (!match)
|
|
955
|
+
return null;
|
|
956
|
+
const modifiers = match[1].trim().split(/\s+/).filter(m => m);
|
|
957
|
+
const name = match[2];
|
|
958
|
+
// Find destructor body end
|
|
959
|
+
let endIdx = startIdx;
|
|
960
|
+
let braceCount = 0;
|
|
961
|
+
let foundOpenBrace = false;
|
|
962
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
963
|
+
for (const char of lines[i]) {
|
|
964
|
+
if (char === '{') {
|
|
965
|
+
braceCount++;
|
|
966
|
+
foundOpenBrace = true;
|
|
967
|
+
}
|
|
968
|
+
if (char === '}')
|
|
969
|
+
braceCount--;
|
|
970
|
+
}
|
|
971
|
+
if (foundOpenBrace && braceCount === 0) {
|
|
972
|
+
endIdx = i;
|
|
973
|
+
break;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
const node = {
|
|
977
|
+
type: 'DestructorDeclaration',
|
|
978
|
+
id: `${filePath}:${startIdx + 1}:0`,
|
|
979
|
+
location: this.createLocation(filePath, startIdx + 1, 0, endIdx + 1, lines[endIdx]?.length || 0),
|
|
980
|
+
children: [],
|
|
981
|
+
metadata: {
|
|
982
|
+
name,
|
|
983
|
+
modifiers,
|
|
984
|
+
isExtern: modifiers.includes('extern'),
|
|
985
|
+
isUnsafe: modifiers.includes('unsafe'),
|
|
986
|
+
},
|
|
987
|
+
};
|
|
988
|
+
return { node, endIndex: endIdx };
|
|
989
|
+
}
|
|
990
|
+
parseEvent(lines, startIdx, filePath) {
|
|
991
|
+
const line = lines[startIdx].trim();
|
|
992
|
+
// Event field or property
|
|
993
|
+
const fieldMatch = line.match(/^(.*?)\s*event\s+([\w<>]+)\s+(\w+)\s*(?:=\s*([^;]+))?;/);
|
|
994
|
+
if (fieldMatch) {
|
|
995
|
+
const modifiers = fieldMatch[1].trim().split(/\s+/).filter(m => m);
|
|
996
|
+
const type = fieldMatch[2].trim();
|
|
997
|
+
const name = fieldMatch[3];
|
|
998
|
+
const node = {
|
|
999
|
+
type: 'EventDeclaration',
|
|
1000
|
+
id: `${filePath}:${startIdx + 1}:0`,
|
|
1001
|
+
location: this.createLocation(filePath, startIdx + 1, 0, startIdx + 1, line.length),
|
|
1002
|
+
children: [],
|
|
1003
|
+
metadata: {
|
|
1004
|
+
name,
|
|
1005
|
+
type,
|
|
1006
|
+
modifiers,
|
|
1007
|
+
isField: true,
|
|
1008
|
+
isPublic: modifiers.includes('public'),
|
|
1009
|
+
isStatic: modifiers.includes('static'),
|
|
1010
|
+
isVirtual: modifiers.includes('virtual'),
|
|
1011
|
+
isAbstract: modifiers.includes('abstract'),
|
|
1012
|
+
isOverride: modifiers.includes('override'),
|
|
1013
|
+
isSealed: modifiers.includes('sealed'),
|
|
1014
|
+
},
|
|
1015
|
+
};
|
|
1016
|
+
return { node, endIndex: startIdx };
|
|
1017
|
+
}
|
|
1018
|
+
// Event property (with add/remove)
|
|
1019
|
+
const propMatch = line.match(/^(.*?)\s*event\s+([\w<>]+)\s+(\w+)\s*\{/);
|
|
1020
|
+
if (propMatch) {
|
|
1021
|
+
const modifiers = propMatch[1].trim().split(/\s+/).filter(m => m);
|
|
1022
|
+
const type = propMatch[2].trim();
|
|
1023
|
+
const name = propMatch[3];
|
|
1024
|
+
// Find property end
|
|
1025
|
+
let endIdx = startIdx;
|
|
1026
|
+
let braceCount = 0;
|
|
1027
|
+
let foundOpenBrace = false;
|
|
1028
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
1029
|
+
for (const char of lines[i]) {
|
|
1030
|
+
if (char === '{') {
|
|
1031
|
+
braceCount++;
|
|
1032
|
+
foundOpenBrace = true;
|
|
1033
|
+
}
|
|
1034
|
+
if (char === '}')
|
|
1035
|
+
braceCount--;
|
|
1036
|
+
}
|
|
1037
|
+
if (foundOpenBrace && braceCount === 0) {
|
|
1038
|
+
endIdx = i;
|
|
1039
|
+
break;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
const body = lines.slice(startIdx, endIdx + 1).join('\n');
|
|
1043
|
+
const node = {
|
|
1044
|
+
type: 'EventDeclaration',
|
|
1045
|
+
id: `${filePath}:${startIdx + 1}:0`,
|
|
1046
|
+
location: this.createLocation(filePath, startIdx + 1, 0, endIdx + 1, lines[endIdx]?.length || 0),
|
|
1047
|
+
children: [],
|
|
1048
|
+
metadata: {
|
|
1049
|
+
name,
|
|
1050
|
+
type,
|
|
1051
|
+
modifiers,
|
|
1052
|
+
isField: false,
|
|
1053
|
+
hasAdd: body.includes('add'),
|
|
1054
|
+
hasRemove: body.includes('remove'),
|
|
1055
|
+
},
|
|
1056
|
+
};
|
|
1057
|
+
return { node, endIndex: endIdx };
|
|
1058
|
+
}
|
|
1059
|
+
return null;
|
|
1060
|
+
}
|
|
1061
|
+
parseIndexer(lines, startIdx, filePath) {
|
|
1062
|
+
const line = lines[startIdx].trim();
|
|
1063
|
+
const match = line.match(/^(.*?)\s*([\w<>\[\],\s]+)\s+this\s*\[([^\]]+)\]\s*\{/);
|
|
1064
|
+
if (!match)
|
|
1065
|
+
return null;
|
|
1066
|
+
const modifiers = match[1].trim().split(/\s+/).filter(m => m);
|
|
1067
|
+
const type = match[2].trim();
|
|
1068
|
+
const parameters = match[3];
|
|
1069
|
+
// Find indexer body end
|
|
1070
|
+
let endIdx = startIdx;
|
|
1071
|
+
let braceCount = 0;
|
|
1072
|
+
let foundOpenBrace = false;
|
|
1073
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
1074
|
+
for (const char of lines[i]) {
|
|
1075
|
+
if (char === '{') {
|
|
1076
|
+
braceCount++;
|
|
1077
|
+
foundOpenBrace = true;
|
|
1078
|
+
}
|
|
1079
|
+
if (char === '}')
|
|
1080
|
+
braceCount--;
|
|
1081
|
+
}
|
|
1082
|
+
if (foundOpenBrace && braceCount === 0) {
|
|
1083
|
+
endIdx = i;
|
|
1084
|
+
break;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
const body = lines.slice(startIdx, endIdx + 1).join('\n');
|
|
1088
|
+
const node = {
|
|
1089
|
+
type: 'IndexerDeclaration',
|
|
1090
|
+
id: `${filePath}:${startIdx + 1}:0`,
|
|
1091
|
+
location: this.createLocation(filePath, startIdx + 1, 0, endIdx + 1, lines[endIdx]?.length || 0),
|
|
1092
|
+
children: [],
|
|
1093
|
+
metadata: {
|
|
1094
|
+
type,
|
|
1095
|
+
parameters: parameters.split(',').map(p => p.trim()),
|
|
1096
|
+
modifiers,
|
|
1097
|
+
hasGetter: body.includes('get'),
|
|
1098
|
+
hasSetter: body.includes('set'),
|
|
1099
|
+
isPublic: modifiers.includes('public'),
|
|
1100
|
+
isVirtual: modifiers.includes('virtual'),
|
|
1101
|
+
isAbstract: modifiers.includes('abstract'),
|
|
1102
|
+
isOverride: modifiers.includes('override'),
|
|
1103
|
+
isSealed: modifiers.includes('sealed'),
|
|
1104
|
+
},
|
|
1105
|
+
};
|
|
1106
|
+
return { node, endIndex: endIdx };
|
|
1107
|
+
}
|
|
1108
|
+
parseOperator(lines, startIdx, filePath) {
|
|
1109
|
+
const line = lines[startIdx].trim();
|
|
1110
|
+
const match = line.match(/^(.*?)\s*([\w<>\[\],\s]+)\s+operator\s+(\S+)\s*\(([^)]*)\)\s*\{/);
|
|
1111
|
+
if (!match)
|
|
1112
|
+
return null;
|
|
1113
|
+
const modifiers = match[1].trim().split(/\s+/).filter(m => m);
|
|
1114
|
+
const returnType = match[2].trim();
|
|
1115
|
+
const operator = match[3];
|
|
1116
|
+
const parameters = match[4];
|
|
1117
|
+
// Find operator body end
|
|
1118
|
+
let endIdx = startIdx;
|
|
1119
|
+
let braceCount = 0;
|
|
1120
|
+
let foundOpenBrace = false;
|
|
1121
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
1122
|
+
for (const char of lines[i]) {
|
|
1123
|
+
if (char === '{') {
|
|
1124
|
+
braceCount++;
|
|
1125
|
+
foundOpenBrace = true;
|
|
1126
|
+
}
|
|
1127
|
+
if (char === '}')
|
|
1128
|
+
braceCount--;
|
|
1129
|
+
}
|
|
1130
|
+
if (foundOpenBrace && braceCount === 0) {
|
|
1131
|
+
endIdx = i;
|
|
1132
|
+
break;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
const node = {
|
|
1136
|
+
type: 'OperatorDeclaration',
|
|
1137
|
+
id: `${filePath}:${startIdx + 1}:0`,
|
|
1138
|
+
location: this.createLocation(filePath, startIdx + 1, 0, endIdx + 1, lines[endIdx]?.length || 0),
|
|
1139
|
+
children: [],
|
|
1140
|
+
metadata: {
|
|
1141
|
+
operator,
|
|
1142
|
+
returnType,
|
|
1143
|
+
parameters: parameters ? parameters.split(',').map(p => p.trim()) : [],
|
|
1144
|
+
modifiers,
|
|
1145
|
+
isImplicit: operator === 'implicit',
|
|
1146
|
+
isExplicit: operator === 'explicit',
|
|
1147
|
+
isConversion: operator === 'implicit' || operator === 'explicit',
|
|
1148
|
+
isPublic: modifiers.includes('public'),
|
|
1149
|
+
isStatic: modifiers.includes('static'),
|
|
1150
|
+
isExtern: modifiers.includes('extern'),
|
|
1151
|
+
},
|
|
1152
|
+
};
|
|
1153
|
+
return { node, endIndex: endIdx };
|
|
1154
|
+
}
|
|
1155
|
+
parseNestedType(lines, startIdx, filePath) {
|
|
1156
|
+
const line = lines[startIdx].trim();
|
|
1157
|
+
if (line.includes('class ')) {
|
|
1158
|
+
return this.parseClass(lines, startIdx, filePath);
|
|
1159
|
+
}
|
|
1160
|
+
else if (line.includes('struct ')) {
|
|
1161
|
+
return this.parseStruct(lines, startIdx, filePath);
|
|
1162
|
+
}
|
|
1163
|
+
else if (line.includes('interface ')) {
|
|
1164
|
+
return this.parseInterface(lines, startIdx, filePath);
|
|
1165
|
+
}
|
|
1166
|
+
else if (line.includes('enum ')) {
|
|
1167
|
+
return this.parseEnum(lines, startIdx, filePath);
|
|
1168
|
+
}
|
|
1169
|
+
else if (line.includes('record ')) {
|
|
1170
|
+
return this.parseRecord(lines, startIdx, filePath);
|
|
1171
|
+
}
|
|
1172
|
+
return null;
|
|
1173
|
+
}
|
|
1174
|
+
parseTopLevelStatements(source, filePath) {
|
|
1175
|
+
const statements = [];
|
|
1176
|
+
// C# 9.0+ allows top-level statements (no Main method required)
|
|
1177
|
+
// Check if there are any top-level statements before namespace/class declarations
|
|
1178
|
+
const lines = source.split('\n');
|
|
1179
|
+
let foundTypeDeclaration = false;
|
|
1180
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1181
|
+
const line = lines[i].trim();
|
|
1182
|
+
// Skip comments and usings
|
|
1183
|
+
if (!line || line.startsWith('//') || line.startsWith('using ') || line.startsWith('#'))
|
|
1184
|
+
continue;
|
|
1185
|
+
// Check for type declaration
|
|
1186
|
+
if (/^(?:public|internal|private|protected|class|struct|interface|enum|record)\s+/.test(line)) {
|
|
1187
|
+
foundTypeDeclaration = true;
|
|
1188
|
+
break;
|
|
1189
|
+
}
|
|
1190
|
+
// This is likely a top-level statement
|
|
1191
|
+
if (!foundTypeDeclaration && line && !line.startsWith('{') && !line.startsWith('}')) {
|
|
1192
|
+
statements.push({
|
|
1193
|
+
type: 'TopLevelStatement',
|
|
1194
|
+
id: `${filePath}:${i + 1}:0`,
|
|
1195
|
+
location: this.createLocation(filePath, i + 1, 0, i + 1, line.length),
|
|
1196
|
+
children: [],
|
|
1197
|
+
metadata: {
|
|
1198
|
+
statement: line,
|
|
1199
|
+
},
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
return statements;
|
|
1204
|
+
}
|
|
1205
|
+
parseInheritance(inheritance) {
|
|
1206
|
+
if (!inheritance)
|
|
1207
|
+
return [];
|
|
1208
|
+
const result = [];
|
|
1209
|
+
const parts = inheritance.split(',');
|
|
1210
|
+
for (const part of parts) {
|
|
1211
|
+
const trimmed = part.trim();
|
|
1212
|
+
if (trimmed) {
|
|
1213
|
+
// In C#, first type is base class (if class), rest are interfaces
|
|
1214
|
+
// Or all are interfaces (if struct/interface)
|
|
1215
|
+
result.push({
|
|
1216
|
+
type: trimmed,
|
|
1217
|
+
name: trimmed,
|
|
1218
|
+
isInterface: trimmed.startsWith('I') && /^I[A-Z]/.test(trimmed),
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
return result;
|
|
1223
|
+
}
|
|
1224
|
+
extractEnumValues(body) {
|
|
1225
|
+
const values = [];
|
|
1226
|
+
const valueRegex = /(\w+)\s*(?:=\s*([^,\n]+))?/g;
|
|
1227
|
+
let match;
|
|
1228
|
+
while ((match = valueRegex.exec(body)) !== null) {
|
|
1229
|
+
const name = match[1];
|
|
1230
|
+
if (name && !['enum', 'class', 'struct', 'public', 'private', 'protected', 'internal'].includes(name)) {
|
|
1231
|
+
const valueStr = match[2]?.trim();
|
|
1232
|
+
const value = valueStr ? parseInt(valueStr) : undefined;
|
|
1233
|
+
values.push({
|
|
1234
|
+
name,
|
|
1235
|
+
value: isNaN(value) ? undefined : value,
|
|
1236
|
+
flags: valueStr?.includes('<<') || valueStr?.includes('|'),
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
return values;
|
|
1241
|
+
}
|
|
1242
|
+
extractClassName(body) {
|
|
1243
|
+
const match = body.match(/class\s+(\w+)/);
|
|
1244
|
+
return match ? match[1] : '';
|
|
1245
|
+
}
|
|
1246
|
+
findSyntaxErrors() {
|
|
1247
|
+
const errors = [];
|
|
1248
|
+
let braceCount = 0;
|
|
1249
|
+
let parenCount = 0;
|
|
1250
|
+
let angleBracketCount = 0;
|
|
1251
|
+
for (let i = 0; i < this.lines.length; i++) {
|
|
1252
|
+
const line = this.lines[i];
|
|
1253
|
+
let inString = false;
|
|
1254
|
+
let stringChar = '';
|
|
1255
|
+
for (let j = 0; j < line.length; j++) {
|
|
1256
|
+
const char = line[j];
|
|
1257
|
+
const prevChar = j > 0 ? line[j - 1] : '';
|
|
1258
|
+
// Handle strings
|
|
1259
|
+
if ((char === '"' || char === "'" || char === '`') && prevChar !== '\\') {
|
|
1260
|
+
if (!inString) {
|
|
1261
|
+
inString = true;
|
|
1262
|
+
stringChar = char;
|
|
1263
|
+
}
|
|
1264
|
+
else if (stringChar === char) {
|
|
1265
|
+
inString = false;
|
|
1266
|
+
}
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
if (inString)
|
|
1270
|
+
continue;
|
|
1271
|
+
// Handle comments
|
|
1272
|
+
if (char === '/' && line[j + 1] === '/')
|
|
1273
|
+
break; // Single line comment
|
|
1274
|
+
if (char === '/' && line[j + 1] === '*') {
|
|
1275
|
+
// Multi-line comment start - skip until end
|
|
1276
|
+
j++;
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
// Count braces, parens, angle brackets
|
|
1280
|
+
if (char === '{')
|
|
1281
|
+
braceCount++;
|
|
1282
|
+
if (char === '}')
|
|
1283
|
+
braceCount--;
|
|
1284
|
+
if (char === '(')
|
|
1285
|
+
parenCount++;
|
|
1286
|
+
if (char === ')')
|
|
1287
|
+
parenCount--;
|
|
1288
|
+
if (char === '<' && /[\w\s]/.test(line[j + 1] || ''))
|
|
1289
|
+
angleBracketCount++;
|
|
1290
|
+
if (char === '>' && /[\w\s,.)\]]/.test(line[j + 1] || ''))
|
|
1291
|
+
angleBracketCount--;
|
|
1292
|
+
}
|
|
1293
|
+
if (braceCount < 0) {
|
|
1294
|
+
errors.push({ message: 'Unexpected }', line: i + 1, severity: 'error' });
|
|
1295
|
+
braceCount = 0;
|
|
1296
|
+
}
|
|
1297
|
+
if (parenCount < 0) {
|
|
1298
|
+
errors.push({ message: 'Unexpected )', line: i + 1, severity: 'error' });
|
|
1299
|
+
parenCount = 0;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
if (braceCount > 0) {
|
|
1303
|
+
errors.push({ message: `Unclosed braces: ${braceCount}`, line: this.lines.length, severity: 'error' });
|
|
1304
|
+
}
|
|
1305
|
+
if (parenCount > 0) {
|
|
1306
|
+
errors.push({ message: `Unclosed parentheses: ${parenCount}`, line: this.lines.length, severity: 'error' });
|
|
1307
|
+
}
|
|
1308
|
+
return errors;
|
|
1309
|
+
}
|
|
1310
|
+
findPreprocessorIssues() {
|
|
1311
|
+
const warnings = [];
|
|
1312
|
+
for (let i = 0; i < this.lines.length; i++) {
|
|
1313
|
+
const line = this.lines[i].trim();
|
|
1314
|
+
// Check for missing #nullable directive in modern C#
|
|
1315
|
+
if (i === 0 && !line.includes('#nullable')) {
|
|
1316
|
+
warnings.push({
|
|
1317
|
+
message: 'Consider adding #nullable enable for null safety',
|
|
1318
|
+
line: 1,
|
|
1319
|
+
severity: 'info',
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
// Check for region directives without endregion
|
|
1323
|
+
if (line.startsWith('#region') && !this.source.includes('#endregion')) {
|
|
1324
|
+
warnings.push({
|
|
1325
|
+
message: '#region directive without matching #endregion',
|
|
1326
|
+
line: i + 1,
|
|
1327
|
+
severity: 'warning',
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
return warnings;
|
|
1332
|
+
}
|
|
1333
|
+
extractComments() {
|
|
1334
|
+
const comments = [];
|
|
1335
|
+
for (let i = 0; i < this.lines.length; i++) {
|
|
1336
|
+
const line = this.lines[i];
|
|
1337
|
+
// Single line comments
|
|
1338
|
+
const singleIdx = line.indexOf('//');
|
|
1339
|
+
if (singleIdx !== -1) {
|
|
1340
|
+
comments.push({
|
|
1341
|
+
type: 'Line',
|
|
1342
|
+
value: line.substring(singleIdx + 2).trim(),
|
|
1343
|
+
line: i + 1,
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
// Block comments (simplified)
|
|
1348
|
+
const blockRegex = /\/\*[\s\S]*?\*\//g;
|
|
1349
|
+
let match;
|
|
1350
|
+
let sourceIndex = 0;
|
|
1351
|
+
while ((match = blockRegex.exec(this.source)) !== null) {
|
|
1352
|
+
const lineNum = this.source.substring(0, match.index).split('\n').length;
|
|
1353
|
+
comments.push({
|
|
1354
|
+
type: 'Block',
|
|
1355
|
+
value: match[0].substring(2, match[0].length - 2).trim(),
|
|
1356
|
+
line: lineNum,
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
// XML documentation comments
|
|
1360
|
+
const xmlDocRegex = /\/\/\/\s*(.+)/g;
|
|
1361
|
+
while ((match = xmlDocRegex.exec(this.source)) !== null) {
|
|
1362
|
+
const lineNum = this.source.substring(0, match.index).split('\n').length;
|
|
1363
|
+
comments.push({
|
|
1364
|
+
type: 'Documentation',
|
|
1365
|
+
value: match[1].trim(),
|
|
1366
|
+
line: lineNum,
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
return comments;
|
|
1370
|
+
}
|
|
1371
|
+
createLocation(file, startLine, startCol, endLine, endCol) {
|
|
1372
|
+
return {
|
|
1373
|
+
file,
|
|
1374
|
+
startLine,
|
|
1375
|
+
startColumn: startCol,
|
|
1376
|
+
endLine,
|
|
1377
|
+
endColumn: endCol,
|
|
1378
|
+
byteOffset: 0,
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
fallbackParse(filePath) {
|
|
1382
|
+
return {
|
|
1383
|
+
ast: this.parseCSharp(this.source, filePath),
|
|
1384
|
+
errors: this.findSyntaxErrors(),
|
|
1385
|
+
warnings: this.findPreprocessorIssues(),
|
|
1386
|
+
tokens: [],
|
|
1387
|
+
comments: this.extractComments(),
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
exports.CSharpParser = CSharpParser;
|
|
1392
|
+
class CSharpAnalyzer {
|
|
1393
|
+
analyze(ast, context) {
|
|
1394
|
+
const symbolTable = this.buildSymbolTable(ast);
|
|
1395
|
+
const asyncAnalysis = this.analyzeAsyncPatterns(ast);
|
|
1396
|
+
const linqAnalysis = this.analyzeLinqUsage(ast);
|
|
1397
|
+
const nullableAnalysis = this.analyzeNullableReferenceTypes(ast);
|
|
1398
|
+
const securityIssues = this.findSecurityIssues(ast);
|
|
1399
|
+
const dotnetAnalysis = this.analyzeDotNetPatterns(ast);
|
|
1400
|
+
return {
|
|
1401
|
+
symbols: symbolTable,
|
|
1402
|
+
callGraph: this.buildCallGraph(ast),
|
|
1403
|
+
dataFlow: { definitions: new Map(), uses: new Map(), taintedSources: [], sinks: [] },
|
|
1404
|
+
controlFlow: { nodes: [], edges: [], loops: [], branches: [] },
|
|
1405
|
+
typeInference: new Map(),
|
|
1406
|
+
metrics: this.calculateMetrics(ast),
|
|
1407
|
+
suggestions: [
|
|
1408
|
+
...asyncAnalysis.suggestions,
|
|
1409
|
+
...linqAnalysis.suggestions,
|
|
1410
|
+
...nullableAnalysis.suggestions,
|
|
1411
|
+
...dotnetAnalysis.suggestions,
|
|
1412
|
+
...securityIssues.map(i => ({
|
|
1413
|
+
type: 'security',
|
|
1414
|
+
severity: i.severity,
|
|
1415
|
+
message: i.description,
|
|
1416
|
+
remediation: i.remediation,
|
|
1417
|
+
})),
|
|
1418
|
+
],
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
buildSymbolTable(ast) {
|
|
1422
|
+
return {
|
|
1423
|
+
variables: new Map(),
|
|
1424
|
+
functions: new Map(),
|
|
1425
|
+
classes: new Map(),
|
|
1426
|
+
modules: new Map(),
|
|
1427
|
+
imports: [],
|
|
1428
|
+
exports: [],
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
analyzeAsyncPatterns(ast) {
|
|
1432
|
+
const suggestions = [];
|
|
1433
|
+
let asyncMethodCount = 0;
|
|
1434
|
+
let syncOverAsyncCount = 0;
|
|
1435
|
+
let missingAwaitCount = 0;
|
|
1436
|
+
const traverse = (node) => {
|
|
1437
|
+
// Count async methods
|
|
1438
|
+
if (node.type === 'MethodDeclaration' && node.metadata.isAsync) {
|
|
1439
|
+
asyncMethodCount++;
|
|
1440
|
+
}
|
|
1441
|
+
// Check for sync-over-async (Task.Result, Task.Wait(), .GetAwaiter().GetResult())
|
|
1442
|
+
if (node.type === 'MethodInvocation') {
|
|
1443
|
+
const method = node.metadata.method || '';
|
|
1444
|
+
if (method.includes('.Result') || method.includes('.Wait()') || method.includes('.GetAwaiter().GetResult()')) {
|
|
1445
|
+
syncOverAsyncCount++;
|
|
1446
|
+
suggestions.push({
|
|
1447
|
+
type: 'performance',
|
|
1448
|
+
severity: 'warning',
|
|
1449
|
+
message: 'Sync-over-async detected - can cause deadlocks',
|
|
1450
|
+
remediation: 'Use await instead of .Result or .Wait()',
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
// Check for missing await on async methods
|
|
1455
|
+
if (node.type === 'MethodInvocation') {
|
|
1456
|
+
const method = node.metadata.method || '';
|
|
1457
|
+
const isAsyncCall = method.includes('Async');
|
|
1458
|
+
if (isAsyncCall && !node.metadata.isAwaited) {
|
|
1459
|
+
missingAwaitCount++;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
node.children.forEach(traverse);
|
|
1463
|
+
};
|
|
1464
|
+
traverse(ast);
|
|
1465
|
+
if (asyncMethodCount > 0 && syncOverAsyncCount > 0) {
|
|
1466
|
+
suggestions.push({
|
|
1467
|
+
type: 'performance',
|
|
1468
|
+
severity: 'warning',
|
|
1469
|
+
message: `Found ${syncOverAsyncCount} sync-over-async calls in async codebase`,
|
|
1470
|
+
remediation: 'Consider using async/await throughout the call stack',
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
return { suggestions };
|
|
1474
|
+
}
|
|
1475
|
+
analyzeLinqUsage(ast) {
|
|
1476
|
+
const suggestions = [];
|
|
1477
|
+
let deferredExecutionCount = 0;
|
|
1478
|
+
let immediateExecutionCount = 0;
|
|
1479
|
+
let multipleEnumerationCount = 0;
|
|
1480
|
+
const traverse = (node) => {
|
|
1481
|
+
// Check for LINQ method calls
|
|
1482
|
+
if (node.type === 'MethodInvocation') {
|
|
1483
|
+
const method = node.metadata.method || '';
|
|
1484
|
+
// Deferred execution methods (ToList, ToArray, First, Single, etc. trigger execution)
|
|
1485
|
+
const immediateMethods = ['ToList', 'ToArray', 'First', 'FirstOrDefault', 'Single', 'SingleOrDefault', 'Last', 'LastOrDefault', 'Count', 'Any', 'All'];
|
|
1486
|
+
const deferredMethods = ['Where', 'Select', 'OrderBy', 'ThenBy', 'GroupBy', 'Join', 'Skip', 'Take', 'Distinct'];
|
|
1487
|
+
if (immediateMethods.some(m => method.includes(m))) {
|
|
1488
|
+
immediateExecutionCount++;
|
|
1489
|
+
}
|
|
1490
|
+
if (deferredMethods.some(m => method.includes(m))) {
|
|
1491
|
+
deferredExecutionCount++;
|
|
1492
|
+
}
|
|
1493
|
+
// Multiple enumeration pattern
|
|
1494
|
+
if (method.includes('Count()') || method.includes('Any()')) {
|
|
1495
|
+
// Check if same enumerable is used again later
|
|
1496
|
+
const parent = node.parent;
|
|
1497
|
+
if (parent && parent.children) {
|
|
1498
|
+
const sameVarUsages = parent.children.filter(c => c.metadata?.variable === node.metadata.variable);
|
|
1499
|
+
if (sameVarUsages.length > 1) {
|
|
1500
|
+
multipleEnumerationCount++;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
node.children.forEach(traverse);
|
|
1506
|
+
};
|
|
1507
|
+
traverse(ast);
|
|
1508
|
+
if (deferredExecutionCount > 0 && immediateExecutionCount === 0) {
|
|
1509
|
+
suggestions.push({
|
|
1510
|
+
type: 'performance',
|
|
1511
|
+
severity: 'info',
|
|
1512
|
+
message: 'LINQ query without terminal operator - deferred execution may cause multiple evaluations',
|
|
1513
|
+
remediation: 'Add .ToList() or .ToArray() if the result is enumerated multiple times',
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
return { suggestions };
|
|
1517
|
+
}
|
|
1518
|
+
analyzeNullableReferenceTypes(ast) {
|
|
1519
|
+
const suggestions = [];
|
|
1520
|
+
let nullableEnabled = false;
|
|
1521
|
+
let nullForgivingCount = 0;
|
|
1522
|
+
let potentialNullDereferenceCount = 0;
|
|
1523
|
+
const traverse = (node) => {
|
|
1524
|
+
// Check for #nullable directive
|
|
1525
|
+
if (node.type === 'PreprocessorDirective' && node.metadata.directive === 'nullable') {
|
|
1526
|
+
nullableEnabled = node.metadata.value?.includes('enable');
|
|
1527
|
+
}
|
|
1528
|
+
// Check for null-forgiving operator (!)
|
|
1529
|
+
if (node.type === 'MemberAccess' && node.metadata.hasNullForgiving) {
|
|
1530
|
+
nullForgivingCount++;
|
|
1531
|
+
}
|
|
1532
|
+
// Check for potential null dereference
|
|
1533
|
+
if (node.type === 'MemberAccess') {
|
|
1534
|
+
const variable = node.metadata.variable || '';
|
|
1535
|
+
const isNullable = node.metadata.isNullable;
|
|
1536
|
+
const hasNullCheck = node.metadata.hasNullCheck;
|
|
1537
|
+
if (isNullable && !hasNullCheck && !node.metadata.hasNullForgiving) {
|
|
1538
|
+
potentialNullDereferenceCount++;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
node.children.forEach(traverse);
|
|
1542
|
+
};
|
|
1543
|
+
traverse(ast);
|
|
1544
|
+
if (!nullableEnabled) {
|
|
1545
|
+
suggestions.push({
|
|
1546
|
+
type: 'modernization',
|
|
1547
|
+
severity: 'info',
|
|
1548
|
+
message: 'Nullable reference types not enabled',
|
|
1549
|
+
remediation: 'Add #nullable enable to enable null safety analysis',
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
if (nullForgivingCount > 5) {
|
|
1553
|
+
suggestions.push({
|
|
1554
|
+
type: 'code-quality',
|
|
1555
|
+
severity: 'warning',
|
|
1556
|
+
message: `Excessive use of null-forgiving operator (!) - ${nullForgivingCount} instances`,
|
|
1557
|
+
remediation: 'Review null safety assumptions and add proper null checks',
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
return { suggestions };
|
|
1561
|
+
}
|
|
1562
|
+
analyzeDotNetPatterns(ast) {
|
|
1563
|
+
const suggestions = [];
|
|
1564
|
+
let disposableNotDisposed = 0;
|
|
1565
|
+
let stringConcatInLoop = 0;
|
|
1566
|
+
let boxingCount = 0;
|
|
1567
|
+
const traverse = (node) => {
|
|
1568
|
+
// IDisposable not disposed
|
|
1569
|
+
if (node.type === 'VariableDeclaration' && node.metadata.type) {
|
|
1570
|
+
const type = node.metadata.type;
|
|
1571
|
+
const disposableTypes = ['Stream', 'HttpClient', 'SqlConnection', 'DbContext', 'FileStream', 'MemoryStream'];
|
|
1572
|
+
if (disposableTypes.some(t => type.includes(t))) {
|
|
1573
|
+
const hasUsing = node.metadata.hasUsingDeclaration || node.metadata.isInUsingBlock;
|
|
1574
|
+
if (!hasUsing) {
|
|
1575
|
+
disposableNotDisposed++;
|
|
1576
|
+
suggestions.push({
|
|
1577
|
+
type: 'resource-leak',
|
|
1578
|
+
severity: 'warning',
|
|
1579
|
+
message: `IDisposable type '${type}' may not be disposed properly`,
|
|
1580
|
+
remediation: 'Use using declaration or try-finally with Dispose()',
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
// String concatenation in loop
|
|
1586
|
+
if (node.type === 'ForStatement' || node.type === 'WhileStatement' || node.type === 'ForEachStatement') {
|
|
1587
|
+
const body = node.children.find(c => c.type === 'Block');
|
|
1588
|
+
if (body) {
|
|
1589
|
+
const hasStringConcat = body.children.some(c => c.type === 'BinaryExpression' &&
|
|
1590
|
+
(c.metadata.operator === '+' || c.metadata.operator === '+=') &&
|
|
1591
|
+
(c.metadata.leftType === 'string' || c.metadata.rightType === 'string'));
|
|
1592
|
+
if (hasStringConcat) {
|
|
1593
|
+
stringConcatInLoop++;
|
|
1594
|
+
suggestions.push({
|
|
1595
|
+
type: 'performance',
|
|
1596
|
+
severity: 'warning',
|
|
1597
|
+
message: 'String concatenation in loop detected',
|
|
1598
|
+
remediation: 'Use StringBuilder for efficient string concatenation in loops',
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
// Boxing/unboxing
|
|
1604
|
+
if (node.type === 'CastExpression') {
|
|
1605
|
+
const fromType = node.metadata.fromType;
|
|
1606
|
+
const toType = node.metadata.toType;
|
|
1607
|
+
if ((fromType === 'int' || fromType === 'double' || fromType === 'bool') && toType === 'object') {
|
|
1608
|
+
boxingCount++;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
node.children.forEach(traverse);
|
|
1612
|
+
};
|
|
1613
|
+
traverse(ast);
|
|
1614
|
+
if (boxingCount > 5) {
|
|
1615
|
+
suggestions.push({
|
|
1616
|
+
type: 'performance',
|
|
1617
|
+
severity: 'info',
|
|
1618
|
+
message: `Potential boxing operations detected: ${boxingCount}`,
|
|
1619
|
+
remediation: 'Use generics to avoid boxing, or ensure type safety at compile time',
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
return { suggestions };
|
|
1623
|
+
}
|
|
1624
|
+
findSecurityIssues(ast) {
|
|
1625
|
+
const issues = [];
|
|
1626
|
+
const traverse = (node) => {
|
|
1627
|
+
// SQL Injection
|
|
1628
|
+
if (node.type === 'MethodInvocation') {
|
|
1629
|
+
const method = node.metadata.method || '';
|
|
1630
|
+
if (method.includes('ExecuteSqlCommand') || method.includes('FromSqlRaw') || method.includes('FromSqlInterpolated')) {
|
|
1631
|
+
const hasInterpolation = node.metadata.hasStringInterpolation;
|
|
1632
|
+
const isRaw = method.includes('Raw') || !method.includes('Interpolated');
|
|
1633
|
+
if (isRaw && hasInterpolation) {
|
|
1634
|
+
issues.push({
|
|
1635
|
+
id: 'CS001',
|
|
1636
|
+
severity: 'critical',
|
|
1637
|
+
category: 'sql-injection',
|
|
1638
|
+
location: node.location,
|
|
1639
|
+
description: 'Potential SQL injection via string interpolation',
|
|
1640
|
+
remediation: 'Use parameterized queries or FromSqlInterpolated instead',
|
|
1641
|
+
falsePositiveLikelihood: 0.2,
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
// Path traversal
|
|
1646
|
+
if (method.includes('File.ReadAllText') || method.includes('File.Open') || method.includes('FileStream')) {
|
|
1647
|
+
const hasUserInput = node.metadata.hasUserInput;
|
|
1648
|
+
if (hasUserInput && !node.metadata.hasPathValidation) {
|
|
1649
|
+
issues.push({
|
|
1650
|
+
id: 'CS002',
|
|
1651
|
+
severity: 'high',
|
|
1652
|
+
category: 'path-traversal',
|
|
1653
|
+
location: node.location,
|
|
1654
|
+
description: 'Potential path traversal vulnerability',
|
|
1655
|
+
remediation: 'Validate file paths using Path.GetFullPath and check for directory traversal',
|
|
1656
|
+
falsePositiveLikelihood: 0.3,
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
// Deserialization
|
|
1661
|
+
if (method.includes('JsonSerializer.Deserialize') || method.includes('BinaryFormatter.Deserialize')) {
|
|
1662
|
+
if (method.includes('BinaryFormatter')) {
|
|
1663
|
+
issues.push({
|
|
1664
|
+
id: 'CS003',
|
|
1665
|
+
severity: 'critical',
|
|
1666
|
+
category: 'deserialization',
|
|
1667
|
+
location: node.location,
|
|
1668
|
+
description: 'BinaryFormatter is obsolete and insecure',
|
|
1669
|
+
remediation: 'Use JsonSerializer with TypeInfoHandling.None or DataContractSerializer',
|
|
1670
|
+
falsePositiveLikelihood: 0.1,
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
const hasTypeNameHandling = node.metadata.hasTypeNameHandling;
|
|
1674
|
+
if (hasTypeNameHandling) {
|
|
1675
|
+
issues.push({
|
|
1676
|
+
id: 'CS004',
|
|
1677
|
+
severity: 'critical',
|
|
1678
|
+
category: 'deserialization',
|
|
1679
|
+
location: node.location,
|
|
1680
|
+
description: 'TypeNameHandling can lead to remote code execution',
|
|
1681
|
+
remediation: 'Disable TypeNameHandling or use a custom SerializationBinder',
|
|
1682
|
+
falsePositiveLikelihood: 0.1,
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
// Insecure random
|
|
1687
|
+
if (method.includes('Random.') || method.includes('new Random()')) {
|
|
1688
|
+
if (node.metadata.isSecuritySensitive) {
|
|
1689
|
+
issues.push({
|
|
1690
|
+
id: 'CS005',
|
|
1691
|
+
severity: 'medium',
|
|
1692
|
+
category: 'weak-cryptography',
|
|
1693
|
+
location: node.location,
|
|
1694
|
+
description: 'System.Random is not cryptographically secure',
|
|
1695
|
+
remediation: 'Use RandomNumberGenerator for security-sensitive operations',
|
|
1696
|
+
falsePositiveLikelihood: 0.5,
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
// Debug.Assert in production
|
|
1701
|
+
if (method.includes('Debug.Assert')) {
|
|
1702
|
+
issues.push({
|
|
1703
|
+
id: 'CS006',
|
|
1704
|
+
severity: 'low',
|
|
1705
|
+
category: 'debug-code',
|
|
1706
|
+
location: node.location,
|
|
1707
|
+
description: 'Debug.Assert is removed in release builds',
|
|
1708
|
+
remediation: 'Use proper validation with exceptions for production code',
|
|
1709
|
+
falsePositiveLikelihood: 0.7,
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
// Weak hashing
|
|
1714
|
+
if (node.type === 'MethodInvocation') {
|
|
1715
|
+
const method = node.metadata.method || '';
|
|
1716
|
+
if (method.includes('MD5') || method.includes('SHA1')) {
|
|
1717
|
+
issues.push({
|
|
1718
|
+
id: 'CS007',
|
|
1719
|
+
severity: 'high',
|
|
1720
|
+
category: 'weak-cryptography',
|
|
1721
|
+
location: node.location,
|
|
1722
|
+
description: 'Weak hashing algorithm detected',
|
|
1723
|
+
remediation: 'Use SHA256 or SHA512 for hashing',
|
|
1724
|
+
falsePositiveLikelihood: 0.3,
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
// Hardcoded secrets
|
|
1729
|
+
if (node.type === 'VariableDeclaration' && node.metadata.initializer) {
|
|
1730
|
+
const init = node.metadata.initializer;
|
|
1731
|
+
const name = node.metadata.name || '';
|
|
1732
|
+
if (/password|secret|key|token|connectionstring/i.test(name)) {
|
|
1733
|
+
if (init.includes('"') && init.length > 10) {
|
|
1734
|
+
issues.push({
|
|
1735
|
+
id: 'CS008',
|
|
1736
|
+
severity: 'critical',
|
|
1737
|
+
category: 'secrets',
|
|
1738
|
+
location: node.location,
|
|
1739
|
+
description: 'Potential hardcoded secret detected',
|
|
1740
|
+
remediation: 'Use configuration files or secret management (Azure Key Vault, AWS Secrets Manager)',
|
|
1741
|
+
falsePositiveLikelihood: 0.4,
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
node.children.forEach(traverse);
|
|
1747
|
+
};
|
|
1748
|
+
traverse(ast);
|
|
1749
|
+
return issues;
|
|
1750
|
+
}
|
|
1751
|
+
buildCallGraph(ast) {
|
|
1752
|
+
return { nodes: [], edges: [], entryPoints: [], deadCode: [] };
|
|
1753
|
+
}
|
|
1754
|
+
calculateMetrics(ast) {
|
|
1755
|
+
return {
|
|
1756
|
+
linesOfCode: 0,
|
|
1757
|
+
logicalLines: 0,
|
|
1758
|
+
commentLines: 0,
|
|
1759
|
+
blankLines: 0,
|
|
1760
|
+
cyclomaticComplexity: 0,
|
|
1761
|
+
cognitiveComplexity: 0,
|
|
1762
|
+
halsteadMetrics: { operators: 0, operands: 0, uniqueOperators: 0, uniqueOperands: 0, volume: 0, difficulty: 0, effort: 0, timeToProgram: 0, bugsDelivered: 0 },
|
|
1763
|
+
maintainabilityIndex: 0,
|
|
1764
|
+
duplicateRate: 0,
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
exports.CSharpAnalyzer = CSharpAnalyzer;
|
|
1769
|
+
exports.CSharpLanguageSupport = {
|
|
1770
|
+
id: 'csharp',
|
|
1771
|
+
name: 'C#',
|
|
1772
|
+
extensions: ['.cs', '.csx', '.cake'],
|
|
1773
|
+
parser: new CSharpParser(),
|
|
1774
|
+
analyzer: new CSharpAnalyzer(),
|
|
1775
|
+
};
|
|
1776
|
+
//# sourceMappingURL=csharp.js.map
|