maven-indexer-mcp 1.0.1 → 1.0.3
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 +8 -1
- package/build/class_parser.js +120 -0
- package/build/config.js +18 -0
- package/build/db/index.js +30 -4
- package/build/index.js +133 -136
- package/build/indexer.js +222 -60
- package/build/source_parser.js +52 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@ A Model Context Protocol (MCP) server that indexes your local Maven repository (
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
* **Semantic Class Search**: Search for classes by name (e.g., `StringUtils`) or purpose (e.g., `JsonToXml`).
|
|
8
|
+
* **Inheritance Search**: Find all implementations of an interface or subclasses of a class.
|
|
8
9
|
* **On-Demand Analysis**: Extracts method signatures (`javap`) and Javadocs directly from JARs without extracting the entire archive.
|
|
9
10
|
* **Source Code Retrieval**: Provides full source code if `-sources.jar` is available.
|
|
10
11
|
* **Real-time Monitoring**: Watches the Maven repository for changes (e.g., new `mvn install`) and automatically updates the index.
|
|
@@ -33,6 +34,7 @@ If the auto-detection fails, or if you want to filter which packages are indexed
|
|
|
33
34
|
|
|
34
35
|
* **`MAVEN_REPO`**: Absolute path to your local Maven repository (e.g., `/Users/yourname/.m2/repository`). Use this if your repository is in a non-standard location.
|
|
35
36
|
* **`INCLUDED_PACKAGES`**: Comma-separated list of package patterns to index (e.g., `com.mycompany.*,org.example.*`). Default is `*` (index everything).
|
|
37
|
+
* **`MAVEN_INDEXER_CFR_PATH`**: (Optional) Absolute path to a specific CFR decompiler JAR. If not provided, the server will attempt to use its bundled CFR version.
|
|
36
38
|
|
|
37
39
|
Example with optional configuration:
|
|
38
40
|
|
|
@@ -44,7 +46,8 @@ Example with optional configuration:
|
|
|
44
46
|
"args": ["-y", "maven-indexer-mcp@latest"],
|
|
45
47
|
"env": {
|
|
46
48
|
"MAVEN_REPO": "/Users/yourname/.m2/repository",
|
|
47
|
-
"INCLUDED_PACKAGES": "com.mycompany.*"
|
|
49
|
+
"INCLUDED_PACKAGES": "com.mycompany.*",
|
|
50
|
+
"MAVEN_INDEXER_CFR_PATH": "/path/to/cfr-0.152.jar"
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
}
|
|
@@ -88,6 +91,10 @@ If you prefer to run from source:
|
|
|
88
91
|
* Input: `className`, `artifactId`, `type` ("signatures", "docs", "source")
|
|
89
92
|
* Output: Method signatures, Javadocs, or full source code.
|
|
90
93
|
* **`search_artifacts`**: Search for artifacts by coordinate (groupId, artifactId).
|
|
94
|
+
* **`search_implementations`**: Search for classes that implement a specific interface or extend a specific class.
|
|
95
|
+
* Input: `className` (e.g. "java.util.List")
|
|
96
|
+
* Output: List of implementation/subclass names and their artifacts.
|
|
97
|
+
* **`refresh_index`**: Trigger a re-scan of the Maven repository.
|
|
91
98
|
|
|
92
99
|
## Development
|
|
93
100
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export class ClassParser {
|
|
2
|
+
buffer;
|
|
3
|
+
offset = 0;
|
|
4
|
+
constantPool = [];
|
|
5
|
+
constructor(buffer) {
|
|
6
|
+
this.buffer = buffer;
|
|
7
|
+
}
|
|
8
|
+
static parse(buffer) {
|
|
9
|
+
const parser = new ClassParser(buffer);
|
|
10
|
+
return parser.parse();
|
|
11
|
+
}
|
|
12
|
+
parse() {
|
|
13
|
+
if (this.buffer.length < 10) {
|
|
14
|
+
throw new Error("Invalid class file: too short");
|
|
15
|
+
}
|
|
16
|
+
const magic = this.readU4();
|
|
17
|
+
if (magic !== 0xCAFEBABE) {
|
|
18
|
+
throw new Error("Invalid magic number");
|
|
19
|
+
}
|
|
20
|
+
this.readU2(); // minor
|
|
21
|
+
this.readU2(); // major
|
|
22
|
+
const cpCount = this.readU2();
|
|
23
|
+
this.constantPool = new Array(cpCount);
|
|
24
|
+
// Constant Pool is 1-indexed (1 to count-1)
|
|
25
|
+
for (let i = 1; i < cpCount; i++) {
|
|
26
|
+
const tag = this.readU1();
|
|
27
|
+
switch (tag) {
|
|
28
|
+
case 1: // UTF8
|
|
29
|
+
const len = this.readU2();
|
|
30
|
+
const str = this.buffer.toString('utf-8', this.offset, this.offset + len);
|
|
31
|
+
this.offset += len;
|
|
32
|
+
this.constantPool[i] = { tag, value: str };
|
|
33
|
+
break;
|
|
34
|
+
case 3: // Integer
|
|
35
|
+
case 4: // Float
|
|
36
|
+
this.offset += 4;
|
|
37
|
+
break;
|
|
38
|
+
case 5: // Long
|
|
39
|
+
case 6: // Double
|
|
40
|
+
this.offset += 8;
|
|
41
|
+
i++; // Takes two slots
|
|
42
|
+
break;
|
|
43
|
+
case 7: // Class
|
|
44
|
+
const nameIndex = this.readU2();
|
|
45
|
+
this.constantPool[i] = { tag, nameIndex };
|
|
46
|
+
break;
|
|
47
|
+
case 8: // String
|
|
48
|
+
this.offset += 2;
|
|
49
|
+
break;
|
|
50
|
+
case 9: // Fieldref
|
|
51
|
+
case 10: // Methodref
|
|
52
|
+
case 11: // InterfaceMethodref
|
|
53
|
+
this.offset += 4;
|
|
54
|
+
break;
|
|
55
|
+
case 12: // NameAndType
|
|
56
|
+
this.offset += 4;
|
|
57
|
+
break;
|
|
58
|
+
case 15: // MethodHandle
|
|
59
|
+
this.offset += 3;
|
|
60
|
+
break;
|
|
61
|
+
case 16: // MethodType
|
|
62
|
+
this.offset += 2;
|
|
63
|
+
break;
|
|
64
|
+
case 17: // Dynamic
|
|
65
|
+
case 18: // InvokeDynamic
|
|
66
|
+
this.offset += 4;
|
|
67
|
+
break;
|
|
68
|
+
case 19: // Module
|
|
69
|
+
case 20: // Package
|
|
70
|
+
this.offset += 2;
|
|
71
|
+
break;
|
|
72
|
+
default:
|
|
73
|
+
throw new Error(`Unknown constant pool tag: ${tag} at offset ${this.offset - 1}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
this.readU2(); // Access flags
|
|
77
|
+
const thisClassIndex = this.readU2();
|
|
78
|
+
const superClassIndex = this.readU2();
|
|
79
|
+
const className = this.resolveClass(thisClassIndex);
|
|
80
|
+
const superClass = superClassIndex === 0 ? undefined : this.resolveClass(superClassIndex);
|
|
81
|
+
const interfacesCount = this.readU2();
|
|
82
|
+
const interfaces = [];
|
|
83
|
+
for (let i = 0; i < interfacesCount; i++) {
|
|
84
|
+
const interfaceIndex = this.readU2();
|
|
85
|
+
interfaces.push(this.resolveClass(interfaceIndex));
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
className: className.replace(/\//g, '.'),
|
|
89
|
+
superClass: superClass ? superClass.replace(/\//g, '.') : undefined,
|
|
90
|
+
interfaces: interfaces.map(i => i.replace(/\//g, '.'))
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
resolveClass(index) {
|
|
94
|
+
const entry = this.constantPool[index];
|
|
95
|
+
if (!entry || entry.tag !== 7) {
|
|
96
|
+
// Fallback or error?
|
|
97
|
+
return "Unknown";
|
|
98
|
+
}
|
|
99
|
+
const nameEntry = this.constantPool[entry.nameIndex];
|
|
100
|
+
if (!nameEntry || nameEntry.tag !== 1) {
|
|
101
|
+
return "Unknown";
|
|
102
|
+
}
|
|
103
|
+
return nameEntry.value;
|
|
104
|
+
}
|
|
105
|
+
readU1() {
|
|
106
|
+
const val = this.buffer.readUInt8(this.offset);
|
|
107
|
+
this.offset += 1;
|
|
108
|
+
return val;
|
|
109
|
+
}
|
|
110
|
+
readU2() {
|
|
111
|
+
const val = this.buffer.readUInt16BE(this.offset);
|
|
112
|
+
this.offset += 2;
|
|
113
|
+
return val;
|
|
114
|
+
}
|
|
115
|
+
readU4() {
|
|
116
|
+
const val = this.buffer.readUInt32BE(this.offset);
|
|
117
|
+
this.offset += 4;
|
|
118
|
+
return val;
|
|
119
|
+
}
|
|
120
|
+
}
|
package/build/config.js
CHANGED
|
@@ -2,11 +2,13 @@ import fs from 'fs/promises';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import xml2js from 'xml2js';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
5
6
|
export class Config {
|
|
6
7
|
static instance;
|
|
7
8
|
localRepository = "";
|
|
8
9
|
javaBinary = "java";
|
|
9
10
|
includedPackages = ["*"];
|
|
11
|
+
cfrPath = null;
|
|
10
12
|
constructor() { }
|
|
11
13
|
static async getInstance() {
|
|
12
14
|
if (!Config.instance) {
|
|
@@ -62,10 +64,26 @@ export class Config {
|
|
|
62
64
|
.map(p => p.trim())
|
|
63
65
|
.filter(p => p.length > 0);
|
|
64
66
|
}
|
|
67
|
+
// Load CFR Path
|
|
68
|
+
if (process.env.MAVEN_INDEXER_CFR_PATH) {
|
|
69
|
+
this.cfrPath = process.env.MAVEN_INDEXER_CFR_PATH;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// Fallback to default bundled lib
|
|
73
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
74
|
+
const __dirname = path.dirname(__filename);
|
|
75
|
+
this.cfrPath = path.resolve(__dirname, '../lib/cfr-0.152.jar');
|
|
76
|
+
}
|
|
65
77
|
// Log to stderr so it doesn't interfere with MCP protocol on stdout
|
|
66
78
|
console.error(`Using local repository: ${this.localRepository}`);
|
|
67
79
|
console.error(`Using Java binary: ${this.javaBinary}`);
|
|
68
80
|
console.error(`Included packages: ${JSON.stringify(this.includedPackages)}`);
|
|
81
|
+
if (this.cfrPath) {
|
|
82
|
+
console.error(`Using CFR jar: ${this.cfrPath}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
getCfrJarPath() {
|
|
86
|
+
return this.cfrPath;
|
|
69
87
|
}
|
|
70
88
|
getJavapPath() {
|
|
71
89
|
if (this.javaBinary === 'java')
|
package/build/db/index.js
CHANGED
|
@@ -17,6 +17,17 @@ export class DB {
|
|
|
17
17
|
return DB.instance;
|
|
18
18
|
}
|
|
19
19
|
initSchema() {
|
|
20
|
+
// Register REGEXP function
|
|
21
|
+
this.db.function('regexp', { deterministic: true }, (regex, text) => {
|
|
22
|
+
if (!regex || !text)
|
|
23
|
+
return 0;
|
|
24
|
+
try {
|
|
25
|
+
return new RegExp(regex).test(text) ? 1 : 0;
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
20
31
|
this.db.exec(`
|
|
21
32
|
CREATE TABLE IF NOT EXISTS artifacts (
|
|
22
33
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -25,6 +36,7 @@ export class DB {
|
|
|
25
36
|
version TEXT NOT NULL,
|
|
26
37
|
abspath TEXT NOT NULL,
|
|
27
38
|
has_source INTEGER DEFAULT 0,
|
|
39
|
+
is_indexed INTEGER DEFAULT 0,
|
|
28
40
|
UNIQUE(group_id, artifact_id, version)
|
|
29
41
|
);
|
|
30
42
|
|
|
@@ -34,12 +46,26 @@ export class DB {
|
|
|
34
46
|
simple_name, -- Just the class name
|
|
35
47
|
tokenize="trigram"
|
|
36
48
|
);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
|
|
50
|
+
CREATE TABLE IF NOT EXISTS inheritance (
|
|
51
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
52
|
+
artifact_id INTEGER NOT NULL,
|
|
53
|
+
class_name TEXT NOT NULL,
|
|
54
|
+
parent_class_name TEXT NOT NULL,
|
|
55
|
+
type TEXT NOT NULL
|
|
41
56
|
);
|
|
57
|
+
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_inheritance_parent ON inheritance(parent_class_name);
|
|
59
|
+
|
|
60
|
+
-- Cleanup old table if exists
|
|
61
|
+
DROP TABLE IF EXISTS indexed_artifacts;
|
|
42
62
|
`);
|
|
63
|
+
try {
|
|
64
|
+
this.db.exec('ALTER TABLE artifacts ADD COLUMN is_indexed INTEGER DEFAULT 0');
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
// Column likely already exists
|
|
68
|
+
}
|
|
43
69
|
}
|
|
44
70
|
getDb() {
|
|
45
71
|
return this.db;
|
package/build/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
4
|
import path from 'path';
|
|
5
|
+
import { z } from "zod";
|
|
6
6
|
import { Indexer } from "./indexer.js";
|
|
7
7
|
import { SourceParser } from "./source_parser.js";
|
|
8
|
-
const server = new
|
|
8
|
+
const server = new McpServer({
|
|
9
9
|
name: "maven-indexer",
|
|
10
10
|
version: "1.0.0",
|
|
11
11
|
}, {
|
|
@@ -20,155 +20,152 @@ indexer.index().then(() => {
|
|
|
20
20
|
// Start watching for changes after initial index
|
|
21
21
|
return indexer.startWatch();
|
|
22
22
|
}).catch(err => console.error("Initial indexing failed:", err));
|
|
23
|
-
server.
|
|
23
|
+
server.registerTool("search_artifacts", {
|
|
24
|
+
description: "Search for Maven artifacts (libraries) in the local repository by coordinate (groupId, artifactId) or keyword. Use this to find available versions of a library.",
|
|
25
|
+
inputSchema: z.object({
|
|
26
|
+
query: z.string().describe("Search query (groupId, artifactId, or keyword)"),
|
|
27
|
+
}),
|
|
28
|
+
}, async ({ query }) => {
|
|
29
|
+
const matches = indexer.search(query);
|
|
30
|
+
// Limit results to avoid overflow
|
|
31
|
+
const limitedMatches = matches.slice(0, 20);
|
|
32
|
+
const text = limitedMatches.length > 0
|
|
33
|
+
? limitedMatches.map(a => `[ID: ${a.id}] ${a.groupId}:${a.artifactId}:${a.version} (Has Source: ${a.hasSource})`).join("\n")
|
|
34
|
+
: "No artifacts found matching the query.";
|
|
24
35
|
return {
|
|
25
|
-
|
|
36
|
+
content: [
|
|
26
37
|
{
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
inputSchema: {
|
|
30
|
-
type: "object",
|
|
31
|
-
properties: {
|
|
32
|
-
query: {
|
|
33
|
-
type: "string",
|
|
34
|
-
description: "Search query (groupId, artifactId, or keyword)",
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
required: ["query"],
|
|
38
|
-
},
|
|
38
|
+
type: "text",
|
|
39
|
+
text: `Found ${matches.length} matches${matches.length > 20 ? ' (showing first 20)' : ''}:\n${text}`,
|
|
39
40
|
},
|
|
40
|
-
{
|
|
41
|
-
name: "search_classes",
|
|
42
|
-
description: "Search for Java classes. Can be used to find classes by name or to find classes for a specific purpose (by searching keywords in class names).",
|
|
43
|
-
inputSchema: {
|
|
44
|
-
type: "object",
|
|
45
|
-
properties: {
|
|
46
|
-
className: {
|
|
47
|
-
type: "string",
|
|
48
|
-
description: "Fully qualified class name, partial name, or keywords describing the class purpose (e.g. 'JsonToXml').",
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
required: ["className"],
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
name: "get_class_details",
|
|
56
|
-
description: "Get details about a specific class from an artifact, including method signatures and javadocs (if source is available).",
|
|
57
|
-
inputSchema: {
|
|
58
|
-
type: "object",
|
|
59
|
-
properties: {
|
|
60
|
-
className: {
|
|
61
|
-
type: "string",
|
|
62
|
-
description: "Fully qualified class name",
|
|
63
|
-
},
|
|
64
|
-
artifactId: {
|
|
65
|
-
type: "number",
|
|
66
|
-
description: "The internal ID of the artifact (returned by search_classes)",
|
|
67
|
-
},
|
|
68
|
-
type: {
|
|
69
|
-
type: "string",
|
|
70
|
-
enum: ["signatures", "docs", "source"],
|
|
71
|
-
description: "Type of detail to retrieve: 'signatures' (methods), 'docs' (javadocs + methods), 'source' (full source code).",
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
required: ["className", "artifactId", "type"],
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
name: "refresh_index",
|
|
79
|
-
description: "Trigger a re-scan of the Maven repository",
|
|
80
|
-
inputSchema: {
|
|
81
|
-
type: "object",
|
|
82
|
-
properties: {},
|
|
83
|
-
},
|
|
84
|
-
}
|
|
85
41
|
],
|
|
86
42
|
};
|
|
87
43
|
});
|
|
88
|
-
server.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
44
|
+
server.registerTool("search_classes", {
|
|
45
|
+
description: "Search for Java classes in the local Maven repository. Use this to find classes from external dependencies/libraries when their source code is not available in the current workspace. You can search by class name (e.g. 'StringUtils') or keywords (e.g. 'Json').",
|
|
46
|
+
inputSchema: z.object({
|
|
47
|
+
className: z.string().describe("Fully qualified class name, partial name, or keywords describing the class purpose (e.g. 'JsonToXml')."),
|
|
48
|
+
}),
|
|
49
|
+
}, async ({ className }) => {
|
|
50
|
+
const matches = indexer.searchClass(className);
|
|
51
|
+
const text = matches.length > 0
|
|
52
|
+
? matches.map(m => {
|
|
53
|
+
// Group by artifact ID to allow easy selection
|
|
54
|
+
const artifacts = m.artifacts.slice(0, 5).map(a => `[ID: ${a.id}] ${a.groupId}:${a.artifactId}:${a.version}${a.hasSource ? ' (Has Source)' : ''}`).join("\n ");
|
|
55
|
+
const more = m.artifacts.length > 5 ? `\n ... (${m.artifacts.length - 5} more versions)` : '';
|
|
56
|
+
return `Class: ${m.className}\n ${artifacts}${more}`;
|
|
57
|
+
}).join("\n\n")
|
|
58
|
+
: "No classes found matching the query. Try different keywords.";
|
|
59
|
+
return {
|
|
60
|
+
content: [{ type: "text", text }]
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
server.registerTool("search_implementations", {
|
|
64
|
+
description: "Search for classes that implement a specific interface or extend a specific class. This is useful for finding implementations of SPIs or base classes.",
|
|
65
|
+
inputSchema: z.object({
|
|
66
|
+
className: z.string().describe("Fully qualified class name of the interface or base class (e.g. 'java.util.List')"),
|
|
67
|
+
}),
|
|
68
|
+
}, async ({ className }) => {
|
|
69
|
+
const matches = indexer.searchImplementations(className);
|
|
70
|
+
const text = matches.length > 0
|
|
71
|
+
? matches.map(m => {
|
|
72
|
+
const artifacts = m.artifacts.slice(0, 5).map(a => `[ID: ${a.id}] ${a.groupId}:${a.artifactId}:${a.version}`).join("\n ");
|
|
73
|
+
const more = m.artifacts.length > 5 ? `\n ... (${m.artifacts.length - 5} more versions)` : '';
|
|
74
|
+
return `Implementation: ${m.className}\n ${artifacts}${more}`;
|
|
75
|
+
}).join("\n\n")
|
|
76
|
+
: `No implementations found for ${className}. Ensure the index is up to date and the class name is correct.`;
|
|
77
|
+
return {
|
|
78
|
+
content: [{ type: "text", text }]
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
server.registerTool("get_class_details", {
|
|
82
|
+
description: "Get details about a specific class from a Maven artifact. Use this to inspect external classes where source code is missing. It can provide method signatures (using javap), Javadocs, or even decompiled source code if the original source jar is unavailable.",
|
|
83
|
+
inputSchema: z.object({
|
|
84
|
+
className: z.string().describe("Fully qualified class name"),
|
|
85
|
+
artifactId: z.number().describe("The internal ID of the artifact (returned by search_classes)"),
|
|
86
|
+
type: z.enum(["signatures", "docs", "source"]).describe("Type of detail to retrieve: 'signatures' (methods), 'docs' (javadocs + methods), 'source' (full source code)."),
|
|
87
|
+
}),
|
|
88
|
+
}, async ({ className, artifactId, type }) => {
|
|
89
|
+
const artifact = indexer.getArtifactById(artifactId);
|
|
90
|
+
if (!artifact) {
|
|
91
|
+
return { content: [{ type: "text", text: "Artifact not found." }] };
|
|
120
92
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (type === 'signatures') {
|
|
131
|
-
// Use Main JAR for signatures (javap)
|
|
132
|
-
jarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
// Use Source JAR for docs and full source
|
|
136
|
-
if (!artifact.hasSource) {
|
|
137
|
-
return { content: [{ type: "text", text: `Artifact ${artifact.groupId}:${artifact.artifactId}:${artifact.version} does not have a sources jar available locally.` }] };
|
|
138
|
-
}
|
|
139
|
-
jarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}-sources.jar`);
|
|
140
|
-
}
|
|
141
|
-
try {
|
|
142
|
-
const detail = await SourceParser.getClassDetail(jarPath, className, type);
|
|
143
|
-
if (!detail) {
|
|
144
|
-
return { content: [{ type: "text", text: `Class ${className} not found in ${type === 'signatures' ? 'artifact' : 'sources'} of ${artifact.artifactId}.` }] };
|
|
93
|
+
let detail = null;
|
|
94
|
+
let usedDecompilation = false;
|
|
95
|
+
let lastError = "";
|
|
96
|
+
// 1. If requesting source/docs, try Source JAR first
|
|
97
|
+
if (type === 'source' || type === 'docs') {
|
|
98
|
+
if (artifact.hasSource) {
|
|
99
|
+
const sourceJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}-sources.jar`);
|
|
100
|
+
try {
|
|
101
|
+
detail = await SourceParser.getClassDetail(sourceJarPath, className, type);
|
|
145
102
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
103
|
+
catch (e) {
|
|
104
|
+
// Ignore error and fallthrough to main jar (decompilation)
|
|
105
|
+
lastError = e.message;
|
|
149
106
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
107
|
+
}
|
|
108
|
+
// If not found in source jar (or no source jar), try main jar (decompilation)
|
|
109
|
+
if (!detail) {
|
|
110
|
+
const mainJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
|
|
111
|
+
try {
|
|
112
|
+
// SourceParser will try to decompile if source file not found in jar
|
|
113
|
+
detail = await SourceParser.getClassDetail(mainJarPath, className, type);
|
|
114
|
+
if (detail && detail.source) {
|
|
115
|
+
usedDecompilation = true;
|
|
156
116
|
}
|
|
157
117
|
}
|
|
158
|
-
|
|
118
|
+
catch (e) {
|
|
119
|
+
console.error(`Decompilation/MainJar access failed: ${e.message}`);
|
|
120
|
+
lastError = e.message;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
// Signatures -> Use Main JAR
|
|
126
|
+
const mainJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
|
|
127
|
+
try {
|
|
128
|
+
detail = await SourceParser.getClassDetail(mainJarPath, className, type);
|
|
159
129
|
}
|
|
160
130
|
catch (e) {
|
|
161
|
-
|
|
131
|
+
lastError = e.message;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
if (!detail) {
|
|
136
|
+
const debugInfo = `Artifact path: ${artifact.abspath}, hasSource: ${artifact.hasSource}`;
|
|
137
|
+
const errorMsg = lastError ? `\nLast error: ${lastError}` : "";
|
|
138
|
+
return { content: [{ type: "text", text: `Class ${className} not found in artifact ${artifact.artifactId}. \nDebug info: ${debugInfo}${errorMsg}` }] };
|
|
162
139
|
}
|
|
140
|
+
let resultText = `Class: ${detail.className}\n\n`;
|
|
141
|
+
if (usedDecompilation) {
|
|
142
|
+
resultText += "*Source code decompiled from binary class file.*\n\n";
|
|
143
|
+
}
|
|
144
|
+
if (type === 'source') {
|
|
145
|
+
resultText += "```java\n" + detail.source + "\n```";
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
if (detail.doc) {
|
|
149
|
+
resultText += "Documentation:\n" + detail.doc + "\n\n";
|
|
150
|
+
}
|
|
151
|
+
if (detail.signatures) {
|
|
152
|
+
resultText += "Methods:\n" + detail.signatures.join("\n") + "\n";
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { content: [{ type: "text", text: resultText }] };
|
|
163
156
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
indexer.index().catch(console.error);
|
|
167
|
-
return {
|
|
168
|
-
content: [{ type: "text", text: "Index refresh started." }]
|
|
169
|
-
};
|
|
157
|
+
catch (e) {
|
|
158
|
+
return { content: [{ type: "text", text: `Error reading source: ${e.message}` }] };
|
|
170
159
|
}
|
|
171
|
-
|
|
160
|
+
});
|
|
161
|
+
server.registerTool("refresh_index", {
|
|
162
|
+
description: "Trigger a re-scan of the Maven repository. This will re-index all artifacts.",
|
|
163
|
+
}, async () => {
|
|
164
|
+
// Re-run index
|
|
165
|
+
indexer.refresh().catch(console.error);
|
|
166
|
+
return {
|
|
167
|
+
content: [{ type: "text", text: "Index refresh started. All artifacts will be re-indexed." }]
|
|
168
|
+
};
|
|
172
169
|
});
|
|
173
170
|
const transport = new StdioServerTransport();
|
|
174
171
|
await server.connect(transport);
|
package/build/indexer.js
CHANGED
|
@@ -5,6 +5,11 @@ import yauzl from 'yauzl';
|
|
|
5
5
|
import chokidar from 'chokidar';
|
|
6
6
|
import { Config } from './config.js';
|
|
7
7
|
import { DB } from './db/index.js';
|
|
8
|
+
import { ClassParser } from './class_parser.js';
|
|
9
|
+
/**
|
|
10
|
+
* Singleton class responsible for indexing Maven artifacts.
|
|
11
|
+
* It scans the local repository, watches for changes, and indexes Java classes.
|
|
12
|
+
*/
|
|
8
13
|
export class Indexer {
|
|
9
14
|
static instance;
|
|
10
15
|
isIndexing = false;
|
|
@@ -17,6 +22,10 @@ export class Indexer {
|
|
|
17
22
|
}
|
|
18
23
|
return Indexer.instance;
|
|
19
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Starts watching the local repository for changes.
|
|
27
|
+
* Debounces changes to trigger re-indexing.
|
|
28
|
+
*/
|
|
20
29
|
async startWatch() {
|
|
21
30
|
const config = await Config.getInstance();
|
|
22
31
|
const repoPath = config.localRepository;
|
|
@@ -52,59 +61,76 @@ export class Indexer {
|
|
|
52
61
|
onChange();
|
|
53
62
|
});
|
|
54
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Forces a full re-index of the repository.
|
|
66
|
+
*/
|
|
67
|
+
async refresh() {
|
|
68
|
+
const db = DB.getInstance();
|
|
69
|
+
console.error("Refreshing index...");
|
|
70
|
+
db.transaction(() => {
|
|
71
|
+
db.prepare('UPDATE artifacts SET is_indexed = 0').run();
|
|
72
|
+
db.prepare('DELETE FROM classes_fts').run();
|
|
73
|
+
db.prepare('DELETE FROM inheritance').run();
|
|
74
|
+
});
|
|
75
|
+
return this.index();
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Main indexing process.
|
|
79
|
+
* 1. Scans the file system for Maven artifacts.
|
|
80
|
+
* 2. Synchronizes the database with found artifacts.
|
|
81
|
+
* 3. Indexes classes for artifacts that haven't been indexed yet.
|
|
82
|
+
*/
|
|
55
83
|
async index() {
|
|
56
84
|
if (this.isIndexing)
|
|
57
85
|
return;
|
|
58
86
|
this.isIndexing = true;
|
|
59
87
|
console.error("Starting index...");
|
|
60
|
-
const config = await Config.getInstance();
|
|
61
|
-
const repoPath = config.localRepository;
|
|
62
|
-
const db = DB.getInstance();
|
|
63
|
-
if (!repoPath) {
|
|
64
|
-
console.error("No local repository path found.");
|
|
65
|
-
this.isIndexing = false;
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
88
|
try {
|
|
89
|
+
const config = await Config.getInstance();
|
|
90
|
+
const repoPath = config.localRepository;
|
|
91
|
+
const db = DB.getInstance();
|
|
92
|
+
if (!repoPath) {
|
|
93
|
+
console.error("No local repository path found.");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
69
96
|
// 1. Scan for artifacts
|
|
70
97
|
console.error("Scanning repository structure...");
|
|
71
98
|
const artifacts = await this.scanRepository(repoPath);
|
|
72
99
|
console.error(`Found ${artifacts.length} artifacts on disk.`);
|
|
73
100
|
// 2. Persist artifacts and determine what needs indexing
|
|
74
|
-
|
|
101
|
+
// We use is_indexed = 0 for new artifacts.
|
|
102
|
+
const insertArtifact = db.prepare(`
|
|
103
|
+
INSERT OR IGNORE INTO artifacts (group_id, artifact_id, version, abspath, has_source, is_indexed)
|
|
104
|
+
VALUES (@groupId, @artifactId, @version, @abspath, @hasSource, 0)
|
|
105
|
+
`);
|
|
106
|
+
// Use a transaction only for the batch insert of artifacts
|
|
75
107
|
db.transaction(() => {
|
|
76
|
-
const insertArtifact = db.prepare(`
|
|
77
|
-
INSERT OR IGNORE INTO artifacts (group_id, artifact_id, version, abspath, has_source)
|
|
78
|
-
VALUES (@groupId, @artifactId, @version, @abspath, @hasSource)
|
|
79
|
-
`);
|
|
80
|
-
const selectId = db.prepare(`
|
|
81
|
-
SELECT id FROM artifacts
|
|
82
|
-
WHERE group_id = @groupId AND artifact_id = @artifactId AND version = @version
|
|
83
|
-
`);
|
|
84
|
-
const checkIndexed = db.prepare(`
|
|
85
|
-
SELECT 1 FROM indexed_artifacts WHERE artifact_id = ?
|
|
86
|
-
`);
|
|
87
108
|
for (const art of artifacts) {
|
|
88
109
|
insertArtifact.run({
|
|
89
110
|
...art,
|
|
90
111
|
hasSource: art.hasSource ? 1 : 0
|
|
91
112
|
});
|
|
92
|
-
const row = selectId.get({
|
|
93
|
-
groupId: art.groupId,
|
|
94
|
-
artifactId: art.artifactId,
|
|
95
|
-
version: art.version
|
|
96
|
-
});
|
|
97
|
-
if (row) {
|
|
98
|
-
art.id = row.id;
|
|
99
|
-
const isIndexed = checkIndexed.get(art.id);
|
|
100
|
-
if (!isIndexed) {
|
|
101
|
-
artifactsToIndex.push(art);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
113
|
}
|
|
105
114
|
});
|
|
115
|
+
// Check if we need to backfill inheritance data (migration)
|
|
116
|
+
const inheritanceCount = db.prepare('SELECT COUNT(*) as count FROM inheritance').get();
|
|
117
|
+
const indexedArtifactsCount = db.prepare('SELECT COUNT(*) as count FROM artifacts WHERE is_indexed = 1').get();
|
|
118
|
+
if (inheritanceCount.count === 0 && indexedArtifactsCount.count > 0) {
|
|
119
|
+
console.error("Detected missing inheritance data. Forcing re-index of classes...");
|
|
120
|
+
db.transaction(() => {
|
|
121
|
+
db.prepare('UPDATE artifacts SET is_indexed = 0').run();
|
|
122
|
+
db.prepare('DELETE FROM classes_fts').run();
|
|
123
|
+
// inheritance is already empty
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// 3. Find artifacts that need indexing (is_indexed = 0)
|
|
127
|
+
const artifactsToIndex = db.prepare(`
|
|
128
|
+
SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
|
|
129
|
+
FROM artifacts
|
|
130
|
+
WHERE is_indexed = 0
|
|
131
|
+
`).all();
|
|
106
132
|
console.error(`${artifactsToIndex.length} artifacts need indexing.`);
|
|
107
|
-
//
|
|
133
|
+
// 4. Scan JARs for classes and update DB
|
|
108
134
|
const CHUNK_SIZE = 50;
|
|
109
135
|
let processedCount = 0;
|
|
110
136
|
for (let i = 0; i < artifactsToIndex.length; i += CHUNK_SIZE) {
|
|
@@ -124,6 +150,9 @@ export class Indexer {
|
|
|
124
150
|
this.isIndexing = false;
|
|
125
151
|
}
|
|
126
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Recursively scans a directory for Maven artifacts (POM files).
|
|
155
|
+
*/
|
|
127
156
|
async scanRepository(rootDir) {
|
|
128
157
|
const results = [];
|
|
129
158
|
const scanDir = async (dir) => {
|
|
@@ -168,6 +197,10 @@ export class Indexer {
|
|
|
168
197
|
await scanDir(rootDir);
|
|
169
198
|
return results;
|
|
170
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Extracts classes from the artifact's JAR and indexes them.
|
|
202
|
+
* Updates the 'is_indexed' flag upon completion.
|
|
203
|
+
*/
|
|
171
204
|
async indexArtifactClasses(artifact) {
|
|
172
205
|
const jarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
|
|
173
206
|
const db = DB.getInstance();
|
|
@@ -178,30 +211,56 @@ export class Indexer {
|
|
|
178
211
|
catch {
|
|
179
212
|
// If jar missing, mark as indexed so we don't retry endlessly?
|
|
180
213
|
// Or maybe it's a pom-only artifact.
|
|
181
|
-
db.prepare('
|
|
214
|
+
db.prepare('UPDATE artifacts SET is_indexed = 1 WHERE id = ?').run(artifact.id);
|
|
182
215
|
return;
|
|
183
216
|
}
|
|
184
217
|
return new Promise((resolve) => {
|
|
185
218
|
yauzl.open(jarPath, { lazyEntries: true, autoClose: true }, (err, zipfile) => {
|
|
186
219
|
if (err || !zipfile) {
|
|
220
|
+
// If we can't open it, maybe it's corrupt. Skip for now.
|
|
187
221
|
resolve();
|
|
188
222
|
return;
|
|
189
223
|
}
|
|
190
224
|
const classes = [];
|
|
225
|
+
const inheritance = [];
|
|
191
226
|
zipfile.readEntry();
|
|
192
227
|
zipfile.on('entry', (entry) => {
|
|
193
228
|
if (entry.fileName.endsWith('.class')) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
// Filter by includedPackages
|
|
199
|
-
if (this.isPackageIncluded(className, config.includedPackages)) {
|
|
200
|
-
classes.push(className);
|
|
229
|
+
zipfile.openReadStream(entry, (err, readStream) => {
|
|
230
|
+
if (err || !readStream) {
|
|
231
|
+
zipfile.readEntry();
|
|
232
|
+
return;
|
|
201
233
|
}
|
|
202
|
-
|
|
234
|
+
const chunks = [];
|
|
235
|
+
readStream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
|
236
|
+
readStream.on('end', () => {
|
|
237
|
+
const buffer = Buffer.concat(chunks);
|
|
238
|
+
try {
|
|
239
|
+
const info = ClassParser.parse(buffer);
|
|
240
|
+
// Simple check to avoid module-info or invalid names
|
|
241
|
+
if (!info.className.includes('$') && info.className.length > 0) {
|
|
242
|
+
// Filter by includedPackages
|
|
243
|
+
if (this.isPackageIncluded(info.className, config.includedPackages)) {
|
|
244
|
+
classes.push(info.className);
|
|
245
|
+
if (info.superClass && info.superClass !== 'java.lang.Object') {
|
|
246
|
+
inheritance.push({ className: info.className, parent: info.superClass, type: 'extends' });
|
|
247
|
+
}
|
|
248
|
+
for (const iface of info.interfaces) {
|
|
249
|
+
inheritance.push({ className: info.className, parent: iface, type: 'implements' });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (e) {
|
|
255
|
+
// console.error(`Failed to parse ${entry.fileName}`, e);
|
|
256
|
+
}
|
|
257
|
+
zipfile.readEntry();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
zipfile.readEntry();
|
|
203
263
|
}
|
|
204
|
-
zipfile.readEntry();
|
|
205
264
|
});
|
|
206
265
|
zipfile.on('end', () => {
|
|
207
266
|
// Batch insert classes
|
|
@@ -210,12 +269,20 @@ export class Indexer {
|
|
|
210
269
|
const insertClass = db.prepare(`
|
|
211
270
|
INSERT INTO classes_fts (artifact_id, class_name, simple_name)
|
|
212
271
|
VALUES (?, ?, ?)
|
|
272
|
+
`);
|
|
273
|
+
const insertInheritance = db.prepare(`
|
|
274
|
+
INSERT INTO inheritance (artifact_id, class_name, parent_class_name, type)
|
|
275
|
+
VALUES (?, ?, ?, ?)
|
|
213
276
|
`);
|
|
214
277
|
for (const cls of classes) {
|
|
215
278
|
const simpleName = cls.split('.').pop() || cls;
|
|
216
279
|
insertClass.run(artifact.id, cls, simpleName);
|
|
217
280
|
}
|
|
218
|
-
|
|
281
|
+
for (const item of inheritance) {
|
|
282
|
+
insertInheritance.run(artifact.id, item.className, item.parent, item.type);
|
|
283
|
+
}
|
|
284
|
+
// Mark as indexed
|
|
285
|
+
db.prepare('UPDATE artifacts SET is_indexed = 1 WHERE id = ?').run(artifact.id);
|
|
219
286
|
});
|
|
220
287
|
}
|
|
221
288
|
catch (e) {
|
|
@@ -229,6 +296,9 @@ export class Indexer {
|
|
|
229
296
|
});
|
|
230
297
|
});
|
|
231
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Checks if a class package is included in the configuration patterns.
|
|
301
|
+
*/
|
|
232
302
|
isPackageIncluded(className, patterns) {
|
|
233
303
|
if (patterns.length === 0 || (patterns.length === 1 && patterns[0] === '*'))
|
|
234
304
|
return true;
|
|
@@ -249,6 +319,9 @@ export class Indexer {
|
|
|
249
319
|
}
|
|
250
320
|
return false;
|
|
251
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Searches for artifacts by group ID or artifact ID.
|
|
324
|
+
*/
|
|
252
325
|
search(query) {
|
|
253
326
|
// Artifact coordinates search
|
|
254
327
|
const db = DB.getInstance();
|
|
@@ -260,25 +333,56 @@ export class Indexer {
|
|
|
260
333
|
`).all(`%${query}%`, `%${query}%`);
|
|
261
334
|
return rows;
|
|
262
335
|
}
|
|
336
|
+
/**
|
|
337
|
+
* Searches for classes matching the pattern.
|
|
338
|
+
* Uses Full-Text Search (FTS) for efficient matching.
|
|
339
|
+
*/
|
|
263
340
|
searchClass(classNamePattern) {
|
|
264
341
|
const db = DB.getInstance();
|
|
265
|
-
// Use FTS for smart matching
|
|
266
|
-
// If pattern has no spaces, we assume it's a prefix or exact match query
|
|
267
|
-
// "String" -> match "String" in simple_name or class_name
|
|
268
|
-
const escapedPattern = classNamePattern.replace(/"/g, '""');
|
|
269
|
-
const safeQuery = classNamePattern.replace(/[^a-zA-Z0-9]/g, ' ').trim();
|
|
270
|
-
const query = safeQuery.length > 0
|
|
271
|
-
? `"${escapedPattern}"* OR ${safeQuery}`
|
|
272
|
-
: `"${escapedPattern}"*`;
|
|
273
342
|
try {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
343
|
+
let rows = [];
|
|
344
|
+
if (classNamePattern.startsWith('regex:')) {
|
|
345
|
+
// Regex search
|
|
346
|
+
const regex = classNamePattern.substring(6);
|
|
347
|
+
rows = db.prepare(`
|
|
348
|
+
SELECT c.class_name, c.simple_name, a.id, a.group_id, a.artifact_id, a.version, a.abspath, a.has_source
|
|
349
|
+
FROM classes_fts c
|
|
350
|
+
JOIN artifacts a ON c.artifact_id = a.id
|
|
351
|
+
WHERE c.class_name REGEXP ? OR c.simple_name REGEXP ?
|
|
352
|
+
LIMIT 100
|
|
353
|
+
`).all(regex, regex);
|
|
354
|
+
}
|
|
355
|
+
else if (classNamePattern.includes('*') || classNamePattern.includes('?')) {
|
|
356
|
+
// Glob-style search (using LIKE for standard wildcards)
|
|
357
|
+
// Convert glob wildcards to SQL wildcards if needed, or just rely on user knowing %/_
|
|
358
|
+
// But standard glob is * and ?
|
|
359
|
+
const likePattern = classNamePattern.replace(/\*/g, '%').replace(/\?/g, '_');
|
|
360
|
+
rows = db.prepare(`
|
|
361
|
+
SELECT c.class_name, c.simple_name, a.id, a.group_id, a.artifact_id, a.version, a.abspath, a.has_source
|
|
362
|
+
FROM classes_fts c
|
|
363
|
+
JOIN artifacts a ON c.artifact_id = a.id
|
|
364
|
+
WHERE c.class_name LIKE ? OR c.simple_name LIKE ?
|
|
365
|
+
LIMIT 100
|
|
366
|
+
`).all(likePattern, likePattern);
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
// Use FTS for smart matching
|
|
370
|
+
// If pattern has no spaces, we assume it's a prefix or exact match query
|
|
371
|
+
// "String" -> match "String" in simple_name or class_name
|
|
372
|
+
const escapedPattern = classNamePattern.replace(/"/g, '""');
|
|
373
|
+
const safeQuery = classNamePattern.replace(/[^a-zA-Z0-9]/g, ' ').trim();
|
|
374
|
+
const query = safeQuery.length > 0
|
|
375
|
+
? `"${escapedPattern}"* OR ${safeQuery}`
|
|
376
|
+
: `"${escapedPattern}"*`;
|
|
377
|
+
rows = db.prepare(`
|
|
378
|
+
SELECT c.class_name, c.simple_name, a.id, a.group_id, a.artifact_id, a.version, a.abspath, a.has_source
|
|
379
|
+
FROM classes_fts c
|
|
380
|
+
JOIN artifacts a ON c.artifact_id = a.id
|
|
381
|
+
WHERE c.classes_fts MATCH ?
|
|
382
|
+
ORDER BY rank
|
|
383
|
+
LIMIT 100
|
|
384
|
+
`).all(query);
|
|
385
|
+
}
|
|
282
386
|
// Group by class name
|
|
283
387
|
const resultMap = new Map();
|
|
284
388
|
for (const row of rows) {
|
|
@@ -305,6 +409,64 @@ export class Indexer {
|
|
|
305
409
|
return [];
|
|
306
410
|
}
|
|
307
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Searches for implementations/subclasses of a specific class/interface.
|
|
414
|
+
*/
|
|
415
|
+
searchImplementations(className) {
|
|
416
|
+
const db = DB.getInstance();
|
|
417
|
+
try {
|
|
418
|
+
console.error(`Searching implementations for ${className}...`);
|
|
419
|
+
// Debug: Check if we have any inheritance data at all
|
|
420
|
+
const count = db.prepare("SELECT count(*) as c FROM inheritance").get();
|
|
421
|
+
if (count.c === 0) {
|
|
422
|
+
console.error("WARNING: Inheritance table is empty!");
|
|
423
|
+
}
|
|
424
|
+
// Recursive search for all implementations/subclasses
|
|
425
|
+
const rows = db.prepare(`
|
|
426
|
+
WITH RECURSIVE hierarchy(class_name, artifact_id) AS (
|
|
427
|
+
SELECT class_name, artifact_id FROM inheritance WHERE parent_class_name = ?
|
|
428
|
+
UNION
|
|
429
|
+
SELECT i.class_name, i.artifact_id FROM inheritance i JOIN hierarchy h ON i.parent_class_name = h.class_name
|
|
430
|
+
)
|
|
431
|
+
SELECT DISTINCT h.class_name, a.id, a.group_id, a.artifact_id, a.version, a.abspath, a.has_source
|
|
432
|
+
FROM hierarchy h
|
|
433
|
+
JOIN artifacts a ON h.artifact_id = a.id
|
|
434
|
+
LIMIT 100
|
|
435
|
+
`).all(className);
|
|
436
|
+
console.error(`Searching implementations for ${className}: found ${rows.length} rows.`);
|
|
437
|
+
if (rows.length === 0) {
|
|
438
|
+
// Fallback: Try searching without recursion to see if direct children exist
|
|
439
|
+
const direct = db.prepare('SELECT count(*) as c FROM inheritance WHERE parent_class_name = ?').get(className);
|
|
440
|
+
console.error(`Direct implementations check for ${className}: ${direct.c}`);
|
|
441
|
+
}
|
|
442
|
+
const resultMap = new Map();
|
|
443
|
+
for (const row of rows) {
|
|
444
|
+
const art = {
|
|
445
|
+
id: row.id,
|
|
446
|
+
groupId: row.group_id,
|
|
447
|
+
artifactId: row.artifact_id,
|
|
448
|
+
version: row.version,
|
|
449
|
+
abspath: row.abspath,
|
|
450
|
+
hasSource: Boolean(row.has_source)
|
|
451
|
+
};
|
|
452
|
+
if (!resultMap.has(row.class_name)) {
|
|
453
|
+
resultMap.set(row.class_name, []);
|
|
454
|
+
}
|
|
455
|
+
resultMap.get(row.class_name).push(art);
|
|
456
|
+
}
|
|
457
|
+
return Array.from(resultMap.entries()).map(([className, artifacts]) => ({
|
|
458
|
+
className,
|
|
459
|
+
artifacts
|
|
460
|
+
}));
|
|
461
|
+
}
|
|
462
|
+
catch (e) {
|
|
463
|
+
console.error("Search implementations failed", e);
|
|
464
|
+
return [];
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Retrieves an artifact by its database ID.
|
|
469
|
+
*/
|
|
308
470
|
getArtifactById(id) {
|
|
309
471
|
const db = DB.getInstance();
|
|
310
472
|
const row = db.prepare(`
|
package/build/source_parser.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
1
3
|
import yauzl from 'yauzl';
|
|
2
4
|
import { exec } from 'child_process';
|
|
3
5
|
import { promisify } from 'util';
|
|
@@ -10,8 +12,12 @@ export class SourceParser {
|
|
|
10
12
|
}
|
|
11
13
|
// className: com.example.MyClass
|
|
12
14
|
// internalPath: com/example/MyClass.java
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
+
const basePath = className.replace(/\./g, '/');
|
|
16
|
+
const candidates = [
|
|
17
|
+
basePath + '.java',
|
|
18
|
+
basePath + '.kt'
|
|
19
|
+
];
|
|
20
|
+
const result = await new Promise((resolve, reject) => {
|
|
15
21
|
yauzl.open(jarPath, { lazyEntries: true, autoClose: true }, (err, zipfile) => {
|
|
16
22
|
if (err || !zipfile) {
|
|
17
23
|
resolve(null);
|
|
@@ -20,7 +26,7 @@ export class SourceParser {
|
|
|
20
26
|
let found = false;
|
|
21
27
|
zipfile.readEntry();
|
|
22
28
|
zipfile.on('entry', (entry) => {
|
|
23
|
-
if (entry.fileName
|
|
29
|
+
if (candidates.includes(entry.fileName)) {
|
|
24
30
|
found = true;
|
|
25
31
|
zipfile.openReadStream(entry, (err, readStream) => {
|
|
26
32
|
if (err || !readStream) {
|
|
@@ -45,6 +51,47 @@ export class SourceParser {
|
|
|
45
51
|
});
|
|
46
52
|
});
|
|
47
53
|
});
|
|
54
|
+
if (result) {
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
// Fallback to decompilation if source not found in this JAR
|
|
58
|
+
// Note: This works best if the provided jarPath is the MAIN jar.
|
|
59
|
+
// If it is the sources jar, decompilation will fail (as it doesn't contain .class files),
|
|
60
|
+
// returning null.
|
|
61
|
+
if (type === 'source' || type === 'docs') {
|
|
62
|
+
return this.decompileClass(jarPath, className, type);
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
static async decompileClass(jarPath, className, type) {
|
|
67
|
+
const config = await Config.getInstance();
|
|
68
|
+
const cfrPath = config.getCfrJarPath();
|
|
69
|
+
if (!cfrPath || !fs.existsSync(cfrPath)) {
|
|
70
|
+
throw new Error(`CFR jar not found at ${cfrPath}`);
|
|
71
|
+
}
|
|
72
|
+
// Use java -cp cfr.jar:target.jar org.benf.cfr.reader.Main className
|
|
73
|
+
const classpath = `${cfrPath}${path.delimiter}${jarPath}`;
|
|
74
|
+
const cmd = `java -cp "${classpath}" org.benf.cfr.reader.Main "${className}"`;
|
|
75
|
+
// console.error("Running decompile command:", cmd);
|
|
76
|
+
try {
|
|
77
|
+
const { stdout, stderr } = await execAsync(cmd);
|
|
78
|
+
if (!stdout && stderr) {
|
|
79
|
+
console.error(`CFR stderr for ${className}:`, stderr);
|
|
80
|
+
// If stderr has content but stdout is empty, it might be an error
|
|
81
|
+
throw new Error(`CFR stderr: ${stderr}`);
|
|
82
|
+
}
|
|
83
|
+
if (stdout) {
|
|
84
|
+
return this.parse(className, stdout, type);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
className,
|
|
88
|
+
source: stdout // Return as source
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
console.error(`CFR failed for ${className} in ${jarPath}:`, e.message);
|
|
93
|
+
throw e; // Rethrow to let caller handle
|
|
94
|
+
}
|
|
48
95
|
}
|
|
49
96
|
static async getSignaturesWithJavap(jarPath, className) {
|
|
50
97
|
try {
|
|
@@ -85,7 +132,8 @@ export class SourceParser {
|
|
|
85
132
|
let inDoc = false;
|
|
86
133
|
// Regex to match method signatures (public/protected, return type, name, args)
|
|
87
134
|
// ignoring annotations for simplicity
|
|
88
|
-
|
|
135
|
+
// Expanded to match decompiled code better (e.g., might not have throws, might be abstract)
|
|
136
|
+
const methodRegex = /^\s*(public|protected)\s+(?:[\w<>?\[\]]+\s+)+(\w+)\s*\([^)]*\)/;
|
|
89
137
|
for (const line of lines) {
|
|
90
138
|
const trimmed = line.trim();
|
|
91
139
|
// Javadoc extraction
|