project-graph-mcp 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # project-graph-mcp
2
2
 
3
- **MCP server for AI agents** — project graph, code quality analysis, and framework-specific lint rules.
3
+ **MCP server for AI agents** — multi-language project graph, code quality analysis, and framework-specific lint rules.
4
4
 
5
5
  > Developed by [RND-PRO](https://rnd-pro.com)
6
6
 
@@ -14,6 +14,7 @@ AI agents struggle with large codebases:
14
14
 
15
15
  **Project Graph MCP solves this:**
16
16
  - 📦 **10-50x compression** — skeleton view fits in context window
17
+ - 🌐 **Multi-language** — JavaScript, TypeScript, Python, Go
17
18
  - 🔍 **Code quality analysis** — dead code, complexity, duplicates
18
19
  - 🎯 **Framework-specific rules** — auto-detect and apply (React, Vue, Express, Node.js, Symbiote)
19
20
  - ✅ **Test checklists** — track @test/@expect annotations
@@ -26,6 +27,9 @@ AI agents struggle with large codebases:
26
27
  - `deps` — Dependency tree for any symbol
27
28
  - `usages` — Find all usages of a symbol
28
29
  - `get_focus_zone` — Auto-enriched context from git diff
30
+ - `get_call_chain` — BFS call path analysis between symbols
31
+
32
+ **Supported languages:** JavaScript (AST via Acorn), TypeScript/TSX, Python, Go — all with unified ParseResult API.
29
33
 
30
34
  ### 🧪 Test Checklists (Universal)
31
35
  - `get_pending_tests` — List tests from `@test/@expect` JSDoc annotations
@@ -79,12 +83,29 @@ Every tool response includes contextual coaching hints:
79
83
  - **Workspace Isolation** — MCP roots set workspace boundary, tools cannot escape it
80
84
 
81
85
  ### 🌐 MCP Ecosystem
82
- Works alongside [agent-pool-mcp](https://github.com/rnd-pro/agent-pool-mcp) for parallel agent orchestration:
86
+ Best used together with [**agent-pool-mcp**](https://www.npmjs.com/package/agent-pool-mcp) multi-agent task delegation via Gemini CLI:
83
87
 
84
88
  | Layer | project-graph-mcp | agent-pool-mcp |
85
89
  |-------|-------------------|----------------|
86
90
  | **Primary IDE agent** | Navigates codebase, runs analysis | Delegates tasks, consults peer |
87
- | **Gemini CLI workers** | Available as MCP tool inside Gemini CLI | Executes delegated tasks |
91
+ | **Gemini CLI workers** | Available as MCP tool inside workers | Executes delegated tasks |
92
+
93
+ Combined config for both:
94
+
95
+ ```json
96
+ {
97
+ "mcpServers": {
98
+ "project-graph": {
99
+ "command": "npx",
100
+ "args": ["-y", "project-graph-mcp"]
101
+ },
102
+ "agent-pool": {
103
+ "command": "npx",
104
+ "args": ["-y", "agent-pool-mcp"]
105
+ }
106
+ }
107
+ }
108
+ ```
88
109
 
89
110
  ## Installation
90
111
 
@@ -236,7 +257,11 @@ project-graph-mcp/
236
257
  │ ├── tool-defs.js # MCP tool schemas
237
258
  │ ├── tools.js # Graph tools (skeleton, expand, deps)
238
259
  │ ├── workspace.js # Path resolution + traversal protection
239
- │ ├── parser.js # AST parser (Acorn)
260
+ │ ├── parser.js # AST parser (Acorn) + language routing
261
+ │ ├── lang-typescript.js # TypeScript/TSX regex parser
262
+ │ ├── lang-python.js # Python regex parser
263
+ │ ├── lang-go.js # Go regex parser
264
+ │ ├── lang-utils.js # Shared: stripStringsAndComments
240
265
  │ ├── graph-builder.js # Minified graph + legend
241
266
  │ ├── filters.js # Exclude patterns, .gitignore
242
267
  │ ├── dead-code.js # Unused code detection
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "project-graph-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
- "description": "MCP server for AI agents — AST-based project graph, code quality analysis, and framework-specific lint rules. Zero dependencies.",
5
+ "description": "MCP server for AI agents — multi-language project graph (JS, TS, Python, Go), code quality analysis, and framework-specific lint rules. Zero dependencies.",
6
6
  "main": "src/server.js",
7
7
  "bin": {
8
8
  "project-graph-mcp": "src/server.js"
@@ -32,7 +32,11 @@
32
32
  "dead-code",
33
33
  "complexity",
34
34
  "lint",
35
- "gemini"
35
+ "gemini",
36
+ "typescript",
37
+ "python",
38
+ "golang",
39
+ "multi-language"
36
40
  ],
37
41
  "repository": {
38
42
  "type": "git",
@@ -43,4 +47,4 @@
43
47
  "engines": {
44
48
  "node": ">=18.0.0"
45
49
  }
46
- }
50
+ }
@@ -0,0 +1 @@
1
+ {"version":1,"path":"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src","mtimes":{"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/cli-handlers.js":1770656908396.6091,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/cli.js":1770402560025.795,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/complexity.js":1770659410318.8738,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/custom-rules.js":1772229894400.4248,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/dead-code.js":1771039661152.3376,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/filters.js":1773680641811.5923,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/framework-references.js":1772230204706.0527,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/full-analysis.js":1770398073907.7559,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/graph-builder.js":1773699507591.4272,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/instructions.js":1770399269998.5479,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/jsdoc-generator.js":1770659484514.0476,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/lang-go.js":1773698053155.2214,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/lang-python.js":1773699559544.998,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/lang-typescript.js":1773698053134.5566,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/lang-utils.js":1773697910333.1853,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/large-files.js":1770659415051.0068,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/mcp-server.js":1773680723694.6929,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/outdated-patterns.js":1770659431382.597,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/parser.js":1773683193338.3542,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/server.js":1773497829401.444,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/similar-functions.js":1770659421339.757,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/test-annotations.js":1772223332771.1304,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/tool-defs.js":1773680708907.4548,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/tools.js":1773699507598.5554,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/undocumented.js":1770659436429.1372,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/workspace.js":1773699468757.9526},"graph":{"v":1,"legend":{"getArg":"gA","getPath":"gP","printHelp":"pH","runCLI":"CLI","findJSFiles":"JSF","calculateComplexity":"cC","getRating":"gR","analyzeFile":"aF","getComplexity":"gC","parseGraphignore":"pG","isGraphignored":"iG","loadRuleSets":"RS","saveRuleSet":"RS1","findFiles":"fF","isExcluded":"iE","isInStringOrComment":"ISO","isWithinContext":"WC","checkFileAgainstRule":"FAR","getCustomRules":"CR","setCustomRule":"CR1","deleteCustomRule":"CR2","detectProjectRuleSets":"PRS","checkCustomRules":"CR3","findProjectRoot":"PR","analyzeFileLocals":"FL","getDeadCode":"DC","getFilters":"gF","setFilters":"sF","addExcludes":"aE","removeExcludes":"rE","resetFilters":"rF","parseGitignore":"pG1","shouldExcludeDir":"ED","shouldExcludeFile":"EF","matchWildcard":"mW","matchGitignorePattern":"GP","fetchReference":"fR","listAvailable":"lA","getFrameworkReference":"FR","calculateHealthScore":"HS","getFullAnalysis":"FA","minifyLegend":"mL","createShortName":"SN","buildGraph":"bG","createSkeleton":"cS","getInstructions":"gI","generateJSDoc":"JSD","buildJSDoc":"JSD1","extractParamName":"PN","inferParamType":"PT","generateJSDocFor":"JSD2","parseGo":"pG2","extractImports":"eI","getBody":"gB","extractCalls":"eC","parsePython":"pP","parseTypeScript":"TS","extractParams":"eP","stripStringsAndComments":"SAC","getLargeFiles":"LF","createServer":"cS1","startStdioServer":"SS","analyzeFilePatterns":"FP","analyzePackageJson":"PJ","getOutdatedPatterns":"OP","parseFile":"pF","parseProject":"pP1","parseFileByExtension":"FBE","isSourceFile":"SF","extractSignatures":"eS","buildSignature":"bS","hashBodyStructure":"BS","calculateSimilarity":"cS2","getSimilarFunctions":"SF1","parseAnnotations":"pA","getAllFeatures":"AF","getPendingTests":"PT1","markTestPassed":"TP","markTestFailed":"TF","getTestSummary":"TS1","resetTestState":"TS2","generateMarkdown":"gM","saveDiskCache":"DC1","loadDiskCache":"DC2","getGraph":"gG","detectChanges":"dC","snapshotMtimes":"sM","getSkeleton":"gS","getFocusZone":"FZ","expand":"ex","deps":"de","usages":"us","extractMethod":"eM","getCallChain":"CC","invalidateCache":"iC","extractComments":"eC1","findJSDocBefore":"JSD3","checkMissing":"cM","getUndocumented":"gU","getUndocumentedSummary":"US","setRoots":"sR","getWorkspaceRoot":"WR","resolvePath":"rP"},"reverseLegend":{"gA":"getArg","gP":"getPath","pH":"printHelp","CLI":"runCLI","JSF":"findJSFiles","cC":"calculateComplexity","gR":"getRating","aF":"analyzeFile","gC":"getComplexity","pG":"parseGraphignore","iG":"isGraphignored","RS":"loadRuleSets","RS1":"saveRuleSet","fF":"findFiles","iE":"isExcluded","ISO":"isInStringOrComment","WC":"isWithinContext","FAR":"checkFileAgainstRule","CR":"getCustomRules","CR1":"setCustomRule","CR2":"deleteCustomRule","PRS":"detectProjectRuleSets","CR3":"checkCustomRules","PR":"findProjectRoot","FL":"analyzeFileLocals","DC":"getDeadCode","gF":"getFilters","sF":"setFilters","aE":"addExcludes","rE":"removeExcludes","rF":"resetFilters","pG1":"parseGitignore","ED":"shouldExcludeDir","EF":"shouldExcludeFile","mW":"matchWildcard","GP":"matchGitignorePattern","fR":"fetchReference","lA":"listAvailable","FR":"getFrameworkReference","HS":"calculateHealthScore","FA":"getFullAnalysis","mL":"minifyLegend","SN":"createShortName","bG":"buildGraph","cS":"createSkeleton","gI":"getInstructions","JSD":"generateJSDoc","JSD1":"buildJSDoc","PN":"extractParamName","PT":"inferParamType","JSD2":"generateJSDocFor","pG2":"parseGo","eI":"extractImports","gB":"getBody","eC":"extractCalls","pP":"parsePython","TS":"parseTypeScript","eP":"extractParams","SAC":"stripStringsAndComments","LF":"getLargeFiles","cS1":"createServer","SS":"startStdioServer","FP":"analyzeFilePatterns","PJ":"analyzePackageJson","OP":"getOutdatedPatterns","pF":"parseFile","pP1":"parseProject","FBE":"parseFileByExtension","SF":"isSourceFile","eS":"extractSignatures","bS":"buildSignature","BS":"hashBodyStructure","cS2":"calculateSimilarity","SF1":"getSimilarFunctions","pA":"parseAnnotations","AF":"getAllFeatures","PT1":"getPendingTests","TP":"markTestPassed","TF":"markTestFailed","TS1":"getTestSummary","TS2":"resetTestState","gM":"generateMarkdown","DC1":"saveDiskCache","DC2":"loadDiskCache","gG":"getGraph","dC":"detectChanges","sM":"snapshotMtimes","gS":"getSkeleton","FZ":"getFocusZone","ex":"expand","de":"deps","us":"usages","eM":"extractMethod","CC":"getCallChain","iC":"invalidateCache","eC1":"extractComments","JSD3":"findJSDocBefore","cM":"checkMissing","gU":"getUndocumented","US":"getUndocumentedSummary","sR":"setRoots","WR":"getWorkspaceRoot","rP":"resolvePath"},"stats":{"files":26,"classes":0,"functions":115},"nodes":{"gA":{"t":"F","e":false,"f":"cli-handlers.js"},"gP":{"t":"F","e":false,"f":"cli-handlers.js"},"pH":{"t":"F","e":true,"f":"cli.js"},"CLI":{"t":"F","e":true,"f":"cli.js"},"JSF":{"t":"F","e":false,"f":"undocumented.js"},"cC":{"t":"F","e":false,"f":"complexity.js"},"gR":{"t":"F","e":false,"f":"complexity.js"},"aF":{"t":"F","e":false,"f":"large-files.js"},"gC":{"t":"F","e":true,"f":"complexity.js"},"pG":{"t":"F","e":false,"f":"custom-rules.js"},"iG":{"t":"F","e":false,"f":"custom-rules.js"},"RS":{"t":"F","e":false,"f":"custom-rules.js"},"RS1":{"t":"F","e":false,"f":"custom-rules.js"},"fF":{"t":"F","e":false,"f":"custom-rules.js"},"iE":{"t":"F","e":false,"f":"custom-rules.js"},"ISO":{"t":"F","e":false,"f":"custom-rules.js"},"WC":{"t":"F","e":false,"f":"custom-rules.js"},"FAR":{"t":"F","e":false,"f":"custom-rules.js"},"CR":{"t":"F","e":true,"f":"custom-rules.js"},"CR1":{"t":"F","e":true,"f":"custom-rules.js"},"CR2":{"t":"F","e":true,"f":"custom-rules.js"},"PRS":{"t":"F","e":true,"f":"custom-rules.js"},"CR3":{"t":"F","e":true,"f":"custom-rules.js"},"PR":{"t":"F","e":false,"f":"dead-code.js"},"FL":{"t":"F","e":false,"f":"dead-code.js"},"DC":{"t":"F","e":true,"f":"dead-code.js"},"gF":{"t":"F","e":true,"f":"filters.js"},"sF":{"t":"F","e":true,"f":"filters.js"},"aE":{"t":"F","e":true,"f":"filters.js"},"rE":{"t":"F","e":true,"f":"filters.js"},"rF":{"t":"F","e":true,"f":"filters.js"},"pG1":{"t":"F","e":true,"f":"filters.js"},"ED":{"t":"F","e":true,"f":"filters.js"},"EF":{"t":"F","e":true,"f":"filters.js"},"mW":{"t":"F","e":false,"f":"filters.js"},"GP":{"t":"F","e":false,"f":"filters.js"},"fR":{"t":"F","e":false,"f":"framework-references.js"},"lA":{"t":"F","e":false,"f":"framework-references.js"},"FR":{"t":"F","e":true,"f":"framework-references.js"},"HS":{"t":"F","e":false,"f":"full-analysis.js"},"FA":{"t":"F","e":true,"f":"full-analysis.js"},"mL":{"t":"F","e":true,"f":"graph-builder.js"},"SN":{"t":"F","e":false,"f":"graph-builder.js"},"bG":{"t":"F","e":true,"f":"graph-builder.js"},"cS":{"t":"F","e":true,"f":"graph-builder.js"},"gI":{"t":"F","e":true,"f":"instructions.js"},"JSD":{"t":"F","e":true,"f":"jsdoc-generator.js"},"JSD1":{"t":"F","e":false,"f":"jsdoc-generator.js"},"PN":{"t":"F","e":false,"f":"similar-functions.js"},"PT":{"t":"F","e":false,"f":"jsdoc-generator.js"},"JSD2":{"t":"F","e":true,"f":"jsdoc-generator.js"},"pG2":{"t":"F","e":true,"f":"lang-go.js"},"eI":{"t":"F","e":false,"f":"lang-go.js"},"gB":{"t":"F","e":false,"f":"lang-go.js"},"eC":{"t":"F","e":false,"f":"parser.js"},"pP":{"t":"F","e":true,"f":"lang-python.js"},"TS":{"t":"F","e":true,"f":"lang-typescript.js"},"eP":{"t":"F","e":false,"f":"lang-typescript.js"},"SAC":{"t":"F","e":true,"f":"lang-utils.js"},"LF":{"t":"F","e":true,"f":"large-files.js"},"cS1":{"t":"F","e":true,"f":"mcp-server.js"},"SS":{"t":"F","e":true,"f":"mcp-server.js"},"FP":{"t":"F","e":false,"f":"outdated-patterns.js"},"PJ":{"t":"F","e":false,"f":"outdated-patterns.js"},"OP":{"t":"F","e":true,"f":"outdated-patterns.js"},"pF":{"t":"F","e":false,"f":"undocumented.js"},"pP1":{"t":"F","e":true,"f":"parser.js"},"FBE":{"t":"F","e":false,"f":"parser.js"},"SF":{"t":"F","e":false,"f":"parser.js"},"eS":{"t":"F","e":false,"f":"similar-functions.js"},"bS":{"t":"F","e":false,"f":"similar-functions.js"},"BS":{"t":"F","e":false,"f":"similar-functions.js"},"cS2":{"t":"F","e":false,"f":"similar-functions.js"},"SF1":{"t":"F","e":true,"f":"similar-functions.js"},"pA":{"t":"F","e":true,"f":"test-annotations.js"},"AF":{"t":"F","e":true,"f":"test-annotations.js"},"PT1":{"t":"F","e":true,"f":"test-annotations.js"},"TP":{"t":"F","e":true,"f":"test-annotations.js"},"TF":{"t":"F","e":true,"f":"test-annotations.js"},"TS1":{"t":"F","e":true,"f":"test-annotations.js"},"TS2":{"t":"F","e":true,"f":"test-annotations.js"},"gM":{"t":"F","e":true,"f":"test-annotations.js"},"DC1":{"t":"F","e":false,"f":"tools.js"},"DC2":{"t":"F","e":false,"f":"tools.js"},"gG":{"t":"F","e":false,"f":"tools.js"},"dC":{"t":"F","e":false,"f":"tools.js"},"sM":{"t":"F","e":false,"f":"tools.js"},"gS":{"t":"F","e":true,"f":"tools.js"},"FZ":{"t":"F","e":true,"f":"tools.js"},"ex":{"t":"F","e":true,"f":"tools.js"},"de":{"t":"F","e":true,"f":"tools.js"},"us":{"t":"F","e":true,"f":"tools.js"},"eM":{"t":"F","e":false,"f":"tools.js"},"CC":{"t":"F","e":true,"f":"tools.js"},"iC":{"t":"F","e":true,"f":"tools.js"},"eC1":{"t":"F","e":false,"f":"undocumented.js"},"JSD3":{"t":"F","e":false,"f":"undocumented.js"},"cM":{"t":"F","e":false,"f":"undocumented.js"},"gU":{"t":"F","e":true,"f":"undocumented.js"},"US":{"t":"F","e":true,"f":"undocumented.js"},"sR":{"t":"F","e":true,"f":"workspace.js"},"WR":{"t":"F","e":true,"f":"workspace.js"},"rP":{"t":"F","e":true,"f":"workspace.js"}},"edges":[],"orphans":["getArg","getPath","findJSFiles","calculateComplexity","getRating","analyzeFile","parseGraphignore","isGraphignored","loadRuleSets","saveRuleSet","findFiles","isExcluded","isInStringOrComment","isWithinContext","checkFileAgainstRule","findProjectRoot","analyzeFileLocals","matchWildcard","matchGitignorePattern","fetchReference","listAvailable","calculateHealthScore","createShortName","buildJSDoc","extractParamName","inferParamType","extractImports","getBody","extractCalls","extractParams","analyzeFilePatterns","analyzePackageJson","parseFile","parseFileByExtension","isSourceFile","extractSignatures","buildSignature","hashBodyStructure","calculateSimilarity","saveDiskCache","loadDiskCache","getGraph","detectChanges","snapshotMtimes","extractMethod","extractComments","findJSDocBefore","checkMissing"],"duplicates":{},"files":["cli-handlers.js","cli.js","complexity.js","custom-rules.js","dead-code.js","filters.js","framework-references.js","full-analysis.js","graph-builder.js","instructions.js","jsdoc-generator.js","lang-go.js","lang-python.js","lang-typescript.js","lang-utils.js","large-files.js","mcp-server.js","outdated-patterns.js","parser.js","server.js","similar-functions.js","test-annotations.js","tool-defs.js","tools.js","undocumented.js","workspace.js"]}}
package/src/filters.js CHANGED
@@ -32,6 +32,7 @@ const DEFAULT_EXCLUDE_PATTERNS = [
32
32
  '*.min.js',
33
33
  '*.bundle.js',
34
34
  '*.d.ts',
35
+ '.project-graph-cache.json',
35
36
  ];
36
37
 
37
38
  // Current filter configuration (mutable via MCP)
@@ -169,7 +169,7 @@ export function buildGraph(parsed) {
169
169
  }
170
170
 
171
171
  // Detect duplicates (same method name in multiple classes)
172
- const methodLocations = {};
172
+ const methodLocations = Object.create(null);
173
173
  for (const cls of classes) {
174
174
  for (const method of cls.methods || []) {
175
175
  if (!methodLocations[method]) {
package/src/lang-go.js ADDED
@@ -0,0 +1,285 @@
1
+ import { stripStringsAndComments } from './lang-utils.js';
2
+
3
+ /**
4
+ * Parse Go file using regex-based structural extraction.
5
+ * @param {string} code - Go source code
6
+ * @param {string} filename - File path
7
+ * @returns {ParseResult}
8
+ */
9
+ export function parseGo(code, filename) {
10
+ const result = {
11
+ file: filename,
12
+ classes: [],
13
+ functions: [],
14
+ imports: [],
15
+ exports: []
16
+ };
17
+
18
+ const { imports, packageNames } = extractImports(code);
19
+ result.imports = imports;
20
+
21
+ const cleanCode = stripStringsAndComments(code, {
22
+ singleQuote: false,
23
+ backtick: true,
24
+ templateInterpolation: false
25
+ });
26
+
27
+ const classesMap = new Map();
28
+
29
+ // Extract Structs (mapped to classes)
30
+ const structRegex = /^\s*type\s+([a-zA-Z_]\w*)\s+struct\s*\{/gm;
31
+ let match;
32
+ while ((match = structRegex.exec(cleanCode)) !== null) {
33
+ const name = match[1];
34
+ const start = match.index + match[0].length;
35
+ const body = getBody(cleanCode, start);
36
+ const line = code.substring(0, match.index).split('\n').length;
37
+
38
+ let extendsName = null;
39
+ const properties = [];
40
+
41
+ const lines = body.split('\n').map(l => l.trim()).filter(l => l);
42
+ for (const lineStr of lines) {
43
+ const parts = lineStr.split(/\s+/);
44
+ if (parts.length === 1) {
45
+ extendsName = parts[0].replace(/^\*/, ''); // Remove pointer if embedded
46
+ } else if (parts.length >= 2) {
47
+ const propName = parts[0].replace(/,$/, '');
48
+ properties.push(propName);
49
+ }
50
+ }
51
+
52
+ classesMap.set(name, {
53
+ name,
54
+ extends: extendsName,
55
+ methods: [],
56
+ properties,
57
+ calls: [],
58
+ file: filename,
59
+ line
60
+ });
61
+ }
62
+
63
+ // Extract Interfaces (mapped to classes)
64
+ const interfaceRegex = /^\s*type\s+([a-zA-Z_]\w*)\s+interface\s*\{/gm;
65
+ while ((match = interfaceRegex.exec(cleanCode)) !== null) {
66
+ const name = match[1];
67
+ const start = match.index + match[0].length;
68
+ const body = getBody(cleanCode, start);
69
+ const line = code.substring(0, match.index).split('\n').length;
70
+
71
+ let extendsName = null;
72
+ const methods = [];
73
+
74
+ const lines = body.split('\n').map(l => l.trim()).filter(l => l);
75
+ for (const lineStr of lines) {
76
+ const parenIndex = lineStr.indexOf('(');
77
+ if (parenIndex !== -1) {
78
+ const beforeParen = lineStr.substring(0, parenIndex).trim();
79
+ const parts = beforeParen.split(/\s+/);
80
+ const methodName = parts[parts.length - 1];
81
+ if (methodName) {
82
+ methods.push(methodName);
83
+ }
84
+ } else {
85
+ const parts = lineStr.split(/\s+/);
86
+ if (parts.length === 1) {
87
+ extendsName = parts[0];
88
+ }
89
+ }
90
+ }
91
+
92
+ classesMap.set(name, {
93
+ name,
94
+ extends: extendsName,
95
+ methods,
96
+ properties: [],
97
+ calls: [],
98
+ file: filename,
99
+ line
100
+ });
101
+ }
102
+
103
+ // Extract Methods
104
+ const methodRegex = /^\s*func\s+\(\s*[a-zA-Z_]\w*\s+\*?([a-zA-Z_]\w*)\s*\)\s+([a-zA-Z_]\w*)[^{]*\{/gm;
105
+ while ((match = methodRegex.exec(cleanCode)) !== null) {
106
+ const className = match[1];
107
+ const methodName = match[2];
108
+ const start = match.index + match[0].length;
109
+ const body = getBody(cleanCode, start);
110
+ const line = code.substring(0, match.index).split('\n').length;
111
+
112
+ const methodCalls = extractCalls(body, packageNames);
113
+
114
+ if (!classesMap.has(className)) {
115
+ classesMap.set(className, {
116
+ name: className,
117
+ extends: null,
118
+ methods: [],
119
+ properties: [],
120
+ calls: [],
121
+ file: filename,
122
+ line
123
+ });
124
+ }
125
+
126
+ const classInfo = classesMap.get(className);
127
+ classInfo.methods.push(methodName);
128
+
129
+ for (const call of methodCalls) {
130
+ if (!classInfo.calls.includes(call)) {
131
+ classInfo.calls.push(call);
132
+ }
133
+ }
134
+ }
135
+
136
+ // Extract Functions (top-level)
137
+ const funcRegex = /^\s*func\s+([a-zA-Z_]\w*)\s*\(([^)]*)\)[^{]*\{/gm;
138
+ while ((match = funcRegex.exec(cleanCode)) !== null) {
139
+ const name = match[1];
140
+ const paramsStr = match[2];
141
+ const params = paramsStr.split(',')
142
+ .map(p => p.trim().split(/\s+/)[0])
143
+ .filter(p => p);
144
+
145
+ const exported = /^[A-Z]/.test(name);
146
+ const start = match.index + match[0].length;
147
+ const body = getBody(cleanCode, start);
148
+ const line = code.substring(0, match.index).split('\n').length;
149
+
150
+ const calls = extractCalls(body, packageNames);
151
+
152
+ result.functions.push({
153
+ name,
154
+ exported,
155
+ calls,
156
+ params,
157
+ file: filename,
158
+ line
159
+ });
160
+ }
161
+
162
+ result.classes = Array.from(classesMap.values());
163
+
164
+ // Extract Exports
165
+ for (const cls of result.classes) {
166
+ if (/^[A-Z]/.test(cls.name)) {
167
+ result.exports.push(cls.name);
168
+ }
169
+ }
170
+ for (const fn of result.functions) {
171
+ if (fn.exported) {
172
+ result.exports.push(fn.name);
173
+ }
174
+ }
175
+
176
+ return result;
177
+ }
178
+
179
+ function extractImports(text) {
180
+ const imports = [];
181
+ const packageNames = new Set();
182
+
183
+ // Strip comments to avoid commented out imports
184
+ const noComments = text.replace(/\/\/.*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
185
+
186
+ const importBlockRegex = /import\s*\(([\s\S]*?)\)/g;
187
+ let match;
188
+ while ((match = importBlockRegex.exec(noComments)) !== null) {
189
+ const block = match[1];
190
+ const lines = block.split('\n');
191
+ for (const line of lines) {
192
+ const lineMatch = line.match(/(?:([a-zA-Z_]\w*)\s+)?"([^"]+)"/);
193
+ if (lineMatch) {
194
+ const alias = lineMatch[1];
195
+ const pkgPath = lineMatch[2];
196
+ if (alias) {
197
+ if (!imports.includes(alias)) {
198
+ imports.push(alias);
199
+ packageNames.add(alias);
200
+ }
201
+ } else {
202
+ if (!imports.includes(pkgPath)) {
203
+ imports.push(pkgPath);
204
+ const parts = pkgPath.split('/');
205
+ packageNames.add(parts[parts.length - 1]);
206
+ }
207
+ }
208
+ }
209
+ }
210
+ }
211
+
212
+ const singleImportRegex = /import\s+(?:([a-zA-Z_]\w*)\s+)?"([^"]+)"/g;
213
+ while ((match = singleImportRegex.exec(noComments)) !== null) {
214
+ const alias = match[1];
215
+ const pkgPath = match[2];
216
+ if (alias) {
217
+ if (!imports.includes(alias)) {
218
+ imports.push(alias);
219
+ packageNames.add(alias);
220
+ }
221
+ } else {
222
+ if (!imports.includes(pkgPath)) {
223
+ imports.push(pkgPath);
224
+ const parts = pkgPath.split('/');
225
+ packageNames.add(parts[parts.length - 1]);
226
+ }
227
+ }
228
+ }
229
+
230
+ return { imports, packageNames };
231
+ }
232
+
233
+ /**
234
+ * Extract block body correctly handling nested braces.
235
+ * @param {string} code
236
+ * @param {number} startIndex
237
+ * @returns {string}
238
+ */
239
+ function getBody(code, startIndex) {
240
+ let braces = 1;
241
+ let end = startIndex;
242
+ while (end < code.length && braces > 0) {
243
+ if (code[end] === '{') braces++;
244
+ else if (code[end] === '}') braces--;
245
+ end++;
246
+ }
247
+ return code.substring(startIndex, end - 1);
248
+ }
249
+
250
+ /**
251
+ * Extract method calls from a block of code, filtering out Go keywords.
252
+ * @param {string} body
253
+ * @param {Set<string>} packageNames
254
+ * @returns {string[]}
255
+ */
256
+ function extractCalls(body, packageNames) {
257
+ const calls = [];
258
+ const callRegex = /([a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)?)\s*\(/g;
259
+ let match;
260
+ while ((match = callRegex.exec(body)) !== null) {
261
+ let callName = match[1];
262
+
263
+ const keywords = [
264
+ 'if', 'for', 'switch', 'func', 'panic', 'recover', 'len', 'cap',
265
+ 'make', 'new', 'append', 'copy', 'delete', 'close',
266
+ 'int', 'string', 'bool', 'byte', 'rune', 'float32', 'float64',
267
+ 'int32', 'int64', 'uint32', 'uint64', 'complex64', 'complex128'
268
+ ];
269
+ if (keywords.includes(callName)) continue;
270
+
271
+ if (callName.includes('.')) {
272
+ const parts = callName.split('.');
273
+ // If the first part is a known package name, keep it (e.g., fmt.Println)
274
+ // Otherwise, assume it's a method call on a variable and strip it (e.g., s.Handle -> Handle)
275
+ if (!packageNames.has(parts[0])) {
276
+ callName = parts[1];
277
+ }
278
+ }
279
+
280
+ if (!calls.includes(callName)) {
281
+ calls.push(callName);
282
+ }
283
+ }
284
+ return calls;
285
+ }
@@ -0,0 +1,197 @@
1
+ import { stripStringsAndComments } from './lang-utils.js';
2
+
3
+ /**
4
+ * Parse Python file using regex-based structural extraction.
5
+ * @param {string} code - Python source code
6
+ * @param {string} filename - File path
7
+ * @returns {ParseResult}
8
+ */
9
+ export function parsePython(code = '', filename = '') {
10
+ const result = {
11
+ file: filename,
12
+ classes: [],
13
+ functions: [],
14
+ imports: [],
15
+ exports: []
16
+ };
17
+
18
+ // Pre-process: remove docstrings, triple-quoted strings, and line comments
19
+ const cleanCode = stripStringsAndComments(code, {
20
+ singleQuote: true,
21
+ hashComment: true,
22
+ tripleQuote: true
23
+ });
24
+
25
+ const lines = cleanCode.split('\n');
26
+
27
+ let currentClass = null;
28
+ let currentFunc = null;
29
+ let classIndent = -1;
30
+
31
+ for (let i = 0; i < lines.length; i++) {
32
+ const line = lines[i];
33
+ if (!line.trim()) continue;
34
+
35
+ const indentMatch = line.match(/^([ \t]*)/);
36
+ const indent = indentMatch ? indentMatch[1].length : 0;
37
+
38
+ // Check if we exited a class scope
39
+ if (currentClass && indent <= classIndent) {
40
+ currentClass = null;
41
+ classIndent = -1;
42
+ }
43
+
44
+ // Check if we exited a function scope
45
+ if (currentFunc && indent === 0) {
46
+ currentFunc = null;
47
+ }
48
+
49
+ // Match Class (top-level)
50
+ const classMatch = line.match(/^class\s+([a-zA-Z_]\w*)(?:\s*\((.*?)\))?\s*:/);
51
+ if (classMatch) {
52
+ currentClass = {
53
+ name: classMatch[1],
54
+ extends: classMatch[2] ? classMatch[2].trim() : null,
55
+ methods: [],
56
+ properties: [],
57
+ calls: [],
58
+ file: filename,
59
+ line: i + 1
60
+ };
61
+ result.classes.push(currentClass);
62
+ classIndent = indent;
63
+ currentFunc = null;
64
+ continue;
65
+ }
66
+
67
+ // Match top-level function
68
+ const funcMatch = line.match(/^(?:async\s+)?def\s+([a-zA-Z_]\w*)\s*\(([^)]*)\)?/);
69
+ if (funcMatch) {
70
+ const paramsStr = funcMatch[2] || '';
71
+ const params = paramsStr.split(',')
72
+ .map(p => p.split(/[:=]/)[0].trim())
73
+ .filter(p => p && p !== 'self' && p !== 'cls');
74
+
75
+ currentFunc = {
76
+ name: funcMatch[1],
77
+ exported: true, // we'll adjust later if __all__ is present
78
+ calls: [],
79
+ params: params,
80
+ file: filename,
81
+ line: i + 1
82
+ };
83
+ result.functions.push(currentFunc);
84
+ currentClass = null;
85
+ continue;
86
+ }
87
+
88
+ // Match Method (inside class)
89
+ const methodMatch = line.match(/^[ \t]+(?:async\s+)?def\s+([a-zA-Z_]\w*)\s*\(/);
90
+ if (methodMatch && currentClass && indent > classIndent) {
91
+ const methodName = methodMatch[1];
92
+ if (methodName !== '__init__') {
93
+ currentClass.methods.push(methodName);
94
+ }
95
+ currentFunc = null; // not a top-level function
96
+ continue;
97
+ }
98
+
99
+ // Match Imports
100
+ const importMatch = line.match(/^\s*import\s+(.+)/);
101
+ if (importMatch) {
102
+ const parts = importMatch[1].split(',');
103
+ for (const part of parts) {
104
+ const p = part.trim();
105
+ const asMatch = p.match(/(?:.+)\s+as\s+([a-zA-Z_]\w*)/);
106
+ if (asMatch) {
107
+ result.imports.push(asMatch[1]);
108
+ } else {
109
+ result.imports.push(p.split('.')[0]); // take root module
110
+ }
111
+ }
112
+ continue;
113
+ }
114
+
115
+ const fromImportMatch = line.match(/^\s*from\s+([.\w]+)\s+import\s*(.*)/);
116
+ if (fromImportMatch) {
117
+ let imported = fromImportMatch[2];
118
+ if (imported.includes('(') && !imported.includes(')')) {
119
+ let j = i + 1;
120
+ while (j < lines.length) {
121
+ imported += ' ' + lines[j];
122
+ if (lines[j].includes(')')) {
123
+ i = j;
124
+ break;
125
+ }
126
+ j++;
127
+ }
128
+ }
129
+ imported = imported.replace(/[()]/g, '');
130
+ const parts = imported.split(',');
131
+ for (const part of parts) {
132
+ const p = part.trim();
133
+ if (!p) continue;
134
+ const asMatch = p.match(/(?:.+)\s+as\s+([a-zA-Z_]\w*)/);
135
+ if (asMatch) {
136
+ result.imports.push(asMatch[1]);
137
+ } else {
138
+ result.imports.push(p);
139
+ }
140
+ }
141
+ continue;
142
+ }
143
+
144
+ // Extract calls: look for func(...)
145
+ const callRegex = /([a-zA-Z_][\w.]*)\s*\(/g;
146
+ let match;
147
+ const keywords = new Set(['if', 'while', 'for', 'elif', 'return', 'yield', 'def', 'class', 'and', 'or', 'not', 'in', 'is', 'print']);
148
+ while ((match = callRegex.exec(line)) !== null) {
149
+ const callName = match[1];
150
+ if (keywords.has(callName)) continue;
151
+
152
+ let cleanCallName = callName;
153
+ if (cleanCallName.startsWith('self.')) {
154
+ cleanCallName = cleanCallName.substring(5);
155
+ }
156
+
157
+ if (currentFunc) {
158
+ if (!currentFunc.calls.includes(cleanCallName)) {
159
+ currentFunc.calls.push(cleanCallName);
160
+ }
161
+ } else if (currentClass) {
162
+ if (!currentClass.calls.includes(cleanCallName)) {
163
+ currentClass.calls.push(cleanCallName);
164
+ }
165
+ }
166
+ }
167
+ }
168
+
169
+ // Handle Exports (__all__)
170
+ const allMatch = code.match(/__all__\s*=\s*\[(.*?)\]/s);
171
+ if (allMatch) {
172
+ const exportsRaw = allMatch[1];
173
+ const exportRegex = /['"]([^'"]+)['"]/g;
174
+ let exMatch;
175
+ while ((exMatch = exportRegex.exec(exportsRaw)) !== null) {
176
+ result.exports.push(exMatch[1]);
177
+ }
178
+ // Update exported flags for functions
179
+ for (const fn of result.functions) {
180
+ fn.exported = result.exports.includes(fn.name);
181
+ }
182
+ } else {
183
+ // Implicit exports: all top-level functions and classes are exported
184
+ for (const cls of result.classes) {
185
+ result.exports.push(cls.name);
186
+ }
187
+ for (const fn of result.functions) {
188
+ result.exports.push(fn.name);
189
+ fn.exported = true;
190
+ }
191
+ }
192
+
193
+ // Deduplicate imports
194
+ result.imports = [...new Set(result.imports)];
195
+
196
+ return result;
197
+ }