maven-indexer-mcp 1.0.5 → 1.0.7
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 +163 -42
- package/build/artifact_resolver.js +109 -0
- package/build/config.js +72 -0
- package/build/index.js +197 -153
- package/build/indexer.js +154 -79
- package/package.json +3 -1
- package/build/reproduce_issue.js +0 -20
package/README.md
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
# Maven Indexer MCP Server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/maven-indexer-mcp)
|
|
4
|
+
[](https://github.com/tangcent/maven-indexer-mcp/actions/workflows/ci.yml)
|
|
5
|
+
|
|
6
|
+
A Model Context Protocol (MCP) server that indexes your local Maven repository (`~/.m2/repository`) and Gradle cache (
|
|
7
|
+
`~/.gradle/caches/modules-2/files-2.1`) to provide AI agents with tools to search for Java classes, method signatures,
|
|
8
|
+
and source code.
|
|
9
|
+
|
|
10
|
+
**Key Use Case**: While AI models are well-versed in popular public libraries (like Spring, Apache Commons, Guava), they
|
|
11
|
+
often struggle with:
|
|
12
|
+
|
|
13
|
+
1. **Internal Company Packages**: Private libraries that are not public.
|
|
14
|
+
2. **Non-Well-Known Public Packages**: Niche or less popular open-source libraries.
|
|
15
|
+
|
|
16
|
+
This server bridges that gap by allowing the AI to "read" your local dependencies, effectively giving it knowledge of
|
|
17
|
+
your private and obscure libraries.
|
|
4
18
|
|
|
5
19
|
## Features
|
|
6
20
|
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* **Efficient Persistence**: Uses SQLite to store the index, handling large repositories with minimal memory footprint.
|
|
21
|
+
* **Semantic Class Search**: Search for classes by name or purpose.
|
|
22
|
+
* **Inheritance Search**: Find all implementations of an interface or subclasses of a class.
|
|
23
|
+
* **On-Demand Analysis**: Extracts method signatures and Javadocs directly from JARs.
|
|
24
|
+
* **Source Code Retrieval**: Provides full source code if available.
|
|
25
|
+
* **Real-time Monitoring**: Automatically updates the index when repositories change.
|
|
13
26
|
|
|
14
27
|
## Getting Started
|
|
15
28
|
|
|
@@ -20,22 +33,116 @@ Add the following config to your MCP client:
|
|
|
20
33
|
"mcpServers": {
|
|
21
34
|
"maven-indexer": {
|
|
22
35
|
"command": "npx",
|
|
23
|
-
"args": [
|
|
36
|
+
"args": [
|
|
37
|
+
"-y",
|
|
38
|
+
"maven-indexer-mcp@latest"
|
|
39
|
+
]
|
|
24
40
|
}
|
|
25
41
|
}
|
|
26
42
|
}
|
|
27
43
|
```
|
|
28
44
|
|
|
29
|
-
This will automatically download and run the latest version of the server. It will auto-detect your Maven repository
|
|
45
|
+
This will automatically download and run the latest version of the server. It will auto-detect your Maven repository
|
|
46
|
+
location (usually `~/.m2/repository`) and Gradle cache.
|
|
30
47
|
|
|
31
|
-
###
|
|
48
|
+
### MCP Client configuration
|
|
49
|
+
|
|
50
|
+
<details>
|
|
51
|
+
<summary>Cline</summary>
|
|
52
|
+
|
|
53
|
+
Follow <a href="https://docs.cline.bot/mcp/configuring-mcp-servers">Cline's MCP guide</a> and use the config provided
|
|
54
|
+
above.
|
|
55
|
+
</details>
|
|
56
|
+
|
|
57
|
+
<details>
|
|
58
|
+
<summary>Codex</summary>
|
|
59
|
+
|
|
60
|
+
Follow the <a href="https://github.com/openai/codex/blob/main/docs/advanced.md#model-context-protocol-mcp">configure MCP
|
|
61
|
+
guide</a> using the standard config from above.
|
|
62
|
+
</details>
|
|
63
|
+
|
|
64
|
+
<details>
|
|
65
|
+
<summary>Cursor</summary>
|
|
66
|
+
|
|
67
|
+
**Click the button to install:**
|
|
68
|
+
|
|
69
|
+
[](https://cursor.com/en/install-mcp?name=maven-indexer&config=eyJjb21tYW5kIjoibnB4IC15IG1hdmVuLWluZGV4ZXItbWNwQGxhdGVzdCJ9)
|
|
70
|
+
|
|
71
|
+
**Or install manually:**
|
|
72
|
+
|
|
73
|
+
Go to `Cursor Settings` -> `MCP` -> `New MCP Server`. Use the config provided above.
|
|
74
|
+
|
|
75
|
+
</details>
|
|
76
|
+
|
|
77
|
+
<details>
|
|
78
|
+
<summary>JetBrains AI Assistant & Junie</summary>
|
|
79
|
+
|
|
80
|
+
Go to `Settings | Tools | AI Assistant | Model Context Protocol (MCP)` -> `Add`. Use the config provided above.
|
|
81
|
+
The same way `maven-indexer` can be configured for JetBrains Junie in `Settings | Tools | Junie | MCP Settings` ->`Add`.
|
|
82
|
+
Use the config provided above.
|
|
83
|
+
</details>
|
|
84
|
+
|
|
85
|
+
<details>
|
|
86
|
+
<summary>Kiro</summary>
|
|
87
|
+
|
|
88
|
+
In **Kiro Settings**, go to `Configure MCP` > `Open Workspace or User MCP Config` > Use the configuration snippet
|
|
89
|
+
provided above.
|
|
32
90
|
|
|
33
|
-
|
|
91
|
+
Or, from the IDE **Activity Bar** > `Kiro` > `MCP Servers` > `Click Open MCP Config`. Use the configuration snippet
|
|
92
|
+
provided above.
|
|
34
93
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
94
|
+
</details>
|
|
95
|
+
|
|
96
|
+
<details>
|
|
97
|
+
<summary>Qoder</summary>
|
|
98
|
+
|
|
99
|
+
In **Qoder Settings**, go to `MCP Server` > `+ Add` > Use the configuration snippet provided above.
|
|
100
|
+
|
|
101
|
+
Alternatively, follow the <a href="https://docs.qoder.com/user-guide/chat/model-context-protocol">MCP guide</a> and use
|
|
102
|
+
the standard config from above.
|
|
103
|
+
|
|
104
|
+
</details>
|
|
105
|
+
|
|
106
|
+
<details>
|
|
107
|
+
<summary>Trae</summary>
|
|
108
|
+
|
|
109
|
+
Go to `Settings` -> `MCP` -> `+ Add` -> `Add Manually` to add an MCP Server. Use the config provided above.
|
|
110
|
+
</details>
|
|
111
|
+
|
|
112
|
+
<details>
|
|
113
|
+
<summary>Windsurf</summary>
|
|
114
|
+
|
|
115
|
+
Follow the <a href="https://docs.windsurf.com/windsurf/cascade/mcp#mcp-config-json">configure MCP guide</a>
|
|
116
|
+
using the standard config from above.
|
|
117
|
+
</details>
|
|
118
|
+
|
|
119
|
+
## Your first prompt
|
|
120
|
+
|
|
121
|
+
Enter the following prompt in your MCP Client to check if everything is working:
|
|
122
|
+
|
|
123
|
+
```text
|
|
124
|
+
Find the class `StringUtils` in my local maven repository and show me its methods.
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Your MCP client should read the class `StringUtils` from your local Maven repository and show its methods.
|
|
128
|
+
|
|
129
|
+
### Configuration (Optional)
|
|
130
|
+
|
|
131
|
+
If the auto-detection fails, or if you want to filter which packages are indexed, you can add environment variables to
|
|
132
|
+
the configuration:
|
|
133
|
+
|
|
134
|
+
* **`MAVEN_REPO`**: Absolute path to your local Maven repository (e.g., `/Users/yourname/.m2/repository`). Use this if
|
|
135
|
+
your repository is in a non-standard location.
|
|
136
|
+
* **`GRADLE_REPO_PATH`**: Absolute path to your Gradle cache (e.g.,
|
|
137
|
+
`/Users/yourname/.gradle/caches/modules-2/files-2.1`).
|
|
138
|
+
* **`INCLUDED_PACKAGES`**: Comma-separated list of package patterns to index (e.g., `com.mycompany.*,org.example.*`).
|
|
139
|
+
Default is `*` (index everything).
|
|
140
|
+
* **`MAVEN_INDEXER_CFR_PATH`**: (Optional) Absolute path to a specific CFR decompiler JAR. If not provided, the server
|
|
141
|
+
will attempt to use its bundled CFR version.
|
|
142
|
+
* **`VERSION_RESOLUTION_STRATEGY`**: (Optional) Strategy to choose the version when multiple versions of an artifact are found and no specific coordinate is provided.
|
|
143
|
+
* `semver`: (Default) Prefer the highest semantic version (e.g. 1.2.0 > 1.1.9).
|
|
144
|
+
* `latest-published`: Prefer the version with the latest publish time (checks `*.pom.lastUpdated` first, then file modification time).
|
|
145
|
+
* `latest-used`: Prefer the version most recently imported/used by the user (based on file creation time).
|
|
39
146
|
|
|
40
147
|
Example with optional configuration:
|
|
41
148
|
|
|
@@ -44,12 +151,16 @@ Example with optional configuration:
|
|
|
44
151
|
"mcpServers": {
|
|
45
152
|
"maven-indexer": {
|
|
46
153
|
"command": "npx",
|
|
47
|
-
"args": [
|
|
154
|
+
"args": [
|
|
155
|
+
"-y",
|
|
156
|
+
"maven-indexer-mcp@latest"
|
|
157
|
+
],
|
|
48
158
|
"env": {
|
|
49
159
|
"MAVEN_REPO": "/Users/yourname/.m2/repository",
|
|
50
160
|
"GRADLE_REPO_PATH": "/Users/yourname/.gradle/caches/modules-2/files-2.1",
|
|
51
161
|
"INCLUDED_PACKAGES": "com.mycompany.*",
|
|
52
|
-
"MAVEN_INDEXER_CFR_PATH": "/path/to/cfr-0.152.jar"
|
|
162
|
+
"MAVEN_INDEXER_CFR_PATH": "/path/to/cfr-0.152.jar",
|
|
163
|
+
"VERSION_RESOLUTION_STRATEGY": "semver"
|
|
53
164
|
}
|
|
54
165
|
}
|
|
55
166
|
}
|
|
@@ -60,19 +171,22 @@ Example with optional configuration:
|
|
|
60
171
|
|
|
61
172
|
If you prefer to run from source:
|
|
62
173
|
|
|
63
|
-
1.
|
|
174
|
+
1. Clone the repository:
|
|
175
|
+
|
|
64
176
|
```bash
|
|
65
177
|
git clone https://github.com/tangcent/maven-indexer-mcp.git
|
|
66
178
|
cd maven-indexer-mcp
|
|
67
179
|
```
|
|
68
180
|
|
|
69
|
-
2.
|
|
181
|
+
2. Install dependencies and build:
|
|
182
|
+
|
|
70
183
|
```bash
|
|
71
184
|
npm install
|
|
72
185
|
npm run build
|
|
73
186
|
```
|
|
74
187
|
|
|
75
|
-
3.
|
|
188
|
+
3. Use the absolute path in your config:
|
|
189
|
+
|
|
76
190
|
```json
|
|
77
191
|
{
|
|
78
192
|
"mcpServers": {
|
|
@@ -86,31 +200,38 @@ If you prefer to run from source:
|
|
|
86
200
|
|
|
87
201
|
## Available Tools
|
|
88
202
|
|
|
89
|
-
*
|
|
90
|
-
|
|
91
|
-
1.
|
|
92
|
-
2.
|
|
93
|
-
3.
|
|
94
|
-
|
|
95
|
-
*
|
|
96
|
-
|
|
97
|
-
*
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
*
|
|
203
|
+
* **`search_classes`**: Search for Java classes in the local Maven repository and Gradle caches.
|
|
204
|
+
* **WHEN TO USE**:
|
|
205
|
+
1. **Internal/Private Code**: You need to find a class from a company-internal library.
|
|
206
|
+
2. **Obscure Libraries**: You are using a less common public library that the AI doesn't know well.
|
|
207
|
+
3. **Version Verification**: You need to check exactly which version of a class is present locally.
|
|
208
|
+
|
|
209
|
+
* *Note*: For well-known libraries (e.g., standard Java lib, Spring), the AI likely knows the class structure
|
|
210
|
+
already, so this tool is less critical.
|
|
211
|
+
* **Examples**: "Show me the source of StringUtils", "What methods are available on DateTimeUtils?", "Where is this
|
|
212
|
+
class imported from?".
|
|
213
|
+
* Input: `className` (e.g., "StringUtils", "Json parser")
|
|
214
|
+
* Output: List of matching classes with their artifacts.
|
|
215
|
+
* **`get_class_details`**: Decompile and read the source code of external libraries/dependencies. **Use this instead
|
|
216
|
+
of 'SearchCodebase' for classes that are imported but defined in JAR files.**
|
|
217
|
+
* **Key Value**: "Don't guess what the internal library does—read the code."
|
|
218
|
+
* **Tip**: Essential for internal/proprietary code where documentation is scarce or non-existent.
|
|
219
|
+
* Input: `className` (required), `artifactId` (optional), `type` ("signatures", "docs", "source")
|
|
220
|
+
* Output: Method signatures, Javadocs, or full source code.
|
|
221
|
+
* **Note**: If `artifactId` is omitted, the tool automatically selects the best available artifact (preferring those
|
|
222
|
+
with source code attached).
|
|
223
|
+
* **`search_artifacts`**: Search for artifacts in Maven/Gradle caches by coordinate (groupId, artifactId).
|
|
224
|
+
* **`search_implementations`**: Search for classes that implement a specific interface or extend a specific class.
|
|
225
|
+
Useful for finding SPI implementations in external libraries.
|
|
226
|
+
* Input: `className` (e.g. "java.util.List")
|
|
227
|
+
* Output: List of implementation/subclass names and their artifacts.
|
|
228
|
+
* **`refresh_index`**: Trigger a re-scan of the Maven repository.
|
|
108
229
|
|
|
109
230
|
## Development
|
|
110
231
|
|
|
111
|
-
*
|
|
112
|
-
*
|
|
232
|
+
* **Run tests**: `npm test`
|
|
233
|
+
* **Watch mode**: `npm run watch`
|
|
113
234
|
|
|
114
235
|
## License
|
|
115
236
|
|
|
116
|
-
ISC
|
|
237
|
+
[ISC](LICENSE)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import semver from 'semver';
|
|
4
|
+
import { Config } from './config.js';
|
|
5
|
+
export class ArtifactResolver {
|
|
6
|
+
static strategies = {
|
|
7
|
+
'semver': ArtifactResolver.compareSemver,
|
|
8
|
+
'latest-published': ArtifactResolver.compareLatestPublished,
|
|
9
|
+
'latest-used': ArtifactResolver.compareLatestUsed
|
|
10
|
+
};
|
|
11
|
+
static async resolveBestArtifact(artifacts) {
|
|
12
|
+
if (!artifacts || artifacts.length === 0)
|
|
13
|
+
return undefined;
|
|
14
|
+
const config = await Config.getInstance();
|
|
15
|
+
const comparator = this.strategies[config.versionResolutionStrategy] || this.strategies['semver'];
|
|
16
|
+
const sorted = [...artifacts].sort((a, b) => {
|
|
17
|
+
// 1. Always prefer source if available
|
|
18
|
+
if (a.hasSource !== b.hasSource) {
|
|
19
|
+
return a.hasSource ? -1 : 1; // source comes first
|
|
20
|
+
}
|
|
21
|
+
// 2. Apply configured strategy
|
|
22
|
+
try {
|
|
23
|
+
return comparator(a, b);
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
// Fallback to ID comparison (likely insert order)
|
|
27
|
+
return b.id - a.id;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return sorted[0];
|
|
31
|
+
}
|
|
32
|
+
static compareSemver(a, b) {
|
|
33
|
+
const vA = semver.coerce(a.version);
|
|
34
|
+
const vB = semver.coerce(b.version);
|
|
35
|
+
if (vA && vB) {
|
|
36
|
+
const comparison = semver.rcompare(vA, vB);
|
|
37
|
+
if (comparison !== 0)
|
|
38
|
+
return comparison;
|
|
39
|
+
}
|
|
40
|
+
if (semver.valid(a.version) && semver.valid(b.version)) {
|
|
41
|
+
return semver.rcompare(a.version, b.version);
|
|
42
|
+
}
|
|
43
|
+
// Fallback for non-semver strings
|
|
44
|
+
return b.id - a.id;
|
|
45
|
+
}
|
|
46
|
+
static getArtifactFileTime(artifact, timeType) {
|
|
47
|
+
let p = artifact.abspath;
|
|
48
|
+
try {
|
|
49
|
+
if (fs.statSync(p).isDirectory()) {
|
|
50
|
+
// Try jar first, then pom
|
|
51
|
+
const jarPath = path.join(p, `${artifact.artifactId}-${artifact.version}.jar`);
|
|
52
|
+
if (fs.existsSync(jarPath))
|
|
53
|
+
return fs.statSync(jarPath)[timeType].getTime();
|
|
54
|
+
const pomPath = path.join(p, `${artifact.artifactId}-${artifact.version}.pom`);
|
|
55
|
+
if (fs.existsSync(pomPath))
|
|
56
|
+
return fs.statSync(pomPath)[timeType].getTime();
|
|
57
|
+
}
|
|
58
|
+
return fs.statSync(p)[timeType].getTime();
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
static getArtifactPublishTime(artifact) {
|
|
65
|
+
let p = artifact.abspath;
|
|
66
|
+
try {
|
|
67
|
+
// 1. Try to read .lastUpdated file (priority)
|
|
68
|
+
if (fs.statSync(p).isDirectory()) {
|
|
69
|
+
const pomPath = path.join(p, `${artifact.artifactId}-${artifact.version}.pom`);
|
|
70
|
+
const lastUpdatedPath = `${pomPath}.lastUpdated`;
|
|
71
|
+
if (fs.existsSync(lastUpdatedPath)) {
|
|
72
|
+
try {
|
|
73
|
+
const content = fs.readFileSync(lastUpdatedPath, 'utf-8');
|
|
74
|
+
// Try to find property with timestamp
|
|
75
|
+
const matches = content.match(/lastUpdated=(\d{13})/g);
|
|
76
|
+
if (matches) {
|
|
77
|
+
let maxTime = 0;
|
|
78
|
+
for (const match of matches) {
|
|
79
|
+
const ts = parseInt(match.split('=')[1]);
|
|
80
|
+
if (!isNaN(ts) && ts > maxTime)
|
|
81
|
+
maxTime = ts;
|
|
82
|
+
}
|
|
83
|
+
if (maxTime > 0)
|
|
84
|
+
return maxTime;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
// ignore parse error
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// 2. Fallback to mtime
|
|
93
|
+
return ArtifactResolver.getArtifactFileTime(artifact, 'mtime');
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
static compareLatestPublished(a, b) {
|
|
100
|
+
const tA = ArtifactResolver.getArtifactPublishTime(a);
|
|
101
|
+
const tB = ArtifactResolver.getArtifactPublishTime(b);
|
|
102
|
+
return tB - tA; // Newer first
|
|
103
|
+
}
|
|
104
|
+
static compareLatestUsed(a, b) {
|
|
105
|
+
const tA = ArtifactResolver.getArtifactFileTime(a, 'birthtime');
|
|
106
|
+
const tB = ArtifactResolver.getArtifactFileTime(b, 'birthtime');
|
|
107
|
+
return tB - tA; // Newer first
|
|
108
|
+
}
|
|
109
|
+
}
|
package/build/config.js
CHANGED
|
@@ -11,7 +11,9 @@ export class Config {
|
|
|
11
11
|
gradleRepository = "";
|
|
12
12
|
javaBinary = "java";
|
|
13
13
|
includedPackages = ["*"];
|
|
14
|
+
normalizedIncludedPackages = [];
|
|
14
15
|
cfrPath = null;
|
|
16
|
+
versionResolutionStrategy = 'semver';
|
|
15
17
|
constructor() { }
|
|
16
18
|
static async getInstance() {
|
|
17
19
|
if (!Config.instance) {
|
|
@@ -79,6 +81,7 @@ export class Config {
|
|
|
79
81
|
.map(p => p.trim())
|
|
80
82
|
.filter(p => p.length > 0);
|
|
81
83
|
}
|
|
84
|
+
this.normalizedIncludedPackages = this.normalizeScanPatterns(this.includedPackages);
|
|
82
85
|
// Load CFR Path
|
|
83
86
|
if (process.env.MAVEN_INDEXER_CFR_PATH) {
|
|
84
87
|
this.cfrPath = process.env.MAVEN_INDEXER_CFR_PATH;
|
|
@@ -89,6 +92,24 @@ export class Config {
|
|
|
89
92
|
const __dirname = path.dirname(__filename);
|
|
90
93
|
this.cfrPath = path.resolve(__dirname, '../lib/cfr-0.152.jar');
|
|
91
94
|
}
|
|
95
|
+
if (process.env.VERSION_RESOLUTION_STRATEGY) {
|
|
96
|
+
const strategy = process.env.VERSION_RESOLUTION_STRATEGY.toLowerCase();
|
|
97
|
+
if (strategy === 'semver' || strategy === 'latest-published' || strategy === 'latest-used') {
|
|
98
|
+
this.versionResolutionStrategy = strategy;
|
|
99
|
+
}
|
|
100
|
+
else if (strategy === 'semver-latest') {
|
|
101
|
+
// Backward compatibility
|
|
102
|
+
this.versionResolutionStrategy = 'semver';
|
|
103
|
+
}
|
|
104
|
+
else if (strategy === 'date-latest' || strategy === 'modification-time' || strategy === 'publish-time') {
|
|
105
|
+
// Backward compatibility
|
|
106
|
+
this.versionResolutionStrategy = 'latest-published';
|
|
107
|
+
}
|
|
108
|
+
else if (strategy === 'creation-time' || strategy === 'usage-time') {
|
|
109
|
+
// Backward compatibility
|
|
110
|
+
this.versionResolutionStrategy = 'latest-used';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
92
113
|
if (this.cfrPath && !(await this.fileExists(this.cfrPath))) {
|
|
93
114
|
try {
|
|
94
115
|
console.error(`CFR jar not found at ${this.cfrPath}, attempting to download...`);
|
|
@@ -174,4 +195,55 @@ export class Config {
|
|
|
174
195
|
}
|
|
175
196
|
return null;
|
|
176
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Normalizes package patterns for scanning.
|
|
200
|
+
* 1. Filters out empty or whitespace-only patterns.
|
|
201
|
+
* 2. Removes wildcards ('*', '.*').
|
|
202
|
+
* 3. Sorts and removes duplicates/sub-packages.
|
|
203
|
+
*
|
|
204
|
+
* @param patterns The raw patterns from configuration.
|
|
205
|
+
* @returns A list of normalized package prefixes. Returns [] if all packages should be scanned.
|
|
206
|
+
*/
|
|
207
|
+
normalizeScanPatterns(patterns) {
|
|
208
|
+
if (!patterns || patterns.length === 0) {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
// 1. Filter empty/blank patterns and remove wildcards
|
|
212
|
+
let clean = patterns
|
|
213
|
+
.map(p => p.trim())
|
|
214
|
+
.filter(p => p.length > 0) // Ignore empty strings
|
|
215
|
+
.map(p => {
|
|
216
|
+
if (p.endsWith('.*'))
|
|
217
|
+
return p.slice(0, -2);
|
|
218
|
+
if (p === '*')
|
|
219
|
+
return ''; // Will be filtered out later if we want strict prefixes, but '*' usually means ALL
|
|
220
|
+
return p;
|
|
221
|
+
});
|
|
222
|
+
// If any pattern became empty string (meaning '*') or was originally '*', it implies "Scan All"
|
|
223
|
+
if (clean.includes('')) {
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
if (clean.length === 0) {
|
|
227
|
+
// Usually empty config means Scan All.
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
// 2. Sort to ensure parents come before children
|
|
231
|
+
clean.sort();
|
|
232
|
+
// 3. Remove duplicates and sub-packages
|
|
233
|
+
const result = [];
|
|
234
|
+
for (const p of clean) {
|
|
235
|
+
if (result.length === 0) {
|
|
236
|
+
result.push(p);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const last = result[result.length - 1];
|
|
240
|
+
// Check if 'p' is sub-package of 'last'
|
|
241
|
+
// e.g. last="com.test", p="com.test.demo" -> p starts with last + '.'
|
|
242
|
+
if (p === last || p.startsWith(last + '.')) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
result.push(p);
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
177
249
|
}
|