project-graph-mcp 1.0.1 → 1.2.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 +10 -2
- package/package.json +7 -3
- package/src/.project-graph-cache.json +1 -0
- package/src/filters.js +1 -0
- package/src/graph-builder.js +1 -1
- package/src/lang-go.js +285 -0
- package/src/lang-python.js +197 -0
- package/src/lang-typescript.js +190 -0
- package/src/lang-utils.js +124 -0
- package/src/mcp-server.js +67 -2
- package/src/parser.js +42 -3
- package/src/server.js +0 -0
- package/src/tool-defs.js +33 -0
- package/src/tools.js +237 -7
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
|
|
@@ -253,7 +257,11 @@ project-graph-mcp/
|
|
|
253
257
|
│ ├── tool-defs.js # MCP tool schemas
|
|
254
258
|
│ ├── tools.js # Graph tools (skeleton, expand, deps)
|
|
255
259
|
│ ├── workspace.js # Path resolution + traversal protection
|
|
256
|
-
│ ├── 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
|
|
257
265
|
│ ├── graph-builder.js # Minified graph + legend
|
|
258
266
|
│ ├── filters.js # Exclude patterns, .gitignore
|
|
259
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
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "MCP server for AI agents —
|
|
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",
|
|
@@ -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":1773842134375.5247,"/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":1773841810070.0337,"/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":1773699575195.6013},"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
package/src/graph-builder.js
CHANGED
|
@@ -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
|
+
}
|