jvm-upgrade 0.1.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/dist/repos.js ADDED
@@ -0,0 +1,121 @@
1
+ const GRADLE_VERSIONS_URL = 'https://services.gradle.org/versions/all';
2
+ const gradleBuildTimes = new WeakMap();
3
+ const publishedMemo = new WeakMap();
4
+ export function artifactPath(group, artifact) {
5
+ return `${group.replaceAll('.', '/')}/${artifact}`;
6
+ }
7
+ export function parseMetadata(xml) {
8
+ const found = new Set();
9
+ const block = /<versions>([\s\S]*?)<\/versions>/.exec(xml)?.[1] ?? '';
10
+ for (const m of block.matchAll(/<version>\s*([^<\s]+)\s*<\/version>/g))
11
+ found.add(m[1]);
12
+ for (const tag of ['latest', 'release']) {
13
+ const m = new RegExp(`<${tag}>\\s*([^<\\s]+)\\s*</${tag}>`).exec(xml);
14
+ if (m)
15
+ found.add(m[1]);
16
+ }
17
+ return [...found];
18
+ }
19
+ export function parseGradleBuildTime(value) {
20
+ const m = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})([+-]\d{4})?$/.exec(value);
21
+ if (!m)
22
+ return undefined;
23
+ const [, y, mo, d, h, mi, s, tz = '+0000'] = m;
24
+ const date = new Date(`${y}-${mo}-${d}T${h}:${mi}:${s}${tz.slice(0, 3)}:${tz.slice(3)}`);
25
+ return Number.isNaN(date.getTime()) ? undefined : date;
26
+ }
27
+ function loadGradle(http) {
28
+ let p = gradleBuildTimes.get(http);
29
+ if (!p) {
30
+ p = (async () => {
31
+ const res = await http.get(GRADLE_VERSIONS_URL);
32
+ if (res.status !== 200)
33
+ throw new Error(`${GRADLE_VERSIONS_URL}: HTTP ${res.status}`);
34
+ const list = JSON.parse(res.body);
35
+ const usable = list.filter((v) => !v.snapshot && !v.nightly && !v.broken && !v.releaseNightly);
36
+ return new Map(usable.map((v) => [v.version, v.buildTime ?? '']));
37
+ })();
38
+ p.catch(() => gradleBuildTimes.delete(http));
39
+ gradleBuildTimes.set(http, p);
40
+ }
41
+ return p;
42
+ }
43
+ async function fetchGithubTags(http, owner, repo) {
44
+ const res = await http.get(`https://api.github.com/repos/${owner}/${repo}/tags?per_page=100`);
45
+ if (res.status !== 200)
46
+ return undefined;
47
+ const tags = JSON.parse(res.body);
48
+ return tags.map((t) => t.name.replace(/^v(?=\d)/i, ''));
49
+ }
50
+ async function versionsFromRepo(http, dep, repo) {
51
+ const url = `${repo}${artifactPath(dep.group, dep.artifact)}/maven-metadata.xml`;
52
+ const res = await http.get(url);
53
+ if (res.status === 200)
54
+ return parseMetadata(res.body);
55
+ if (res.status === 404) {
56
+ const owner = dep.group.startsWith('com.github.') ? dep.group.split('.')[2] : undefined;
57
+ if (repo.includes('jitpack.io') && owner) {
58
+ return (await fetchGithubTags(http, owner, dep.artifact)) ?? [];
59
+ }
60
+ return [];
61
+ }
62
+ throw new Error(`${url}: HTTP ${res.status}`);
63
+ }
64
+ export async function fetchVersions(http, dep) {
65
+ const repoOf = new Map();
66
+ if (dep.kind === 'gradle') {
67
+ try {
68
+ return { versions: [...(await loadGradle(http)).keys()], repoOf, errors: [] };
69
+ }
70
+ catch (err) {
71
+ return { versions: [], repoOf, errors: [errorText(err)] };
72
+ }
73
+ }
74
+ const results = await Promise.allSettled(dep.repositories.map((r) => versionsFromRepo(http, dep, r)));
75
+ const errors = [];
76
+ results.forEach((result, i) => {
77
+ const repo = dep.repositories[i];
78
+ if (result.status === 'rejected') {
79
+ errors.push(`${repo}: ${errorText(result.reason)}`);
80
+ return;
81
+ }
82
+ for (const v of result.value)
83
+ if (!repoOf.has(v))
84
+ repoOf.set(v, repo);
85
+ });
86
+ return { versions: [...repoOf.keys()], repoOf, errors };
87
+ }
88
+ function errorText(err) {
89
+ return err instanceof Error ? err.message : String(err);
90
+ }
91
+ async function publishedAt(http, dep, version, repo) {
92
+ if (dep.kind === 'gradle') {
93
+ const time = (await loadGradle(http).catch(() => undefined))?.get(version);
94
+ return time ? parseGradleBuildTime(time) : undefined;
95
+ }
96
+ const base = repo ?? dep.repositories[0];
97
+ if (!base)
98
+ return undefined;
99
+ const url = `${base}${artifactPath(dep.group, dep.artifact)}/${version}/${dep.artifact}-${version}.pom`;
100
+ const res = await http.head(url).catch(() => undefined);
101
+ const header = res?.status === 200 ? res.headers['last-modified'] : undefined;
102
+ if (!header)
103
+ return undefined;
104
+ const date = new Date(header);
105
+ return Number.isNaN(date.getTime()) ? undefined : date;
106
+ }
107
+ export function fetchPublishedAt(http, dep, version, repo) {
108
+ let memo = publishedMemo.get(http);
109
+ if (!memo)
110
+ publishedMemo.set(http, (memo = new Map()));
111
+ const key = `${dep.kind}|${dep.group}:${dep.artifact}:${version}|${repo ?? dep.repositories[0] ?? ''}`;
112
+ let p = memo.get(key);
113
+ if (!p)
114
+ memo.set(key, (p = publishedAt(http, dep, version, repo)));
115
+ return p;
116
+ }
117
+ export async function fetchPom(http, group, artifact, version, repo) {
118
+ const url = `${repo}${artifactPath(group, artifact)}/${version}/${artifact}-${version}.pom`;
119
+ const res = await http.get(url).catch(() => undefined);
120
+ return res?.status === 200 ? res.body : undefined;
121
+ }
package/dist/rows.js ADDED
@@ -0,0 +1,81 @@
1
+ import { matchesAny } from './config.js';
2
+ import { intersectVersions, pick, selectCandidates } from './policy.js';
3
+ import { fetchPublishedAt, fetchVersions } from './repos.js';
4
+ import { compareVersions, preferStyle, upgradeLevel } from './version.js';
5
+ export function displayName(dep) {
6
+ if (dep.kind === 'plugin' && dep.artifact.endsWith('.gradle.plugin'))
7
+ return dep.group;
8
+ if (dep.kind === 'gradle')
9
+ return 'gradle (wrapper)';
10
+ return `${dep.group}:${dep.artifact}`;
11
+ }
12
+ export function levelColor(level) {
13
+ return level === 'major' ? 'red' : level === 'minor' ? 'yellow' : level === 'patch' ? 'green' : 'magenta';
14
+ }
15
+ /** Dependencies sharing one version literal (catalog ref, property) become one row. */
16
+ function groupByLocation(deps) {
17
+ const groups = new Map();
18
+ for (const dep of deps) {
19
+ const key = dep.location
20
+ ? `${dep.location.file}:${dep.location.start}`
21
+ : `${dep.group}:${dep.artifact}:${dep.version}:${dep.file}`;
22
+ const list = groups.get(key) ?? [];
23
+ list.push(dep);
24
+ groups.set(key, list);
25
+ }
26
+ return [...groups.values()];
27
+ }
28
+ export async function buildRows(deps, opts) {
29
+ const filtered = [];
30
+ for (const dep of deps) {
31
+ const options = await opts.optionsFor(dep.file);
32
+ if (options.include.length && !matchesAny(options.include, dep.group, dep.artifact))
33
+ continue;
34
+ if (matchesAny(options.exclude, dep.group, dep.artifact))
35
+ continue;
36
+ filtered.push({ dep, options });
37
+ }
38
+ const optionsOf = new Map(filtered.map((f) => [f.dep, f.options]));
39
+ const groups = groupByLocation(filtered.map((f) => f.dep));
40
+ opts.progress.total(groups.length);
41
+ return Promise.all(groups.map(async (group) => {
42
+ try {
43
+ return await buildRow(group, optionsOf.get(group[0]), opts);
44
+ }
45
+ finally {
46
+ opts.progress.tick();
47
+ }
48
+ }));
49
+ }
50
+ async function buildRow(group, options, opts) {
51
+ const first = group[0];
52
+ const extra = first.kind === 'gradle' ? [] : [...opts.extraRepositories, ...options.repositories].map((r) => (r.endsWith('/') ? r : `${r}/`));
53
+ const results = await Promise.all(group.map((dep) => fetchVersions(opts.http, { ...dep, repositories: [...new Set([...dep.repositories, ...extra])] })));
54
+ const errors = [...new Set(results.flatMap((r) => r.errors))];
55
+ const repoOf = results[0].repoOf;
56
+ const versions = preferStyle(first.version, intersectVersions(results.map((r) => r.versions)));
57
+ const name = group.length > 1 ? `${displayName(first)} (+${group.length - 1})` : displayName(first);
58
+ const candidates = await selectCandidates(first.version, versions, { target: options.target, pre: options.pre, cooldownDays: options.cooldown, allowDowngrade: options.allowDowngrade, now: opts.now }, (v) => fetchPublishedAt(opts.http, first, v, repoOf.get(v)));
59
+ const choices = toChoices(first.version, candidates);
60
+ const wanted = candidates.downgrade ?? pick(candidates, options.target);
61
+ const selected = wanted === undefined ? undefined : choices.findIndex((c) => c.version === wanted);
62
+ return { name, deps: group, current: first.version, location: first.location, choices, selected: selected === -1 ? undefined : selected, repoOf, errors };
63
+ }
64
+ export function toChoices(current, c) {
65
+ const seen = new Set();
66
+ const choices = [];
67
+ if (c.downgrade) {
68
+ choices.push({ level: 'downgrade', version: c.downgrade });
69
+ seen.add(c.downgrade);
70
+ }
71
+ for (const v of [c.patch, c.minor, c.major]) {
72
+ if (!v || seen.has(v))
73
+ continue;
74
+ const level = upgradeLevel(current, v);
75
+ if (!level)
76
+ continue;
77
+ seen.add(v);
78
+ choices.push({ level, version: v });
79
+ }
80
+ return choices.sort((a, b) => compareVersions(a.version, b.version));
81
+ }
@@ -0,0 +1,454 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { dedupeRepos, GOOGLE, GRADLE_PLUGIN_PORTAL, MAVEN_CENTRAL, NAMED_REPOS } from './repos.js';
4
+ const COORD_RE = /(["'])([\w.\-]+):([\w.\-]+):([^"'\s@:]+)(?::[\w.\-]+)?(?:@\w+)?\1/dg;
5
+ const MAP_RE = /\bgroup\s*[:=]\s*(["'])([^"']+)\1\s*,\s*name\s*[:=]\s*(["'])([^"']+)\3\s*,\s*version\s*[:=]\s*(?:(["'])([^"']*)\5|([A-Za-z_][\w.]*))/dg;
6
+ const KOTLIN_DEP_RE = /\bkotlin\s*\(\s*"([\w\-]+)"\s*,\s*(?:version\s*=\s*)?"([^"]*)"\s*\)/dg;
7
+ const PLUGIN_RE = /\b(id|kotlin)\s*\(?\s*(["'])([\w.\-]+)\2\s*\)?\s*\.?\s*version\s*\(?\s*(?:(["'])([^"']*)\4|([A-Za-z_][\w.]*(?:\(\))?))/dg;
8
+ const REPO_RE = /\b(mavenCentral|google|gradlePluginPortal|jcenter|mavenLocal)\s*[({]|\bmaven\s*\(\s*(?:url\s*=\s*)?(?:uri\s*\(\s*)?["']([^"'$]+)["']|\b(?:url|setUrl)\s*(?:=\s*)?\(?\s*(?:uri\s*\(\s*)?["']([^"'$]+)["']/g;
9
+ const CATALOG_RE = /(?:create\s*\(\s*["'](\w+)["']\s*\)|(\w+))\s*\{[^{}]*?\bfrom\s*\(\s*files\s*\(\s*["']([^"']+)["']/g;
10
+ const VAR_RES = [
11
+ /\b(?:ext|extra)\.(\w+)\s*=\s*(["'])([^"'$\n]*)\2/dg,
12
+ /\b(?:ext|extra)\.set\s*\(\s*["'](\w+)["']\s*,\s*(["'])([^"'$\n]*)\2/dg,
13
+ /\bextra\s*\[\s*["'](\w+)["']\s*\]\s*=\s*(["'])([^"'$\n]*)\2/dg,
14
+ /\b(?:val|var|def|String)\s+(\w+)\s*(?::\s*String\s*)?(?:=|by\s+extra\s*\()\s*(["'])([^"'$\n]*)\2/dg,
15
+ ];
16
+ const EXT_BLOCK_RES = [
17
+ /(?<![\w.])(\w+)\s*=\s*(["'])([^"'$\n]*)\2/dg,
18
+ /\bset\s*\(\s*["'](\w+)["']\s*,\s*(["'])([^"'$\n]*)\2/dg,
19
+ ];
20
+ const PROPERTY_RE = /^[ \t]*([\w.\-]+)[ \t]*[=:][ \t]*(.*?)[ \t]*\r?$/dgm;
21
+ const WRAPPER_RE = /distributionUrl\s*[=:].*?gradle-([\w.\-]+?)-(?:bin|all)\.zip/d;
22
+ export function isDynamic(version) {
23
+ return version === '' || /[+\[\]()*,]|^latest\./.test(version);
24
+ }
25
+ export function stripCode(src) {
26
+ const out = src.split('');
27
+ const blank = (from, to) => {
28
+ for (let k = from; k < to; k++)
29
+ if (src[k] !== '\n')
30
+ out[k] = ' ';
31
+ };
32
+ let i = 0;
33
+ while (i < src.length) {
34
+ if (src.startsWith('//', i)) {
35
+ const end = src.indexOf('\n', i);
36
+ blank(i, (i = end < 0 ? src.length : end));
37
+ }
38
+ else if (src.startsWith('/*', i)) {
39
+ const close = src.indexOf('*/', i + 2);
40
+ blank(i, (i = close < 0 ? src.length : close + 2));
41
+ }
42
+ else if (src[i] === '"' || src[i] === "'") {
43
+ i = skipString(src, i);
44
+ }
45
+ else {
46
+ i++;
47
+ }
48
+ }
49
+ return out.join('');
50
+ }
51
+ function skipString(src, i) {
52
+ const quote = src[i];
53
+ const delim = src.startsWith(quote.repeat(3), i) ? quote.repeat(3) : quote;
54
+ let j = i + delim.length;
55
+ while (j < src.length) {
56
+ if (src[j] === '\\')
57
+ j += 2;
58
+ else if (src.startsWith(delim, j))
59
+ return j + delim.length;
60
+ else if (delim.length === 1 && src[j] === '\n')
61
+ return j;
62
+ else
63
+ j++;
64
+ }
65
+ return j;
66
+ }
67
+ export function stripToml(src) {
68
+ const out = src.split('');
69
+ let quote;
70
+ for (let i = 0; i < src.length; i++) {
71
+ const c = src[i];
72
+ if (quote) {
73
+ if (c === '\\' && quote === '"')
74
+ i++;
75
+ else if (c === quote || c === '\n')
76
+ quote = undefined;
77
+ }
78
+ else if (c === '"' || c === "'") {
79
+ quote = c;
80
+ }
81
+ else if (c === '#') {
82
+ for (; i < src.length && src[i] !== '\n'; i++)
83
+ out[i] = ' ';
84
+ }
85
+ }
86
+ return out.join('');
87
+ }
88
+ export function stripProperties(src) {
89
+ return src.replace(/^[ \t]*[#!].*$/gm, (line) => ' '.repeat(line.length));
90
+ }
91
+ async function readSource(file, strip) {
92
+ try {
93
+ const content = await readFile(file, 'utf8');
94
+ return { file, content, code: strip(content) };
95
+ }
96
+ catch {
97
+ return undefined;
98
+ }
99
+ }
100
+ function findBlocks(code, name) {
101
+ const re = new RegExp(`\\b${name}\\s*(?:\\([^()]*\\))?\\s*\\{`, 'g');
102
+ return [...code.matchAll(re)].map((m) => {
103
+ const open = m.index + m[0].length - 1;
104
+ return { start: open + 1, end: matchBrace(code, open) };
105
+ });
106
+ }
107
+ function matchBrace(code, open) {
108
+ let depth = 0;
109
+ for (let i = open; i < code.length; i++) {
110
+ if (code[i] === '{')
111
+ depth++;
112
+ else if (code[i] === '}' && --depth === 0)
113
+ return i;
114
+ }
115
+ return code.length;
116
+ }
117
+ function within(pos, spans) {
118
+ return spans.some((s) => pos >= s.start && pos < s.end);
119
+ }
120
+ function matchesIn(code, re, spans) {
121
+ return [...code.matchAll(re)].filter((m) => !spans || within(m.index, spans));
122
+ }
123
+ function locOf(src, m, group) {
124
+ const [start, end] = m.indices[group];
125
+ return { file: src.file, start, end };
126
+ }
127
+ function reposIn(code, spans) {
128
+ const urls = [];
129
+ for (const span of spans) {
130
+ for (const m of code.slice(span.start, span.end).matchAll(REPO_RE)) {
131
+ urls.push(m[1] ? NAMED_REPOS[m[1]]() : (m[2] ?? m[3]));
132
+ }
133
+ }
134
+ return dedupeRepos(urls);
135
+ }
136
+ function reposWithin(src, outer) {
137
+ const outerSpans = findBlocks(src.code, outer);
138
+ return reposIn(src.code, findBlocks(src.code, 'repositories').filter((s) => within(s.start, outerSpans)));
139
+ }
140
+ function buildFileRepos(src) {
141
+ const publishing = findBlocks(src.code, 'publishing');
142
+ return reposIn(src.code, findBlocks(src.code, 'repositories').filter((s) => !within(s.start, publishing)));
143
+ }
144
+ function collectVars(src) {
145
+ const vars = new Map();
146
+ const extBlocks = findBlocks(src.code, 'ext');
147
+ const matches = [
148
+ ...EXT_BLOCK_RES.flatMap((re) => matchesIn(src.code, re, extBlocks)),
149
+ ...VAR_RES.flatMap((re) => matchesIn(src.code, re)),
150
+ ].sort((a, b) => a.index - b.index);
151
+ for (const m of matches)
152
+ vars.set(m[1], { value: m[3], loc: locOf(src, m, 3) });
153
+ return vars;
154
+ }
155
+ function propertyVars(src) {
156
+ const vars = new Map();
157
+ for (const m of src?.code.matchAll(PROPERTY_RE) ?? []) {
158
+ if (m[2])
159
+ vars.set(m[1], { value: m[2], loc: locOf(src, m, 2) });
160
+ }
161
+ return vars;
162
+ }
163
+ function normKey(key) {
164
+ return key.replace(/[-_]/g, '.');
165
+ }
166
+ function lookup(scope, expr) {
167
+ const e = expr.trim().replace(/\.get(?:\(\))?$/, '');
168
+ const catalog = /^(\w+)\.versions\.([\w.]+)$/.exec(e);
169
+ if (catalog)
170
+ return scope.catalogs.get(catalog[1])?.get(normKey(catalog[2]));
171
+ const key = /(?:extra|properties)\s*\[\s*["']([\w.\-]+)["']\s*\]$/.exec(e)?.[1] ??
172
+ /property\s*\(\s*["']([\w.\-]+)["']\s*\)$/.exec(e)?.[1] ??
173
+ e.replace(/^(?:(?:rootProject|project|ext|extra|properties)\.)+/, '');
174
+ for (const vars of scope.vars) {
175
+ const value = vars.get(key);
176
+ if (value)
177
+ return value;
178
+ }
179
+ return undefined;
180
+ }
181
+ const REF_RE = /\$\{([^}]+)\}|\$([A-Za-z_][\w.]*(?:\(\))?)/g;
182
+ function resolveVersion(raw, loc, scope) {
183
+ const whole = /^(?:\$\{([^}]+)\}|\$([A-Za-z_][\w.]*(?:\(\))?))$/.exec(raw);
184
+ let result;
185
+ if (whole) {
186
+ result = lookup(scope, (whole[1] ?? whole[2]));
187
+ }
188
+ else if (!raw.includes('$')) {
189
+ result = { value: raw, loc };
190
+ }
191
+ else {
192
+ let ok = true;
193
+ const value = raw.replace(REF_RE, (_, braced, bare) => {
194
+ const found = lookup(scope, (braced ?? bare));
195
+ if (!found)
196
+ ok = false;
197
+ return found?.value ?? '';
198
+ });
199
+ result = ok ? { value } : undefined;
200
+ }
201
+ return result && (isDynamic(result.value) || !result.loc) ? { value: result.value } : result;
202
+ }
203
+ function makeDep(kind, group, artifact, version, file, repositories) {
204
+ return { group, artifact, kind, version: version.value, file, repositories, ...(version.loc && { location: version.loc }) };
205
+ }
206
+ function addResolved(ctx, src, kind, group, artifact, raw, loc) {
207
+ const version = resolveVersion(raw, loc, ctx.scope);
208
+ if (!version) {
209
+ ctx.warnings.push(`${src.file}: cannot resolve version "${raw}" of ${group}:${artifact}`);
210
+ return;
211
+ }
212
+ ctx.out.push(makeDep(kind, group, artifact, version, src.file, ctx.repositories));
213
+ }
214
+ function libraryDeps(src, ctx) {
215
+ for (const m of src.code.matchAll(COORD_RE)) {
216
+ addResolved(ctx, src, 'library', m[2], m[3], m[4], locOf(src, m, 4));
217
+ }
218
+ for (const m of src.code.matchAll(MAP_RE)) {
219
+ if (m[6] !== undefined)
220
+ addResolved(ctx, src, 'library', m[2], m[4], m[6], locOf(src, m, 6));
221
+ else
222
+ addResolved(ctx, src, 'library', m[2], m[4], `\${${m[7]}}`);
223
+ }
224
+ const plugins = findBlocks(src.code, 'plugins');
225
+ for (const m of src.code.matchAll(KOTLIN_DEP_RE)) {
226
+ if (!within(m.index, plugins)) {
227
+ addResolved(ctx, src, 'library', 'org.jetbrains.kotlin', `kotlin-${m[1]}`, m[2], locOf(src, m, 2));
228
+ }
229
+ }
230
+ }
231
+ function pluginDeps(src, ctx) {
232
+ for (const m of matchesIn(src.code, PLUGIN_RE, findBlocks(src.code, 'plugins'))) {
233
+ const id = m[1] === 'kotlin' ? `org.jetbrains.kotlin.${m[3]}` : m[3];
234
+ if (m[5] !== undefined)
235
+ addResolved(ctx, src, 'plugin', id, `${id}.gradle.plugin`, m[5], locOf(src, m, 5));
236
+ else
237
+ addResolved(ctx, src, 'plugin', id, `${id}.gradle.plugin`, `\${${m[6]}}`);
238
+ }
239
+ }
240
+ export function parseToml(code) {
241
+ const entries = [];
242
+ let table = '';
243
+ let i = 0;
244
+ const skipWs = (newlines) => {
245
+ while (i < code.length && (code[i] === ' ' || code[i] === '\t' || code[i] === '\r' || (newlines && code[i] === '\n')))
246
+ i++;
247
+ };
248
+ const skipLine = () => {
249
+ const nl = code.indexOf('\n', i);
250
+ i = nl < 0 ? code.length : nl + 1;
251
+ };
252
+ const readKey = () => {
253
+ const key = /^(?:[\w.\-]+|"[^"\n]*"|'[^'\n]*')+/.exec(code.slice(i, i + 256))?.[0] ?? '';
254
+ i += key.length;
255
+ return key.replace(/["']/g, '');
256
+ };
257
+ const readValue = (path, fields) => {
258
+ const c = code[i];
259
+ if (c === '"' || c === "'") {
260
+ const start = i + 1;
261
+ let j = start;
262
+ while (j < code.length && code[j] !== c && code[j] !== '\n')
263
+ j += c === '"' && code[j] === '\\' ? 2 : 1;
264
+ fields.set(path, { value: code.slice(start, j), start, end: j });
265
+ i = j + 1;
266
+ }
267
+ else if (c === '{') {
268
+ i++;
269
+ for (;;) {
270
+ skipWs(true);
271
+ if (i >= code.length || code[i] === '}')
272
+ break;
273
+ if (code[i] === ',') {
274
+ i++;
275
+ continue;
276
+ }
277
+ const key = readKey();
278
+ skipWs(false);
279
+ if (!key || code[i] !== '=')
280
+ break;
281
+ i++;
282
+ skipWs(true);
283
+ readValue(path ? `${path}.${key}` : key, fields);
284
+ }
285
+ i++;
286
+ }
287
+ else if (c === '[') {
288
+ let depth = 0;
289
+ for (; i < code.length; i++) {
290
+ if (code[i] === '[')
291
+ depth++;
292
+ else if (code[i] === ']' && --depth === 0)
293
+ break;
294
+ }
295
+ i++;
296
+ }
297
+ else {
298
+ while (i < code.length && !/[\n,}]/.test(code[i]))
299
+ i++;
300
+ }
301
+ };
302
+ while (i < code.length) {
303
+ skipWs(true);
304
+ if (i >= code.length)
305
+ break;
306
+ if (code[i] === '[') {
307
+ const end = code.indexOf(']', i);
308
+ table = code.slice(i + 1, end).trim();
309
+ i = end + 1;
310
+ skipLine();
311
+ continue;
312
+ }
313
+ const key = readKey();
314
+ skipWs(false);
315
+ if (!key || code[i] !== '=') {
316
+ skipLine();
317
+ continue;
318
+ }
319
+ i++;
320
+ skipWs(false);
321
+ const fields = new Map();
322
+ readValue('', fields);
323
+ entries.push({ table, key, fields });
324
+ skipLine();
325
+ }
326
+ return entries;
327
+ }
328
+ async function catalogDeclarations(root, settings) {
329
+ const declared = [];
330
+ if (settings) {
331
+ const blocks = findBlocks(settings.code, 'versionCatalogs');
332
+ for (const m of matchesIn(settings.code, CATALOG_RE, blocks)) {
333
+ declared.push({ name: (m[1] ?? m[2]), file: resolve(dirname(settings.file), m[3]) });
334
+ }
335
+ }
336
+ const defaultFile = join(root, 'gradle', 'libs.versions.toml');
337
+ if (!declared.some((c) => c.file === defaultFile))
338
+ declared.push({ name: 'libs', file: defaultFile });
339
+ return declared;
340
+ }
341
+ function tomlValue(src, s) {
342
+ return isDynamic(s.value) ? { value: s.value } : { value: s.value, loc: { file: src.file, start: s.start, end: s.end } };
343
+ }
344
+ function pickVersion(src, fields, path) {
345
+ const plain = fields.get(path);
346
+ if (plain)
347
+ return tomlValue(src, plain);
348
+ const rich = ['require', 'strictly', 'prefer']
349
+ .map((k) => fields.get(path ? `${path}.${k}` : k))
350
+ .filter((s) => s !== undefined);
351
+ const best = rich.find((s) => !isDynamic(s.value)) ?? rich[0];
352
+ return best && tomlValue(src, best);
353
+ }
354
+ function splitNotation(src, s) {
355
+ const parts = s.value.split(':');
356
+ const last = parts[parts.length - 1];
357
+ const start = s.end - last.length;
358
+ return { parts, version: tomlValue(src, { value: last, start, end: s.end }) };
359
+ }
360
+ function parseCatalog(src, libraryRepos, pluginRepos, warnings) {
361
+ const entries = parseToml(src.code);
362
+ const versions = new Map();
363
+ for (const e of entries.filter((e) => e.table === 'versions')) {
364
+ const v = pickVersion(src, e.fields, '');
365
+ if (v)
366
+ versions.set(e.key, v);
367
+ }
368
+ const versionOf = (e) => {
369
+ const ref = e.fields.get('version.ref');
370
+ if (!ref)
371
+ return pickVersion(src, e.fields, 'version');
372
+ const found = versions.get(ref.value);
373
+ if (!found)
374
+ warnings.push(`${src.file}: unknown version.ref "${ref.value}" in ${e.key}`);
375
+ return found;
376
+ };
377
+ const deps = [];
378
+ for (const e of entries.filter((e) => e.table === 'libraries')) {
379
+ const str = e.fields.get('');
380
+ if (str) {
381
+ const { parts, version } = splitNotation(src, str);
382
+ if (parts.length >= 3 && version)
383
+ deps.push(makeDep('library', parts[0], parts[1], version, src.file, libraryRepos));
384
+ continue;
385
+ }
386
+ const [group, artifact] = e.fields.get('module')?.value.split(':') ?? [e.fields.get('group')?.value, e.fields.get('name')?.value];
387
+ const version = versionOf(e);
388
+ if (group && artifact && version)
389
+ deps.push(makeDep('library', group, artifact, version, src.file, libraryRepos));
390
+ }
391
+ for (const e of entries.filter((e) => e.table === 'plugins')) {
392
+ const str = e.fields.get('');
393
+ const id = str ? str.value.split(':')[0] : e.fields.get('id')?.value;
394
+ const version = str ? (str.value.includes(':') ? splitNotation(src, str).version : undefined) : versionOf(e);
395
+ if (id && version)
396
+ deps.push(makeDep('plugin', id, `${id}.gradle.plugin`, version, src.file, pluginRepos));
397
+ }
398
+ const normalized = new Map([...versions].map(([k, v]) => [normKey(k), v]));
399
+ return { versions: normalized, deps };
400
+ }
401
+ async function wrapperDep(root) {
402
+ const src = await readSource(join(root, 'gradle', 'wrapper', 'gradle-wrapper.properties'), stripProperties);
403
+ const m = src && WRAPPER_RE.exec(src.code);
404
+ if (!src || !m)
405
+ return undefined;
406
+ return makeDep('gradle', 'org.gradle', 'gradle', { value: m[1], loc: locOf(src, m, 1) }, src.file, []);
407
+ }
408
+ export async function scanGradle(build) {
409
+ const warnings = [];
410
+ const out = [];
411
+ const settings = build.settings ? await readSource(build.settings, stripCode) : undefined;
412
+ const rootProps = propertyVars(await readSource(join(build.root, 'gradle.properties'), stripProperties));
413
+ const rootBuildFile = build.buildFiles.find((f) => dirname(f) === build.root);
414
+ const rootBuild = rootBuildFile ? await readSource(rootBuildFile, stripCode) : undefined;
415
+ const rootVars = rootBuild ? collectVars(rootBuild) : new Map();
416
+ const pluginManagementRepos = settings ? reposWithin(settings, 'pluginManagement') : [];
417
+ const resolutionRepos = settings ? reposWithin(settings, 'dependencyResolutionManagement') : [];
418
+ const sharedLibraryRepos = [...resolutionRepos, ...(rootBuild ? buildFileRepos(rootBuild) : [])];
419
+ const libraryRepos = (extra) => {
420
+ const repos = dedupeRepos([...sharedLibraryRepos, ...extra]);
421
+ return repos.length ? repos : [MAVEN_CENTRAL, GOOGLE];
422
+ };
423
+ const pluginRepos = dedupeRepos([
424
+ ...(pluginManagementRepos.length ? pluginManagementRepos : [GRADLE_PLUGIN_PORTAL]),
425
+ ...resolutionRepos.filter((url) => url === GOOGLE || url === MAVEN_CENTRAL),
426
+ ]);
427
+ const catalogs = new Map();
428
+ for (const decl of await catalogDeclarations(build.root, settings)) {
429
+ const src = await readSource(decl.file, stripToml);
430
+ if (!src)
431
+ continue;
432
+ const parsed = parseCatalog(src, libraryRepos([]), pluginRepos, warnings);
433
+ catalogs.set(decl.name, parsed.versions);
434
+ out.push(...parsed.deps);
435
+ }
436
+ if (settings) {
437
+ const scope = { vars: [collectVars(settings), rootProps], catalogs };
438
+ pluginDeps(settings, { scope, repositories: pluginRepos, warnings, out });
439
+ }
440
+ for (const file of build.buildFiles) {
441
+ const src = file === rootBuildFile ? rootBuild : await readSource(file, stripCode);
442
+ if (!src)
443
+ continue;
444
+ const dir = dirname(file);
445
+ const moduleProps = dir === build.root ? new Map() : propertyVars(await readSource(join(dir, 'gradle.properties'), stripProperties));
446
+ const scope = { vars: [collectVars(src), rootVars, moduleProps, rootProps], catalogs };
447
+ libraryDeps(src, { scope, repositories: libraryRepos(buildFileRepos(src)), warnings, out });
448
+ pluginDeps(src, { scope, repositories: pluginRepos, warnings, out });
449
+ }
450
+ const wrapper = await wrapperDep(build.root);
451
+ if (wrapper)
452
+ out.push(wrapper);
453
+ return { dependencies: out, warnings };
454
+ }