gitnexus 1.6.11-rc.17 → 1.6.11-rc.18

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.
@@ -1,7 +1,20 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { XMLParser } from 'fast-xml-parser';
3
4
  import { shouldIgnorePath, loadIgnoreRules } from '../../../config/ignore-service.js';
4
5
  import { logger } from '../../logger.js';
6
+ // POMs are static metadata. Parse hierarchy with a real XML parser, but do not
7
+ // invoke Maven or resolve the effective model. Properties, profiles, and remote
8
+ // parent resolution remain outside this extractor's deterministic boundary.
9
+ const pomParser = new XMLParser({
10
+ ignoreAttributes: true,
11
+ removeNSPrefix: true,
12
+ trimValues: true,
13
+ parseTagValue: false,
14
+ processEntities: false,
15
+ ignoreDeclaration: true,
16
+ ignorePiTags: true,
17
+ });
5
18
  async function parseJavaManifest(repoPath) {
6
19
  const pomPath = path.join(repoPath, 'pom.xml');
7
20
  try {
@@ -9,13 +22,14 @@ async function parseJavaManifest(repoPath) {
9
22
  return parsePom(content);
10
23
  }
11
24
  catch {
12
- // fall through to Gradle
25
+ // Missing pom.xml — fall through to Gradle.
13
26
  }
27
+ const gradleSidecars = await readGradleSidecars(repoPath);
14
28
  for (const name of ['build.gradle.kts', 'build.gradle']) {
15
29
  const gradlePath = path.join(repoPath, name);
16
30
  try {
17
31
  const content = await fs.readFile(gradlePath, 'utf-8');
18
- return parseGradle(content, repoPath);
32
+ return parseGradle(content, repoPath, gradleSidecars);
19
33
  }
20
34
  catch {
21
35
  continue;
@@ -23,45 +37,244 @@ async function parseJavaManifest(repoPath) {
23
37
  }
24
38
  return null;
25
39
  }
40
+ async function readIfPresent(filePath) {
41
+ try {
42
+ return await fs.readFile(filePath, 'utf-8');
43
+ }
44
+ catch {
45
+ return undefined;
46
+ }
47
+ }
48
+ async function readGradleSidecars(repoPath) {
49
+ const [properties, settingsKts, settingsGroovy, catalog] = await Promise.all([
50
+ readIfPresent(path.join(repoPath, 'gradle.properties')),
51
+ readIfPresent(path.join(repoPath, 'settings.gradle.kts')),
52
+ readIfPresent(path.join(repoPath, 'settings.gradle')),
53
+ readIfPresent(path.join(repoPath, 'gradle', 'libs.versions.toml')),
54
+ ]);
55
+ const sidecars = {
56
+ catalogLibraries: new Map(),
57
+ catalogBundles: new Map(),
58
+ };
59
+ const groupMatch = properties?.match(/(?:^|\n)\s*group\s*=\s*([^\s#]+)/);
60
+ if (groupMatch)
61
+ sidecars.propertiesGroup = groupMatch[1];
62
+ const settings = settingsKts ?? settingsGroovy;
63
+ const nameMatch = settings?.match(/rootProject\.name\s*=\s*['"]([^'"]+)['"]/);
64
+ if (nameMatch)
65
+ sidecars.rootProjectName = nameMatch[1];
66
+ if (catalog) {
67
+ const parsed = parseGradleVersionCatalog(catalog);
68
+ sidecars.catalogLibraries = parsed.libraries;
69
+ sidecars.catalogBundles = parsed.bundles;
70
+ }
71
+ return sidecars;
72
+ }
73
+ function catalogAccessors(alias) {
74
+ const dotted = alias.replace(/[-_]/g, '.');
75
+ const camel = alias.replace(/[-_]+([A-Za-z0-9])/g, (_, char) => char.toUpperCase());
76
+ return [...new Set([alias, dotted, camel])];
77
+ }
78
+ function projectAccessorToArtifactId(accessor) {
79
+ const last = accessor.split('.').pop();
80
+ return last.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`).replace(/^-/, '');
81
+ }
82
+ function moduleToGa(module) {
83
+ const parts = module.split(':');
84
+ return parts.length >= 2 ? `${parts[0]}:${parts[1]}` : undefined;
85
+ }
86
+ function parseInlineTomlTable(rhs) {
87
+ const fields = {};
88
+ for (const match of rhs.matchAll(/([A-Za-z0-9_-]+)\s*=\s*['"]([^'"]+)['"]/g)) {
89
+ fields[match[1]] = match[2];
90
+ }
91
+ return fields;
92
+ }
93
+ /** Default Gradle catalog (`gradle/libs.versions.toml`) — aliases only, no version resolution. */
94
+ function parseGradleVersionCatalog(toml) {
95
+ const libraries = new Map();
96
+ const bundles = new Map();
97
+ let section = 'other';
98
+ const addLibrary = (alias, ga) => {
99
+ for (const accessor of catalogAccessors(alias))
100
+ libraries.set(accessor, ga);
101
+ };
102
+ for (const raw of toml.split(/\r?\n/)) {
103
+ const line = raw.replace(/#.*$/, '').trim();
104
+ if (!line)
105
+ continue;
106
+ const header = line.match(/^\[([^\]]+)\]$/);
107
+ if (header) {
108
+ const name = header[1];
109
+ section =
110
+ name === 'libraries' || name.endsWith('.libraries')
111
+ ? 'libraries'
112
+ : name === 'bundles' || name.endsWith('.bundles')
113
+ ? 'bundles'
114
+ : 'other';
115
+ continue;
116
+ }
117
+ if (section === 'libraries') {
118
+ const dottedModule = line.match(/^([A-Za-z0-9._-]+)\.module\s*=\s*['"]([^'"]+)['"]$/);
119
+ if (dottedModule) {
120
+ const ga = moduleToGa(dottedModule[2]);
121
+ if (ga)
122
+ addLibrary(dottedModule[1], ga);
123
+ continue;
124
+ }
125
+ const assignment = line.match(/^([A-Za-z0-9._-]+)\s*=\s*(.+)$/);
126
+ if (!assignment)
127
+ continue;
128
+ const alias = assignment[1];
129
+ const rhs = assignment[2].trim();
130
+ const quoted = rhs.match(/^['"]([^'"]+)['"]$/);
131
+ if (quoted) {
132
+ const ga = moduleToGa(quoted[1]);
133
+ if (ga)
134
+ addLibrary(alias, ga);
135
+ continue;
136
+ }
137
+ const table = parseInlineTomlTable(rhs);
138
+ const ga = table.module
139
+ ? moduleToGa(table.module)
140
+ : table.group && table.name
141
+ ? `${table.group}:${table.name}`
142
+ : undefined;
143
+ if (ga)
144
+ addLibrary(alias, ga);
145
+ continue;
146
+ }
147
+ if (section === 'bundles') {
148
+ const assignment = line.match(/^([A-Za-z0-9._-]+)\s*=\s*\[([^\]]*)\]$/);
149
+ if (!assignment)
150
+ continue;
151
+ const members = [...assignment[2].matchAll(/['"]([^'"]+)['"]/g)].map((match) => match[1]);
152
+ for (const accessor of catalogAccessors(assignment[1]))
153
+ bundles.set(accessor, members);
154
+ }
155
+ }
156
+ return { libraries, bundles };
157
+ }
158
+ const GRADLE_GROUP_PATTERNS = [
159
+ /(?:^|[\n{;])\s*(?:rootProject\.)?group\s*=\s*['"]([^'"]+)['"]/,
160
+ /(?:^|[\n{;])\s*group\s+['"]([^'"]+)['"]/,
161
+ ];
162
+ const GRADLE_COORD_CONFIGS = 'implementation|api|compileOnly|runtimeOnly|testImplementation|testApi|testCompileOnly|compile|kapt|ksp|commonMainImplementation|commonMainApi';
163
+ const CATALOG_ALIAS = '([A-Za-z0-9_]+(?:\\.[A-Za-z0-9_]+)*)(?:\\.get\\(\\)|\\.asProvider\\(\\))?';
164
+ function gradleDepRe(suffix) {
165
+ return new RegExp(`(?:${GRADLE_COORD_CONFIGS})\\s*${suffix}`, 'g');
166
+ }
167
+ function parseGradleGroup(content) {
168
+ for (const pattern of GRADLE_GROUP_PATTERNS) {
169
+ const match = content.match(pattern);
170
+ if (match?.[1])
171
+ return match[1];
172
+ }
173
+ return undefined;
174
+ }
175
+ function asXmlNode(value) {
176
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
177
+ ? value
178
+ : undefined;
179
+ }
180
+ function xmlText(value) {
181
+ if (typeof value === 'string' || typeof value === 'number') {
182
+ const text = String(value).trim();
183
+ return text || undefined;
184
+ }
185
+ const nested = asXmlNode(value)?.['#text'];
186
+ if (nested === undefined)
187
+ return undefined;
188
+ return xmlText(nested);
189
+ }
190
+ function xmlChildText(node, name) {
191
+ return node ? xmlText(node[name]) : undefined;
192
+ }
193
+ function asList(value) {
194
+ if (value === undefined || value === null)
195
+ return [];
196
+ return Array.isArray(value) ? value : [value];
197
+ }
198
+ /** Direct project dependencies only — not BOM, profiles, or plugin classpath. */
199
+ function collectProjectDependencies(project, deps) {
200
+ const dependencies = asXmlNode(project.dependencies);
201
+ if (!dependencies)
202
+ return;
203
+ for (const dep of asList(dependencies.dependency)) {
204
+ const depNode = asXmlNode(dep);
205
+ const groupId = xmlChildText(depNode, 'groupId');
206
+ const artifactId = xmlChildText(depNode, 'artifactId');
207
+ if (groupId && artifactId)
208
+ deps.push(`${groupId}:${artifactId}`);
209
+ }
210
+ }
26
211
  function parsePom(content) {
27
- const projectGroupMatch = content.match(/<project[^>]*>[\s\S]*?<groupId>([^<]+)<\/groupId>/);
28
- const projectArtifactMatch = content.match(/<project[^>]*>[\s\S]*?<artifactId>([^<]+)<\/artifactId>/);
29
- if (!projectGroupMatch || !projectArtifactMatch)
212
+ let parsed;
213
+ try {
214
+ // parseSourceSafe guards tree-sitter's Windows SIGSEGV by switching to a
215
+ // chunked input callback above 16 KB; XMLParser only accepts XML text, so
216
+ // routing POMs through it silently yields an empty document.
217
+ // eslint-disable-next-line gitnexus/require-safe-parse
218
+ parsed = pomParser.parse(content);
219
+ }
220
+ catch {
30
221
  return null;
31
- const groupId = projectGroupMatch[1].trim();
32
- const artifactId = projectArtifactMatch[1].trim();
33
- const deps = [];
34
- const depBlocks = content.matchAll(/<dependency>\s*([\s\S]*?)<\/dependency>/g);
35
- for (const block of depBlocks) {
36
- const gMatch = block[1].match(/<groupId>([^<]+)<\/groupId>/);
37
- const aMatch = block[1].match(/<artifactId>([^<]+)<\/artifactId>/);
38
- if (gMatch && aMatch) {
39
- deps.push(`${gMatch[1].trim()}:${aMatch[1].trim()}`);
40
- }
41
222
  }
223
+ const project = asXmlNode(asXmlNode(parsed)?.project);
224
+ if (!project)
225
+ return null;
226
+ // Maven inherits groupId from <parent>, but artifactId is always the
227
+ // project's own direct child and must never fall back to parent.artifactId.
228
+ const groupId = xmlChildText(project, 'groupId') ?? xmlChildText(asXmlNode(project.parent), 'groupId');
229
+ const artifactId = xmlChildText(project, 'artifactId');
230
+ if (!groupId || !artifactId)
231
+ return null;
232
+ const deps = [];
233
+ collectProjectDependencies(project, deps);
42
234
  return { groupId, artifactId, deps: [...new Set(deps)] };
43
235
  }
44
- function parseGradle(content, repoPath) {
45
- const groupMatch = content.match(/group\s*=\s*['"]([^'"]+)['"]/);
46
- const dirName = path.basename(repoPath);
47
- const groupId = groupMatch ? groupMatch[1] : '';
236
+ function parseGradle(content, repoPath, sidecars = { catalogLibraries: new Map(), catalogBundles: new Map() }) {
237
+ // Static text + default catalog file. Do not execute Gradle.
238
+ const groupId = parseGradleGroup(content) ?? sidecars.propertiesGroup ?? '';
48
239
  if (!groupId)
49
240
  return null;
50
- const artifactId = dirName;
241
+ const artifactId = sidecars.rootProjectName ?? path.basename(repoPath);
242
+ const { catalogLibraries, catalogBundles } = sidecars;
51
243
  const deps = [];
52
- // implementation("group:artifact:version") or api("group:artifact:version")
53
- const depMatches = content.matchAll(/(?:implementation|api|compileOnly|runtimeOnly)\s*\(\s*['"]([^'"]+)['"]\s*\)/g);
54
- for (const m of depMatches) {
55
- const parts = m[1].split(':');
56
- if (parts.length >= 2) {
57
- deps.push(`${parts[0]}:${parts[1]}`);
244
+ const pushCatalogAlias = (alias) => {
245
+ const ga = catalogLibraries.get(alias);
246
+ if (ga)
247
+ deps.push(ga);
248
+ };
249
+ const namedPattern = gradleDepRe(`(?:\\(\\s*)?(?:group\\s*=\\s*['"](?<group1>[^'"]+)['"]\\s*,\\s*name\\s*=\\s*['"](?<name1>[^'"]+)['"]|name\\s*=\\s*['"](?<name2>[^'"]+)['"]\\s*,\\s*group\\s*=\\s*['"](?<group2>[^'"]+)['"]|group:\\s*['"](?<group3>[^'"]+)['"]\\s*,\\s*name:\\s*['"](?<name3>[^'"]+)['"]|name:\\s*['"](?<name4>[^'"]+)['"]\\s*,\\s*group:\\s*['"](?<group4>[^'"]+)['"])`);
250
+ for (const match of content.matchAll(namedPattern)) {
251
+ const group = match.groups?.group1 ?? match.groups?.group2 ?? match.groups?.group3 ?? match.groups?.group4;
252
+ const name = match.groups?.name1 ?? match.groups?.name2 ?? match.groups?.name3 ?? match.groups?.name4;
253
+ if (group && name)
254
+ deps.push(`${group}:${name}`);
255
+ }
256
+ for (const match of content.matchAll(gradleDepRe(`(?:\\(\\s*)?libs(?:\\.libraries)?\\.(?!bundles\\.|plugins\\.)${CATALOG_ALIAS}`))) {
257
+ pushCatalogAlias(match[1]);
258
+ }
259
+ for (const match of content.matchAll(gradleDepRe(`(?:\\(\\s*)?libs\\.bundles\\.${CATALOG_ALIAS}`))) {
260
+ for (const member of catalogBundles.get(match[1]) ?? []) {
261
+ for (const accessor of catalogAccessors(member))
262
+ pushCatalogAlias(accessor);
58
263
  }
59
264
  }
60
- // implementation(project(":subproject"))
61
- const projDeps = content.matchAll(/(?:implementation|api)\s*\(\s*project\s*\(\s*['"]([^'"]+)['"]\s*\)\s*\)/g);
62
- for (const m of projDeps) {
63
- const subName = m[1].replace(/^:/, '');
64
- deps.push(`${groupId}:${subName}`);
265
+ for (const match of content.matchAll(gradleDepRe(`\\(\\s*projects\\.([A-Za-z][A-Za-z0-9.]*)`))) {
266
+ deps.push(`${groupId}:${projectAccessorToArtifactId(match[1])}`);
267
+ }
268
+ for (const match of content.matchAll(gradleDepRe(`(?:\\(\\s*['"]([^'"]+)['"]\\s*\\)|['"]([^'"]+)['"])`))) {
269
+ const coord = match[1] ?? match[2];
270
+ if (!coord)
271
+ continue;
272
+ const parts = coord.split(':');
273
+ if (parts.length >= 2)
274
+ deps.push(`${parts[0]}:${parts[1]}`);
275
+ }
276
+ for (const match of content.matchAll(gradleDepRe(`(?:\\(\\s*)?project\\s*\\(\\s*['"]([^'"]+)['"]\\s*\\)`))) {
277
+ deps.push(`${groupId}:${match[1].replace(/^:/, '')}`);
65
278
  }
66
279
  return { groupId, artifactId, deps: [...new Set(deps)] };
67
280
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.11-rc.17",
3
+ "version": "1.6.11-rc.18",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
@@ -66,6 +66,7 @@
66
66
  "cors": "^2.8.5",
67
67
  "express": "^5.2.1",
68
68
  "express-rate-limit": "^8.4.1",
69
+ "fast-xml-parser": "^5.11.1",
69
70
  "glob": "^13.0.6",
70
71
  "graphology": "^0.26.0",
71
72
  "graphology-indices": "^0.17.0",