forked from colbymchenry/codegraph
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcargo-workspace.ts
More file actions
244 lines (203 loc) · 6.13 KB
/
Copy pathcargo-workspace.ts
File metadata and controls
244 lines (203 loc) · 6.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
/**
* Cargo Workspace Resolver Helper
*
* Parses a project's root Cargo.toml and member crate manifests to
* build a crate-name -> member-directory map. Used by the Rust
* resolver to resolve `use crate_name::...` references that point
* into workspace member crates.
*/
import picomatch from 'picomatch';
import { ResolutionContext } from '../types';
const GLOB_CHARS = /[*?[\]{}!]/;
const SKIP_DIRS = new Set(['target', 'node_modules', '.git', 'dist', 'build']);
const MAX_GLOB_WALK_DEPTH = 5;
function getSection(content: string, sectionName: string): string | null {
const lines = content.split('\n');
let inSection = false;
const sectionLines: string[] = [];
for (const line of lines) {
const trimmed = line.trim();
if (!inSection) {
if (trimmed === `[${sectionName}]`) {
inSection = true;
}
continue;
}
if (/^\[[^\]]+\]$/.test(trimmed)) {
break;
}
sectionLines.push(line);
}
if (!inSection) return null;
return sectionLines.join('\n');
}
function extractQuotedValues(valueList: string): string[] {
const values: string[] = [];
let quote: '"' | "'" | null = null;
let escaped = false;
let current = '';
for (const ch of valueList) {
if (!quote) {
if (ch === '"' || ch === "'") {
quote = ch;
current = '';
}
continue;
}
if (escaped) {
current += ch;
escaped = false;
continue;
}
if (ch === '\\') {
escaped = true;
continue;
}
if (ch === quote) {
values.push(current.trim());
quote = null;
current = '';
continue;
}
current += ch;
}
return values.filter(Boolean);
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function getArrayValue(section: string, key: string): string | null {
const keyRegex = new RegExp(`\\b${escapeRegExp(key)}\\b\\s*=`, 'm');
const keyMatch = keyRegex.exec(section);
if (!keyMatch) return null;
let i = keyMatch.index + keyMatch[0].length;
while (i < section.length && /\s/.test(section.charAt(i))) i++;
if (section.charAt(i) !== '[') return null;
i++;
let inQuote: '"' | "'" | null = null;
let escaped = false;
let depth = 1;
const start = i;
while (i < section.length) {
const ch = section.charAt(i);
if (inQuote) {
if (escaped) {
escaped = false;
} else if (ch === '\\') {
escaped = true;
} else if (ch === inQuote) {
inQuote = null;
}
i++;
continue;
}
if (ch === '"' || ch === "'") {
inQuote = ch;
i++;
continue;
}
if (ch === '[') {
depth++;
i++;
continue;
}
if (ch === ']') {
depth--;
if (depth === 0) {
return section.slice(start, i);
}
i++;
continue;
}
i++;
}
return null;
}
function parseWorkspaceMembers(cargoToml: string): string[] {
const workspaceSection = getSection(cargoToml, 'workspace');
if (!workspaceSection) return [];
const membersValue = getArrayValue(workspaceSection, 'members');
if (!membersValue) return [];
return extractQuotedValues(membersValue);
}
function parsePackageName(cargoToml: string): string | null {
const packageSection = getSection(cargoToml, 'package');
if (!packageSection) return null;
const packageNameMatch = packageSection.match(/name\s*=\s*["']([^"'\n]+)["']/);
return packageNameMatch?.[1]?.trim() ?? null;
}
function addCrateAlias(map: Map<string, string>, crateName: string, memberPath: string): void {
const normalized = crateName.replace(/-/g, '_');
map.set(crateName, memberPath);
if (normalized !== crateName) {
map.set(normalized, memberPath);
}
}
function cleanPath(memberPath: string): string {
return memberPath.replace(/\\/g, '/').replace(/\/$/, '');
}
function expandGlobMember(member: string, context: ResolutionContext): string[] {
if (!context.listDirectories) return [];
const firstGlobIdx = member.search(GLOB_CHARS);
const staticPrefix = member
.slice(0, firstGlobIdx)
.replace(/[^/]*$/, '')
.replace(/\/$/, '');
const matcher = picomatch(member, { dot: false });
const matches: string[] = [];
const seen = new Set<string>();
function walk(dir: string, depth: number): void {
if (depth > MAX_GLOB_WALK_DEPTH) return;
const children = context.listDirectories!(dir);
for (const child of children) {
if (SKIP_DIRS.has(child) || child.startsWith('.')) continue;
const rel = dir === '.' ? child : `${dir}/${child}`;
if (matcher(rel) && !seen.has(rel)) {
seen.add(rel);
matches.push(rel);
}
walk(rel, depth + 1);
}
}
walk(staticPrefix || '.', 0);
return matches;
}
function expandMembers(members: string[], context: ResolutionContext): string[] {
const expanded: string[] = [];
const seen = new Set<string>();
for (const member of members) {
const candidates = GLOB_CHARS.test(member)
? expandGlobMember(member, context)
: [member];
for (const candidate of candidates) {
const cleaned = cleanPath(candidate);
if (seen.has(cleaned)) continue;
seen.add(cleaned);
expanded.push(cleaned);
}
}
return expanded;
}
/**
* Build a map from crate-name aliases to workspace member directory paths.
* Example: "mytool-core" and "mytool_core" -> "crates/mytool-core"
*
* Supports glob members (e.g. `members = ["crates/*"]`) via picomatch
* when the context exposes `listDirectories`.
*/
export function getCargoWorkspaceCrateMap(context: ResolutionContext): Map<string, string> {
const result = new Map<string, string>();
const rootCargoToml = context.readFile('Cargo.toml');
if (!rootCargoToml) return result;
const rawMembers = parseWorkspaceMembers(rootCargoToml);
const members = expandMembers(rawMembers, context);
for (const memberPath of members) {
const memberCargoPath = `${memberPath}/Cargo.toml`;
const memberCargoToml = context.readFile(memberCargoPath);
if (!memberCargoToml) continue;
const packageName = parsePackageName(memberCargoToml);
if (!packageName) continue;
addCrateAlias(result, packageName, memberPath);
}
return result;
}