neuronlayer 0.1.6 → 0.1.8
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.
Potentially problematic release.
This version of neuronlayer might be problematic. Click here for more details.
- package/README.md +173 -120
- package/dist/index.js +724 -134
- package/package.json +2 -2
- package/src/cli/commands.ts +12 -6
- package/src/core/engine.ts +53 -3
- package/src/core/ghost-mode.ts +53 -0
- package/src/core/project-manager.ts +5 -1
- package/src/indexing/ast.ts +356 -51
- package/src/indexing/indexer.ts +25 -0
- package/src/server/mcp.ts +1 -1
- package/src/server/tools.ts +129 -0
- package/src/storage/tier2.ts +212 -4
- package/real-benchmark.mjs +0 -322
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neuronlayer",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.8",
|
|
4
|
+
"description": "Code intelligence layer disguised as memory - MCP server giving AI persistent understanding of your codebase",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
package/src/cli/commands.ts
CHANGED
|
@@ -159,13 +159,19 @@ export function exportDecisions(
|
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
// Open database and get decisions
|
|
163
|
-
|
|
162
|
+
// Open database and get decisions (check both new and old names)
|
|
163
|
+
let dbPath = join(projectInfo.dataDir, 'neuronlayer.db');
|
|
164
164
|
if (!existsSync(dbPath)) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
// Fall back to old name for backwards compatibility
|
|
166
|
+
const oldDbPath = join(projectInfo.dataDir, 'memorylayer.db');
|
|
167
|
+
if (existsSync(oldDbPath)) {
|
|
168
|
+
dbPath = oldDbPath;
|
|
169
|
+
} else {
|
|
170
|
+
return {
|
|
171
|
+
success: false,
|
|
172
|
+
message: `Project database not found. Has the project been indexed?`
|
|
173
|
+
};
|
|
174
|
+
}
|
|
169
175
|
}
|
|
170
176
|
|
|
171
177
|
const db = initializeDatabase(dbPath);
|
package/src/core/engine.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join, basename } from 'path';
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, statSync } from 'fs';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, statSync, renameSync } from 'fs';
|
|
3
3
|
import { initializeDatabase, closeDatabase } from '../storage/database.js';
|
|
4
4
|
import { Tier1Storage } from '../storage/tier1.js';
|
|
5
5
|
import { Tier2Storage } from '../storage/tier2.js';
|
|
@@ -68,8 +68,22 @@ export class MemoryLayerEngine {
|
|
|
68
68
|
mkdirSync(config.dataDir, { recursive: true });
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
// Initialize database
|
|
72
|
-
|
|
71
|
+
// Initialize database (with migration from old name)
|
|
72
|
+
let dbPath = join(config.dataDir, 'neuronlayer.db');
|
|
73
|
+
const oldDbPath = join(config.dataDir, 'memorylayer.db');
|
|
74
|
+
|
|
75
|
+
// Migrate from old database name if it exists
|
|
76
|
+
if (!existsSync(dbPath) && existsSync(oldDbPath)) {
|
|
77
|
+
try {
|
|
78
|
+
console.error('Migrating database from memorylayer.db to neuronlayer.db...');
|
|
79
|
+
renameSync(oldDbPath, dbPath);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
// If rename fails (file locked), use the old database path
|
|
82
|
+
console.error('Migration skipped (file in use), using existing database');
|
|
83
|
+
dbPath = oldDbPath;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
73
87
|
this.db = initializeDatabase(dbPath);
|
|
74
88
|
|
|
75
89
|
// Initialize storage tiers
|
|
@@ -214,6 +228,16 @@ export class MemoryLayerEngine {
|
|
|
214
228
|
this.updateProjectStats();
|
|
215
229
|
// Extract decisions from git and comments
|
|
216
230
|
this.extractDecisions().catch(err => console.error('Decision extraction error:', err));
|
|
231
|
+
|
|
232
|
+
// Index tests after code indexing is complete
|
|
233
|
+
try {
|
|
234
|
+
const testResult = this.testAwareness.refreshIndex();
|
|
235
|
+
if (testResult.testsIndexed > 0) {
|
|
236
|
+
console.error(`Test index: ${testResult.testsIndexed} tests (${testResult.framework})`);
|
|
237
|
+
}
|
|
238
|
+
} catch (err) {
|
|
239
|
+
console.error('Test indexing error:', err);
|
|
240
|
+
}
|
|
217
241
|
});
|
|
218
242
|
|
|
219
243
|
this.indexer.on('fileIndexed', (path) => {
|
|
@@ -224,6 +248,22 @@ export class MemoryLayerEngine {
|
|
|
224
248
|
this.summarizer.invalidateSummaryByPath(path);
|
|
225
249
|
});
|
|
226
250
|
|
|
251
|
+
this.indexer.on('fileImpact', (impact: { file: string; affectedFiles: string[]; affectedCount: number }) => {
|
|
252
|
+
// Log impact warning for file changes
|
|
253
|
+
if (impact.affectedCount > 0) {
|
|
254
|
+
console.error(`[Impact] ${impact.file} changed → ${impact.affectedCount} file(s) may be affected`);
|
|
255
|
+
if (impact.affectedCount <= 5) {
|
|
256
|
+
impact.affectedFiles.forEach(f => console.error(` → ${f}`));
|
|
257
|
+
} else {
|
|
258
|
+
impact.affectedFiles.slice(0, 3).forEach(f => console.error(` → ${f}`));
|
|
259
|
+
console.error(` ... and ${impact.affectedCount - 3} more`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Track impact in ghost mode for proactive warnings
|
|
263
|
+
this.ghostMode.onFileImpact(impact.file, impact.affectedFiles);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
227
267
|
this.indexer.on('error', (error) => {
|
|
228
268
|
console.error('Indexer error:', error);
|
|
229
269
|
});
|
|
@@ -727,6 +767,16 @@ export class MemoryLayerEngine {
|
|
|
727
767
|
return { imports, importedBy, symbols };
|
|
728
768
|
}
|
|
729
769
|
|
|
770
|
+
// Find circular dependencies in the project
|
|
771
|
+
findCircularDependencies(): Array<string[]> {
|
|
772
|
+
return this.tier2.findCircularDependencies();
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Get transitive dependents (all files affected by changing a file)
|
|
776
|
+
getTransitiveDependents(filePath: string, maxDepth: number = 3): Array<{ file: string; depth: number; imports: string[] }> {
|
|
777
|
+
return this.tier2.getTransitiveDependents(filePath, maxDepth);
|
|
778
|
+
}
|
|
779
|
+
|
|
730
780
|
// Phase 2: Get symbol count
|
|
731
781
|
getSymbolCount(): number {
|
|
732
782
|
return this.tier2.getSymbolCount();
|
package/src/core/ghost-mode.ts
CHANGED
|
@@ -64,9 +64,16 @@ const TECH_PATTERNS = [
|
|
|
64
64
|
{ pattern: /\b(sass|scss|less)\b/i, category: 'styling', term: 'CSS preprocessor' },
|
|
65
65
|
];
|
|
66
66
|
|
|
67
|
+
export interface FileImpact {
|
|
68
|
+
changedFile: string;
|
|
69
|
+
affectedFiles: string[];
|
|
70
|
+
timestamp: Date;
|
|
71
|
+
}
|
|
72
|
+
|
|
67
73
|
export class GhostMode {
|
|
68
74
|
private activeFiles: Map<string, FileContext> = new Map();
|
|
69
75
|
private recentDecisions: Map<string, Decision[]> = new Map();
|
|
76
|
+
private recentImpacts: Map<string, FileImpact> = new Map(); // Track file impacts
|
|
70
77
|
private tier2: Tier2Storage;
|
|
71
78
|
private embeddingGenerator: EmbeddingGenerator;
|
|
72
79
|
|
|
@@ -74,12 +81,58 @@ export class GhostMode {
|
|
|
74
81
|
private readonly MAX_ACTIVE_FILES = 20;
|
|
75
82
|
private readonly FILE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
76
83
|
private readonly DECISION_CACHE_SIZE = 50;
|
|
84
|
+
private readonly IMPACT_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
77
85
|
|
|
78
86
|
constructor(tier2: Tier2Storage, embeddingGenerator: EmbeddingGenerator) {
|
|
79
87
|
this.tier2 = tier2;
|
|
80
88
|
this.embeddingGenerator = embeddingGenerator;
|
|
81
89
|
}
|
|
82
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Called when a file change impacts other files
|
|
93
|
+
*/
|
|
94
|
+
onFileImpact(changedFile: string, affectedFiles: string[]): void {
|
|
95
|
+
const impact: FileImpact = {
|
|
96
|
+
changedFile,
|
|
97
|
+
affectedFiles,
|
|
98
|
+
timestamp: new Date()
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Store impact for each affected file
|
|
102
|
+
for (const file of affectedFiles) {
|
|
103
|
+
this.recentImpacts.set(file, impact);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Evict old impacts
|
|
107
|
+
this.evictStaleImpacts();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check if a file was recently impacted by changes to another file
|
|
112
|
+
*/
|
|
113
|
+
getImpactWarning(filePath: string): FileImpact | null {
|
|
114
|
+
const impact = this.recentImpacts.get(filePath);
|
|
115
|
+
if (!impact) return null;
|
|
116
|
+
|
|
117
|
+
// Check if still within TTL
|
|
118
|
+
const age = Date.now() - impact.timestamp.getTime();
|
|
119
|
+
if (age > this.IMPACT_TTL_MS) {
|
|
120
|
+
this.recentImpacts.delete(filePath);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return impact;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private evictStaleImpacts(): void {
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
for (const [file, impact] of this.recentImpacts) {
|
|
130
|
+
if (now - impact.timestamp.getTime() > this.IMPACT_TTL_MS) {
|
|
131
|
+
this.recentImpacts.delete(file);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
83
136
|
/**
|
|
84
137
|
* Called when any file is read - silently track and pre-fetch decisions
|
|
85
138
|
*/
|
|
@@ -269,7 +269,11 @@ export class ProjectManager {
|
|
|
269
269
|
const result: Array<{ project: ProjectInfo; db: Database.Database }> = [];
|
|
270
270
|
|
|
271
271
|
for (const project of this.listProjects()) {
|
|
272
|
-
|
|
272
|
+
// Check both new and old database names
|
|
273
|
+
let dbPath = join(project.dataDir, 'neuronlayer.db');
|
|
274
|
+
if (!existsSync(dbPath)) {
|
|
275
|
+
dbPath = join(project.dataDir, 'memorylayer.db');
|
|
276
|
+
}
|
|
273
277
|
|
|
274
278
|
if (existsSync(dbPath)) {
|
|
275
279
|
try {
|
package/src/indexing/ast.ts
CHANGED
|
@@ -80,6 +80,30 @@ const LANGUAGE_CONFIGS: Record<string, LanguageConfig> = {
|
|
|
80
80
|
(import_from_statement) @import
|
|
81
81
|
`
|
|
82
82
|
}
|
|
83
|
+
},
|
|
84
|
+
go: {
|
|
85
|
+
wasmFile: 'tree-sitter-go.wasm',
|
|
86
|
+
extensions: ['.go'],
|
|
87
|
+
queries: {
|
|
88
|
+
functions: `(function_declaration name: (identifier) @name) @func`,
|
|
89
|
+
classes: `(type_declaration (type_spec name: (type_identifier) @name type: (struct_type))) @class`
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
rust: {
|
|
93
|
+
wasmFile: 'tree-sitter-rust.wasm',
|
|
94
|
+
extensions: ['.rs'],
|
|
95
|
+
queries: {
|
|
96
|
+
functions: `(function_item name: (identifier) @name) @func`,
|
|
97
|
+
classes: `(struct_item name: (type_identifier) @name) @class`
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
java: {
|
|
101
|
+
wasmFile: 'tree-sitter-java.wasm',
|
|
102
|
+
extensions: ['.java'],
|
|
103
|
+
queries: {
|
|
104
|
+
functions: `(method_declaration name: (identifier) @name) @func`,
|
|
105
|
+
classes: `(class_declaration name: (identifier) @name) @class`
|
|
106
|
+
}
|
|
83
107
|
}
|
|
84
108
|
};
|
|
85
109
|
|
|
@@ -95,41 +119,15 @@ export class ASTParser {
|
|
|
95
119
|
|
|
96
120
|
async initialize(): Promise<void> {
|
|
97
121
|
if (this.initialized) return;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
this.parser = new Parser();
|
|
102
|
-
this.initialized = true;
|
|
103
|
-
console.error('AST Parser initialized');
|
|
104
|
-
} catch (error) {
|
|
105
|
-
console.error('Failed to initialize AST parser:', error);
|
|
106
|
-
throw error;
|
|
107
|
-
}
|
|
122
|
+
// Using regex-based parsing for reliable cross-platform support
|
|
123
|
+
// Tree-sitter WASM support reserved for future enhancement
|
|
124
|
+
this.initialized = true;
|
|
108
125
|
}
|
|
109
126
|
|
|
110
|
-
private async loadLanguage(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const config = LANGUAGE_CONFIGS[langName];
|
|
116
|
-
if (!config) {
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
// Try to load from node_modules or bundled location
|
|
122
|
-
const wasmDir = join(this.dataDir, 'wasm');
|
|
123
|
-
const wasmPath = join(wasmDir, config.wasmFile);
|
|
124
|
-
|
|
125
|
-
// For now, we'll use a simplified approach without external WASM files
|
|
126
|
-
// In production, you'd download these from tree-sitter releases
|
|
127
|
-
console.error(`Language ${langName} WASM not available yet`);
|
|
128
|
-
return null;
|
|
129
|
-
} catch (error) {
|
|
130
|
-
console.error(`Failed to load language ${langName}:`, error);
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
127
|
+
private async loadLanguage(_langName: string): Promise<Parser.Language | null> {
|
|
128
|
+
// Tree-sitter WASM loading not implemented - using regex fallback
|
|
129
|
+
// Language configs are retained for future tree-sitter support
|
|
130
|
+
return null;
|
|
133
131
|
}
|
|
134
132
|
|
|
135
133
|
getLanguageForFile(filePath: string): string | null {
|
|
@@ -152,11 +150,13 @@ export class ASTParser {
|
|
|
152
150
|
await this.initialize();
|
|
153
151
|
}
|
|
154
152
|
|
|
155
|
-
//
|
|
153
|
+
// Regex-based parsing - reliable cross-platform symbol extraction
|
|
156
154
|
return this.parseWithRegex(filePath, content);
|
|
157
155
|
}
|
|
158
156
|
|
|
159
|
-
// Regex-based parsing
|
|
157
|
+
// Regex-based parsing for symbol extraction
|
|
158
|
+
// Handles: functions, classes, interfaces, types, imports, exports
|
|
159
|
+
// Supports: TypeScript, JavaScript, Python, Go, Rust, Java
|
|
160
160
|
private parseWithRegex(filePath: string, content: string): {
|
|
161
161
|
symbols: CodeSymbol[];
|
|
162
162
|
imports: Import[];
|
|
@@ -172,6 +172,12 @@ export class ASTParser {
|
|
|
172
172
|
this.parseTypeScriptJS(filePath, content, lines, symbols, imports, exports);
|
|
173
173
|
} else if (lang === 'python') {
|
|
174
174
|
this.parsePython(filePath, content, lines, symbols, imports, exports);
|
|
175
|
+
} else if (lang === 'go') {
|
|
176
|
+
this.parseGo(filePath, content, lines, symbols, imports, exports);
|
|
177
|
+
} else if (lang === 'rust') {
|
|
178
|
+
this.parseRust(filePath, content, lines, symbols, imports, exports);
|
|
179
|
+
} else if (lang === 'java') {
|
|
180
|
+
this.parseJava(filePath, content, lines, symbols, imports, exports);
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
return { symbols, imports, exports };
|
|
@@ -187,37 +193,60 @@ export class ASTParser {
|
|
|
187
193
|
): void {
|
|
188
194
|
// Patterns for TypeScript/JavaScript
|
|
189
195
|
const patterns = {
|
|
190
|
-
// Functions: function name(),
|
|
191
|
-
function: /^(?:export\s+)?(?:async\s+)?function\s+(\w+)/,
|
|
192
|
-
|
|
196
|
+
// Functions: function name(), export default function name(), const name = () =>
|
|
197
|
+
function: /^(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+(\w+)/,
|
|
198
|
+
// Arrow functions: handles type annotations and destructured params
|
|
199
|
+
arrowFunc: /^(?:export\s+)?(?:const|let|var)\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*(?::\s*[^=]+)?\s*=>/,
|
|
193
200
|
// Classes
|
|
194
|
-
class: /^(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/,
|
|
201
|
+
class: /^(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+(\w+)/,
|
|
195
202
|
// Interfaces (TS only)
|
|
196
203
|
interface: /^(?:export\s+)?interface\s+(\w+)/,
|
|
197
204
|
// Types (TS only)
|
|
198
|
-
type: /^(?:export\s+)?type\s+(\w+)\s*=/,
|
|
199
|
-
// Imports
|
|
200
|
-
import: /^import\s+(?:(\w+)(?:\s*,\s*)?)?(?:\{([^}]+)\})?\s*from\s*['"]([^'"]+)['"]/,
|
|
201
|
-
importAll: /^import\s
|
|
205
|
+
type: /^(?:export\s+)?type\s+(\w+)\s*(?:<[^>]*>)?\s*=/,
|
|
206
|
+
// Imports - supports 'import type'
|
|
207
|
+
import: /^import\s+(?:type\s+)?(?:(\w+)(?:\s*,\s*)?)?(?:\{([^}]+)\})?\s*from\s*['"]([^'"]+)['"]/,
|
|
208
|
+
importAll: /^import\s+(?:type\s+)?\*\s+as\s+(\w+)\s+from\s*['"]([^'"]+)['"]/,
|
|
202
209
|
importSideEffect: /^import\s*['"]([^'"]+)['"]/,
|
|
203
210
|
// Exports
|
|
204
|
-
exportNamed: /^export\s
|
|
211
|
+
exportNamed: /^export\s+(?:type\s+)?\{([^}]+)\}/,
|
|
205
212
|
exportDefault: /^export\s+default\s+(?:class|function|const|let|var)?\s*(\w+)?/,
|
|
206
213
|
exportDirect: /^export\s+(?:const|let|var|function|class|interface|type|enum|async\s+function)\s+(\w+)/,
|
|
207
214
|
// Enums (TS)
|
|
208
215
|
enum: /^(?:export\s+)?(?:const\s+)?enum\s+(\w+)/,
|
|
209
|
-
// Methods inside classes
|
|
210
|
-
method: /^\s+(?:async\s+)?(?:static\s+)?(?:private\s+|public\s+|protected\s+)?(\w+)\s
|
|
216
|
+
// Methods inside classes - handles generics, readonly, and all modifiers
|
|
217
|
+
method: /^\s+(?:async\s+)?(?:static\s+)?(?:readonly\s+)?(?:private\s+|public\s+|protected\s+)?(?:get\s+|set\s+)?(\w+)\s*(?:<[^>]+>)?\s*\(/,
|
|
211
218
|
};
|
|
212
219
|
|
|
213
220
|
let currentClass: { name: string; startLine: number } | null = null;
|
|
214
221
|
let braceDepth = 0;
|
|
222
|
+
let inBlockComment = false;
|
|
215
223
|
|
|
216
224
|
for (let i = 0; i < lines.length; i++) {
|
|
217
225
|
const line = lines[i] || '';
|
|
218
226
|
const trimmed = line.trim();
|
|
219
227
|
const lineNum = i + 1;
|
|
220
228
|
|
|
229
|
+
// Track block comments properly
|
|
230
|
+
if (inBlockComment) {
|
|
231
|
+
if (trimmed.includes('*/')) {
|
|
232
|
+
inBlockComment = false;
|
|
233
|
+
}
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Skip single-line comments
|
|
238
|
+
if (trimmed.startsWith('//')) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Handle block comment start
|
|
243
|
+
if (trimmed.startsWith('/*')) {
|
|
244
|
+
if (!trimmed.includes('*/')) {
|
|
245
|
+
inBlockComment = true;
|
|
246
|
+
}
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
221
250
|
// Track brace depth for class scope
|
|
222
251
|
braceDepth += (line.match(/\{/g) || []).length;
|
|
223
252
|
braceDepth -= (line.match(/\}/g) || []).length;
|
|
@@ -231,11 +260,6 @@ export class ASTParser {
|
|
|
231
260
|
currentClass = null;
|
|
232
261
|
}
|
|
233
262
|
|
|
234
|
-
// Skip comments
|
|
235
|
-
if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) {
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
263
|
// Functions
|
|
240
264
|
let match = trimmed.match(patterns.function);
|
|
241
265
|
if (match && match[1]) {
|
|
@@ -511,6 +535,287 @@ export class ASTParser {
|
|
|
511
535
|
}
|
|
512
536
|
}
|
|
513
537
|
|
|
538
|
+
private parseGo(
|
|
539
|
+
filePath: string,
|
|
540
|
+
content: string,
|
|
541
|
+
lines: string[],
|
|
542
|
+
symbols: CodeSymbol[],
|
|
543
|
+
imports: Import[],
|
|
544
|
+
exports: Export[]
|
|
545
|
+
): void {
|
|
546
|
+
let inImportBlock = false;
|
|
547
|
+
|
|
548
|
+
for (let i = 0; i < lines.length; i++) {
|
|
549
|
+
const line = lines[i] || '';
|
|
550
|
+
const trimmed = line.trim();
|
|
551
|
+
const lineNum = i + 1;
|
|
552
|
+
|
|
553
|
+
// Skip comments
|
|
554
|
+
if (trimmed.startsWith('//')) continue;
|
|
555
|
+
|
|
556
|
+
// Handle import blocks
|
|
557
|
+
if (trimmed === 'import (') {
|
|
558
|
+
inImportBlock = true;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (inImportBlock && trimmed === ')') {
|
|
562
|
+
inImportBlock = false;
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Single import or import block item
|
|
567
|
+
const importMatch = inImportBlock
|
|
568
|
+
? trimmed.match(/^(?:(\w+)\s+)?"([^"]+)"/)
|
|
569
|
+
: trimmed.match(/^import\s+(?:(\w+)\s+)?"([^"]+)"/);
|
|
570
|
+
if (importMatch) {
|
|
571
|
+
const alias = importMatch[1];
|
|
572
|
+
const path = importMatch[2] || '';
|
|
573
|
+
const pkg = alias || path.split('/').pop() || '';
|
|
574
|
+
imports.push({
|
|
575
|
+
fileId: 0,
|
|
576
|
+
filePath,
|
|
577
|
+
importedFrom: path,
|
|
578
|
+
importedSymbols: [pkg],
|
|
579
|
+
isDefault: false,
|
|
580
|
+
isNamespace: false,
|
|
581
|
+
lineNumber: lineNum
|
|
582
|
+
});
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Functions and methods
|
|
587
|
+
const funcMatch = trimmed.match(/^func\s+(?:\((\w+)\s+\*?(\w+)\)\s+)?(\w+)\s*\(/);
|
|
588
|
+
if (funcMatch) {
|
|
589
|
+
const receiver = funcMatch[2];
|
|
590
|
+
const name = receiver ? `${receiver}.${funcMatch[3]}` : (funcMatch[3] || '');
|
|
591
|
+
symbols.push({
|
|
592
|
+
fileId: 0,
|
|
593
|
+
filePath,
|
|
594
|
+
kind: receiver ? 'method' : 'function',
|
|
595
|
+
name,
|
|
596
|
+
lineStart: lineNum,
|
|
597
|
+
lineEnd: this.findBlockEnd(lines, i),
|
|
598
|
+
exported: /^[A-Z]/.test(funcMatch[3] || ''),
|
|
599
|
+
signature: trimmed.split('{')[0]?.trim()
|
|
600
|
+
});
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Structs
|
|
605
|
+
const structMatch = trimmed.match(/^type\s+(\w+)\s+struct\s*\{?/);
|
|
606
|
+
if (structMatch) {
|
|
607
|
+
symbols.push({
|
|
608
|
+
fileId: 0,
|
|
609
|
+
filePath,
|
|
610
|
+
kind: 'class',
|
|
611
|
+
name: structMatch[1] || '',
|
|
612
|
+
lineStart: lineNum,
|
|
613
|
+
lineEnd: this.findBlockEnd(lines, i),
|
|
614
|
+
exported: /^[A-Z]/.test(structMatch[1] || '')
|
|
615
|
+
});
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Interfaces
|
|
620
|
+
const ifaceMatch = trimmed.match(/^type\s+(\w+)\s+interface\s*\{?/);
|
|
621
|
+
if (ifaceMatch) {
|
|
622
|
+
symbols.push({
|
|
623
|
+
fileId: 0,
|
|
624
|
+
filePath,
|
|
625
|
+
kind: 'interface',
|
|
626
|
+
name: ifaceMatch[1] || '',
|
|
627
|
+
lineStart: lineNum,
|
|
628
|
+
lineEnd: this.findBlockEnd(lines, i),
|
|
629
|
+
exported: /^[A-Z]/.test(ifaceMatch[1] || '')
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
private parseRust(
|
|
636
|
+
filePath: string,
|
|
637
|
+
content: string,
|
|
638
|
+
lines: string[],
|
|
639
|
+
symbols: CodeSymbol[],
|
|
640
|
+
imports: Import[],
|
|
641
|
+
exports: Export[]
|
|
642
|
+
): void {
|
|
643
|
+
for (let i = 0; i < lines.length; i++) {
|
|
644
|
+
const line = lines[i] || '';
|
|
645
|
+
const trimmed = line.trim();
|
|
646
|
+
const lineNum = i + 1;
|
|
647
|
+
|
|
648
|
+
// Skip comments
|
|
649
|
+
if (trimmed.startsWith('//')) continue;
|
|
650
|
+
|
|
651
|
+
// Functions
|
|
652
|
+
const fnMatch = trimmed.match(/^(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/);
|
|
653
|
+
if (fnMatch) {
|
|
654
|
+
symbols.push({
|
|
655
|
+
fileId: 0,
|
|
656
|
+
filePath,
|
|
657
|
+
kind: 'function',
|
|
658
|
+
name: fnMatch[1] || '',
|
|
659
|
+
lineStart: lineNum,
|
|
660
|
+
lineEnd: this.findBlockEnd(lines, i),
|
|
661
|
+
exported: trimmed.startsWith('pub'),
|
|
662
|
+
signature: trimmed.split('{')[0]?.trim()
|
|
663
|
+
});
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Structs
|
|
668
|
+
const structMatch = trimmed.match(/^(?:pub\s+)?struct\s+(\w+)/);
|
|
669
|
+
if (structMatch) {
|
|
670
|
+
symbols.push({
|
|
671
|
+
fileId: 0,
|
|
672
|
+
filePath,
|
|
673
|
+
kind: 'class',
|
|
674
|
+
name: structMatch[1] || '',
|
|
675
|
+
lineStart: lineNum,
|
|
676
|
+
lineEnd: this.findBlockEnd(lines, i),
|
|
677
|
+
exported: trimmed.startsWith('pub')
|
|
678
|
+
});
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Enums
|
|
683
|
+
const enumMatch = trimmed.match(/^(?:pub\s+)?enum\s+(\w+)/);
|
|
684
|
+
if (enumMatch) {
|
|
685
|
+
symbols.push({
|
|
686
|
+
fileId: 0,
|
|
687
|
+
filePath,
|
|
688
|
+
kind: 'enum',
|
|
689
|
+
name: enumMatch[1] || '',
|
|
690
|
+
lineStart: lineNum,
|
|
691
|
+
lineEnd: this.findBlockEnd(lines, i),
|
|
692
|
+
exported: trimmed.startsWith('pub')
|
|
693
|
+
});
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Traits (similar to interfaces)
|
|
698
|
+
const traitMatch = trimmed.match(/^(?:pub\s+)?trait\s+(\w+)/);
|
|
699
|
+
if (traitMatch) {
|
|
700
|
+
symbols.push({
|
|
701
|
+
fileId: 0,
|
|
702
|
+
filePath,
|
|
703
|
+
kind: 'interface',
|
|
704
|
+
name: traitMatch[1] || '',
|
|
705
|
+
lineStart: lineNum,
|
|
706
|
+
lineEnd: this.findBlockEnd(lines, i),
|
|
707
|
+
exported: trimmed.startsWith('pub')
|
|
708
|
+
});
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Impl blocks
|
|
713
|
+
const implMatch = trimmed.match(/^impl\s+(?:<[^>]+>\s+)?(?:(\w+)\s+for\s+)?(\w+)/);
|
|
714
|
+
if (implMatch) {
|
|
715
|
+
const traitName = implMatch[1];
|
|
716
|
+
const typeName = implMatch[2] || '';
|
|
717
|
+
const name = traitName ? `${traitName} for ${typeName}` : typeName;
|
|
718
|
+
symbols.push({
|
|
719
|
+
fileId: 0,
|
|
720
|
+
filePath,
|
|
721
|
+
kind: 'class',
|
|
722
|
+
name: `impl ${name}`,
|
|
723
|
+
lineStart: lineNum,
|
|
724
|
+
lineEnd: this.findBlockEnd(lines, i),
|
|
725
|
+
exported: false
|
|
726
|
+
});
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// use statements
|
|
731
|
+
const useMatch = trimmed.match(/^(?:pub\s+)?use\s+(.+);/);
|
|
732
|
+
if (useMatch) {
|
|
733
|
+
const path = (useMatch[1] || '').replace(/::/g, '/');
|
|
734
|
+
imports.push({
|
|
735
|
+
fileId: 0,
|
|
736
|
+
filePath,
|
|
737
|
+
importedFrom: path,
|
|
738
|
+
importedSymbols: [path.split('/').pop()?.replace(/[{}]/g, '') || ''],
|
|
739
|
+
isDefault: false,
|
|
740
|
+
isNamespace: path.includes('*'),
|
|
741
|
+
lineNumber: lineNum
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
private parseJava(
|
|
748
|
+
filePath: string,
|
|
749
|
+
content: string,
|
|
750
|
+
lines: string[],
|
|
751
|
+
symbols: CodeSymbol[],
|
|
752
|
+
imports: Import[],
|
|
753
|
+
exports: Export[]
|
|
754
|
+
): void {
|
|
755
|
+
let currentClass: string | null = null;
|
|
756
|
+
|
|
757
|
+
for (let i = 0; i < lines.length; i++) {
|
|
758
|
+
const line = lines[i] || '';
|
|
759
|
+
const trimmed = line.trim();
|
|
760
|
+
const lineNum = i + 1;
|
|
761
|
+
|
|
762
|
+
// Skip comments
|
|
763
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) continue;
|
|
764
|
+
|
|
765
|
+
// Imports
|
|
766
|
+
const importMatch = trimmed.match(/^import\s+(?:static\s+)?([^;]+);/);
|
|
767
|
+
if (importMatch) {
|
|
768
|
+
const path = importMatch[1] || '';
|
|
769
|
+
imports.push({
|
|
770
|
+
fileId: 0,
|
|
771
|
+
filePath,
|
|
772
|
+
importedFrom: path,
|
|
773
|
+
importedSymbols: [path.split('.').pop() || ''],
|
|
774
|
+
isDefault: false,
|
|
775
|
+
isNamespace: path.endsWith('*'),
|
|
776
|
+
lineNumber: lineNum
|
|
777
|
+
});
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Classes and interfaces
|
|
782
|
+
const classMatch = trimmed.match(/^(?:public\s+|private\s+|protected\s+)?(?:abstract\s+)?(?:final\s+)?(class|interface|enum)\s+(\w+)/);
|
|
783
|
+
if (classMatch) {
|
|
784
|
+
currentClass = classMatch[2] || '';
|
|
785
|
+
symbols.push({
|
|
786
|
+
fileId: 0,
|
|
787
|
+
filePath,
|
|
788
|
+
kind: classMatch[1] === 'interface' ? 'interface' : classMatch[1] === 'enum' ? 'enum' : 'class',
|
|
789
|
+
name: currentClass,
|
|
790
|
+
lineStart: lineNum,
|
|
791
|
+
lineEnd: this.findBlockEnd(lines, i),
|
|
792
|
+
exported: trimmed.includes('public')
|
|
793
|
+
});
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Methods
|
|
798
|
+
const methodMatch = trimmed.match(/^(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:final\s+)?(?:synchronized\s+)?(?:abstract\s+)?(?:<[^>]+>\s+)?(\w+(?:<[^>]+>)?)\s+(\w+)\s*\(/);
|
|
799
|
+
if (methodMatch && currentClass && !['if', 'for', 'while', 'switch', 'catch', 'class', 'interface', 'enum'].includes(methodMatch[2] || '')) {
|
|
800
|
+
const returnType = methodMatch[1];
|
|
801
|
+
const methodName = methodMatch[2] || '';
|
|
802
|
+
// Skip constructors (name matches class name)
|
|
803
|
+
if (methodName !== currentClass) {
|
|
804
|
+
symbols.push({
|
|
805
|
+
fileId: 0,
|
|
806
|
+
filePath,
|
|
807
|
+
kind: 'method',
|
|
808
|
+
name: `${currentClass}.${methodName}`,
|
|
809
|
+
lineStart: lineNum,
|
|
810
|
+
lineEnd: this.findBlockEnd(lines, i),
|
|
811
|
+
exported: trimmed.includes('public'),
|
|
812
|
+
signature: `${returnType} ${methodName}(...)`
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
514
819
|
private findBlockEnd(lines: string[], startIndex: number): number {
|
|
515
820
|
let braceCount = 0;
|
|
516
821
|
let started = false;
|