@configdirector/cli 0.0.0-stage → 0.1.0-beta.2
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/CHANGELOG.md +29 -0
- package/LICENSE +21 -0
- package/README.md +89 -2
- package/dist/configdirector.mjs +2912 -0
- package/package.json +56 -3
- package/schema/scan-output.schema.json +410 -0
|
@@ -0,0 +1,2912 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { Command, CommanderError } from "commander";
|
|
4
|
+
import { access, mkdir, open, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import * as v from "valibot";
|
|
6
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
|
+
import { createInterface } from "node:readline";
|
|
8
|
+
import ignore from "ignore";
|
|
9
|
+
import picomatch from "picomatch";
|
|
10
|
+
//#region package.json
|
|
11
|
+
var version = "0.1.0-beta.2";
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/CliError.ts
|
|
14
|
+
var CliError = class extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = new.target.name;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/ExitCode.ts
|
|
22
|
+
const ExitCode = {
|
|
23
|
+
success: 0,
|
|
24
|
+
problemsFound: 1,
|
|
25
|
+
couldNotRun: 2
|
|
26
|
+
};
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/Invocation.ts
|
|
29
|
+
const nonEmptyVariable = (invocation, name) => invocation.environmentVariables[name] || void 0;
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/admin-api/admin-api-errors.ts
|
|
32
|
+
const ApiAuthorizationErrorBodySchema = v.object({
|
|
33
|
+
error: v.string(),
|
|
34
|
+
code: v.string()
|
|
35
|
+
});
|
|
36
|
+
const ApiValidationErrorBodySchema = v.object({ errors: v.array(v.object({
|
|
37
|
+
name: v.optional(v.string()),
|
|
38
|
+
message: v.string()
|
|
39
|
+
})) });
|
|
40
|
+
const ServiceSuspendedBodySchema = v.object({ code: v.literal("SERVICE_SUSPENDED") });
|
|
41
|
+
const describeFieldError = (fieldError) => fieldError.name === void 0 ? fieldError.message : `${fieldError.name}: ${fieldError.message}`;
|
|
42
|
+
var ApiAuthorizationError = class extends CliError {
|
|
43
|
+
constructor(code, apiMessage) {
|
|
44
|
+
super(`The Admin API refused the request: ${apiMessage} (${code})`);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
var ApiValidationError = class extends CliError {
|
|
48
|
+
constructor(errors) {
|
|
49
|
+
super(`The Admin API rejected the request:\n${errors.map((error) => ` ${describeFieldError(error)}`).join("\n")}`);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var ServiceSuspendedError = class extends CliError {
|
|
53
|
+
constructor(retryAfterSeconds) {
|
|
54
|
+
super(`Your organization's service is suspended, so ConfigDirector refuses Admin API requests. The dashboard's billing page shows why and how to reinstate it. Retry after ${retryAfterSeconds} seconds.`);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var ApiUnreachableError = class extends CliError {
|
|
58
|
+
constructor(baseUrl, cause) {
|
|
59
|
+
const reason = cause instanceof Error && cause.cause instanceof Error ? cause.cause.message : String(cause);
|
|
60
|
+
super(`Could not reach the Admin API at ${baseUrl} (${reason}). Check the network connection, --base-url or CONFIGDIRECTOR_BASE_URL.`);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var UnexpectedApiResponseError = class extends CliError {
|
|
64
|
+
constructor(url, description) {
|
|
65
|
+
super(`The Admin API answered ${url} with ${description}.`);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/admin-api/AdminApiClient.ts
|
|
70
|
+
const ProjectSchema = v.object({
|
|
71
|
+
id: v.string(),
|
|
72
|
+
name: v.string(),
|
|
73
|
+
slug: v.string()
|
|
74
|
+
});
|
|
75
|
+
const configFields = {
|
|
76
|
+
key: v.string(),
|
|
77
|
+
deprecatedKeys: v.array(v.object({ key: v.string() })),
|
|
78
|
+
client: v.boolean(),
|
|
79
|
+
server: v.boolean()
|
|
80
|
+
};
|
|
81
|
+
const EnumConfigSchema = v.object({
|
|
82
|
+
...configFields,
|
|
83
|
+
type: v.literal("enum"),
|
|
84
|
+
typeOptions: v.object({
|
|
85
|
+
valueType: v.string(),
|
|
86
|
+
values: v.array(v.string())
|
|
87
|
+
})
|
|
88
|
+
});
|
|
89
|
+
const ConfigSchema = v.variant("type", [EnumConfigSchema, v.object({
|
|
90
|
+
...configFields,
|
|
91
|
+
type: v.pipe(v.string(), v.notValue("enum"))
|
|
92
|
+
})]);
|
|
93
|
+
const isEnumConfig = (config) => config.type === "enum";
|
|
94
|
+
const readJsonBody = async (response) => {
|
|
95
|
+
try {
|
|
96
|
+
return await response.json();
|
|
97
|
+
} catch {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const describeStatus = (response) => `HTTP ${response.status}`;
|
|
102
|
+
const failureFor = (url, response, body) => {
|
|
103
|
+
switch (response.status) {
|
|
104
|
+
case 400: {
|
|
105
|
+
const validationError = v.safeParse(ApiValidationErrorBodySchema, body);
|
|
106
|
+
return validationError.success ? new ApiValidationError(validationError.output.errors) : new UnexpectedApiResponseError(url, describeStatus(response));
|
|
107
|
+
}
|
|
108
|
+
case 403: {
|
|
109
|
+
const authorizationError = v.safeParse(ApiAuthorizationErrorBodySchema, body);
|
|
110
|
+
return authorizationError.success ? new ApiAuthorizationError(authorizationError.output.code, authorizationError.output.error) : new UnexpectedApiResponseError(url, describeStatus(response));
|
|
111
|
+
}
|
|
112
|
+
case 429: {
|
|
113
|
+
const retryAfterSeconds = response.headers.get("retry-after");
|
|
114
|
+
return v.is(ServiceSuspendedBodySchema, body) && retryAfterSeconds !== null ? new ServiceSuspendedError(retryAfterSeconds) : new UnexpectedApiResponseError(url, describeStatus(response));
|
|
115
|
+
}
|
|
116
|
+
default: return new UnexpectedApiResponseError(url, describeStatus(response));
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
var AdminApiClient = class {
|
|
120
|
+
constructor(connection) {
|
|
121
|
+
this.connection = connection;
|
|
122
|
+
}
|
|
123
|
+
listProjects() {
|
|
124
|
+
return this.get("/v1/projects", v.array(ProjectSchema));
|
|
125
|
+
}
|
|
126
|
+
listConfigs(projectId) {
|
|
127
|
+
return this.get(`/v1/projects/${encodeURIComponent(projectId)}/configs`, v.array(ConfigSchema));
|
|
128
|
+
}
|
|
129
|
+
async get(path, schema) {
|
|
130
|
+
const url = `${this.connection.baseUrl}${path}`;
|
|
131
|
+
const response = await this.fetchResponse(url);
|
|
132
|
+
const body = await readJsonBody(response);
|
|
133
|
+
if (!response.ok) throw failureFor(url, response, body);
|
|
134
|
+
const parsedBody = v.safeParse(schema, body);
|
|
135
|
+
if (!parsedBody.success) throw new UnexpectedApiResponseError(url, `an ${describeStatus(response)} response the CLI cannot read`);
|
|
136
|
+
return parsedBody.output;
|
|
137
|
+
}
|
|
138
|
+
async fetchResponse(url) {
|
|
139
|
+
try {
|
|
140
|
+
return await fetch(url, { headers: {
|
|
141
|
+
Accept: "application/json",
|
|
142
|
+
Authorization: `Bearer ${this.connection.token}`,
|
|
143
|
+
"User-Agent": `configdirector-cli/${version}`,
|
|
144
|
+
"X-SourceClient": "cli"
|
|
145
|
+
} });
|
|
146
|
+
} catch (error) {
|
|
147
|
+
throw new ApiUnreachableError(this.connection.baseUrl, error);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/file-contents.ts
|
|
153
|
+
const readFileIfPresent = async (file) => {
|
|
154
|
+
try {
|
|
155
|
+
return await readFile(file, "utf8");
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error.code === "ENOENT") return;
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const parseJson = (contents) => {
|
|
162
|
+
try {
|
|
163
|
+
return JSON.parse(contents);
|
|
164
|
+
} catch {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
const pathExists = (path) => stat(path).then(() => true, () => false);
|
|
169
|
+
const fileExists = (path) => stat(path).then((stats) => stats.isFile(), () => false);
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/saved-api-token.ts
|
|
172
|
+
const SavedApiTokenSchema = v.object({ token: v.string() });
|
|
173
|
+
const platformConfigDirectory = (invocation) => {
|
|
174
|
+
switch (invocation.platform) {
|
|
175
|
+
case "darwin": return join(invocation.homeDirectory, "Library", "Application Support");
|
|
176
|
+
case "win32": return nonEmptyVariable(invocation, "APPDATA") ?? join(invocation.homeDirectory, "AppData", "Roaming");
|
|
177
|
+
default: return join(invocation.homeDirectory, ".config");
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
const userConfigDirectory = (invocation) => nonEmptyVariable(invocation, "XDG_CONFIG_HOME") ?? platformConfigDirectory(invocation);
|
|
181
|
+
const savedApiTokenFile = (invocation) => join(userConfigDirectory(invocation), "configdirector", "config.json");
|
|
182
|
+
const readSavedApiToken = async (invocation) => {
|
|
183
|
+
const file = savedApiTokenFile(invocation);
|
|
184
|
+
const contents = await readFileIfPresent(file);
|
|
185
|
+
if (contents === void 0) return;
|
|
186
|
+
const savedToken = v.safeParse(SavedApiTokenSchema, parseJson(contents));
|
|
187
|
+
if (!savedToken.success) throw new CliError(`${file} does not hold a saved API token. Run \`configdirector login\` again.`);
|
|
188
|
+
return savedToken.output.token;
|
|
189
|
+
};
|
|
190
|
+
const saveApiToken = async (invocation, token) => {
|
|
191
|
+
const file = savedApiTokenFile(invocation);
|
|
192
|
+
await mkdir(dirname(file), {
|
|
193
|
+
recursive: true,
|
|
194
|
+
mode: 448
|
|
195
|
+
});
|
|
196
|
+
const handle = await open(file, "w", 384);
|
|
197
|
+
try {
|
|
198
|
+
await handle.chmod(384);
|
|
199
|
+
await handle.writeFile(`${JSON.stringify({ token }, null, 2)}\n`);
|
|
200
|
+
} finally {
|
|
201
|
+
await handle.close();
|
|
202
|
+
}
|
|
203
|
+
return file;
|
|
204
|
+
};
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/configured-api-token.ts
|
|
207
|
+
const configuredApiToken = async (context) => {
|
|
208
|
+
const token = context.tokenOption ?? nonEmptyVariable(context.invocation, "CONFIGDIRECTOR_TOKEN") ?? await readSavedApiToken(context.invocation);
|
|
209
|
+
if (!token) throw new CliError("No API token found. Set CONFIGDIRECTOR_TOKEN, pass --token, or run `configdirector login`.");
|
|
210
|
+
return token;
|
|
211
|
+
};
|
|
212
|
+
//#endregion
|
|
213
|
+
//#region src/project-selection.ts
|
|
214
|
+
const requireProjectSlug = (context) => {
|
|
215
|
+
if (context.projectSlug === void 0) throw new CliError("Name the project with --project <slug> or CONFIGDIRECTOR_PROJECT.");
|
|
216
|
+
return context.projectSlug;
|
|
217
|
+
};
|
|
218
|
+
const requireLinkedProjectSlug = (context, settings) => {
|
|
219
|
+
const slug = context.projectSlug ?? settings?.project;
|
|
220
|
+
if (slug === void 0) throw new CliError("Name the project with --project <slug>, CONFIGDIRECTOR_PROJECT, or `project` in configdirector.json, which `configdirector init` writes.");
|
|
221
|
+
return slug;
|
|
222
|
+
};
|
|
223
|
+
const findProjectBySlug = async (client, slug) => {
|
|
224
|
+
const project = (await client.listProjects()).find((listedProject) => listedProject.slug === slug);
|
|
225
|
+
if (project === void 0) throw new CliError(`No project in the API token's organization has the slug "${slug}".`);
|
|
226
|
+
return project;
|
|
227
|
+
};
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region src/project-settings.ts
|
|
230
|
+
const ProjectSettingsSchema = v.object({
|
|
231
|
+
project: v.optional(v.string()),
|
|
232
|
+
include: v.optional(v.array(v.string())),
|
|
233
|
+
exclude: v.optional(v.array(v.string())),
|
|
234
|
+
ignoreKeys: v.optional(v.array(v.string()))
|
|
235
|
+
});
|
|
236
|
+
const projectSettingsFile = (workingDirectory) => join(workingDirectory, "configdirector.json");
|
|
237
|
+
const describeIssue = (issue) => {
|
|
238
|
+
const path = v.getDotPath(issue);
|
|
239
|
+
return path === null ? issue.message : `${path}: ${issue.message}`;
|
|
240
|
+
};
|
|
241
|
+
const readProjectSettings = async (workingDirectory) => {
|
|
242
|
+
const file = projectSettingsFile(workingDirectory);
|
|
243
|
+
const contents = await readFileIfPresent(file);
|
|
244
|
+
if (contents === void 0) return;
|
|
245
|
+
const settings = v.safeParse(ProjectSettingsSchema, parseJson(contents));
|
|
246
|
+
if (!settings.success) throw new CliError(`${file} is not valid (${settings.issues.map(describeIssue).join("; ")}).`);
|
|
247
|
+
return settings.output;
|
|
248
|
+
};
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region src/commands/init.ts
|
|
251
|
+
const refuseExistingFile = async (file) => {
|
|
252
|
+
if (await access(file).then(() => true, () => false)) throw new CliError(`${file} already exists. init never overwrites it; edit the file instead.`);
|
|
253
|
+
};
|
|
254
|
+
const init = async (context) => {
|
|
255
|
+
const settingsFile = projectSettingsFile(context.invocation.workingDirectory);
|
|
256
|
+
await refuseExistingFile(settingsFile);
|
|
257
|
+
const projectSlug = requireProjectSlug(context);
|
|
258
|
+
const project = await findProjectBySlug(new AdminApiClient({
|
|
259
|
+
baseUrl: context.baseUrl,
|
|
260
|
+
token: await configuredApiToken(context)
|
|
261
|
+
}), projectSlug);
|
|
262
|
+
const settings = {
|
|
263
|
+
project: project.slug,
|
|
264
|
+
include: ["**"],
|
|
265
|
+
exclude: [],
|
|
266
|
+
ignoreKeys: []
|
|
267
|
+
};
|
|
268
|
+
await writeFile(settingsFile, `${JSON.stringify(settings, null, 2)}\n`, { flag: "wx" });
|
|
269
|
+
return {
|
|
270
|
+
json: {
|
|
271
|
+
settingsFile,
|
|
272
|
+
project: {
|
|
273
|
+
id: project.id,
|
|
274
|
+
slug: project.slug,
|
|
275
|
+
name: project.name
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
text: `Linked this directory to the project ${project.name} (${project.slug}) in ${settingsFile}\n`,
|
|
279
|
+
exitCode: ExitCode.success
|
|
280
|
+
};
|
|
281
|
+
};
|
|
282
|
+
//#endregion
|
|
283
|
+
//#region src/pasted-api-token.ts
|
|
284
|
+
const Key = {
|
|
285
|
+
carriageReturn: "\r",
|
|
286
|
+
lineFeed: "\n",
|
|
287
|
+
interrupt: "",
|
|
288
|
+
delete: "",
|
|
289
|
+
backspace: "\b"
|
|
290
|
+
};
|
|
291
|
+
const isTerminal = (input) => input.isTTY === true && typeof input.setRawMode === "function";
|
|
292
|
+
const readLine = async (input) => {
|
|
293
|
+
const lines = createInterface({
|
|
294
|
+
input,
|
|
295
|
+
crlfDelay: Infinity
|
|
296
|
+
});
|
|
297
|
+
for await (const line of lines) return line;
|
|
298
|
+
return "";
|
|
299
|
+
};
|
|
300
|
+
const readLineWithoutEcho = (input, output) => new Promise((resolve, reject) => {
|
|
301
|
+
let typedCharacters = "";
|
|
302
|
+
const endHiddenInput = () => {
|
|
303
|
+
input.off("data", onData);
|
|
304
|
+
input.setRawMode(false);
|
|
305
|
+
input.pause();
|
|
306
|
+
output.write("\n");
|
|
307
|
+
};
|
|
308
|
+
const onData = (chunk) => {
|
|
309
|
+
for (const character of chunk.toString()) switch (character) {
|
|
310
|
+
case Key.carriageReturn:
|
|
311
|
+
case Key.lineFeed:
|
|
312
|
+
endHiddenInput();
|
|
313
|
+
resolve(typedCharacters);
|
|
314
|
+
return;
|
|
315
|
+
case Key.interrupt:
|
|
316
|
+
endHiddenInput();
|
|
317
|
+
reject(new CliError("Login canceled."));
|
|
318
|
+
return;
|
|
319
|
+
case Key.delete:
|
|
320
|
+
case Key.backspace:
|
|
321
|
+
typedCharacters = typedCharacters.slice(0, -1);
|
|
322
|
+
break;
|
|
323
|
+
default: typedCharacters += character;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
input.setRawMode(true);
|
|
327
|
+
input.on("data", onData);
|
|
328
|
+
input.resume();
|
|
329
|
+
});
|
|
330
|
+
const readPastedApiToken = async (invocation) => {
|
|
331
|
+
invocation.stderr.write("Paste your API token: ");
|
|
332
|
+
const { stdin } = invocation;
|
|
333
|
+
const token = (isTerminal(stdin) ? await readLineWithoutEcho(stdin, invocation.stderr) : await readLine(stdin)).trim();
|
|
334
|
+
if (token === "") throw new CliError("No API token was pasted.");
|
|
335
|
+
return token;
|
|
336
|
+
};
|
|
337
|
+
//#endregion
|
|
338
|
+
//#region src/commands/login.ts
|
|
339
|
+
const login = async (context) => {
|
|
340
|
+
const token = context.tokenOption ?? await readPastedApiToken(context.invocation);
|
|
341
|
+
await new AdminApiClient({
|
|
342
|
+
baseUrl: context.baseUrl,
|
|
343
|
+
token
|
|
344
|
+
}).listProjects();
|
|
345
|
+
const file = await saveApiToken(context.invocation, token);
|
|
346
|
+
return {
|
|
347
|
+
json: { tokenFile: file },
|
|
348
|
+
text: `Logged in. The API token is saved in ${file}\n`,
|
|
349
|
+
exitCode: ExitCode.success
|
|
350
|
+
};
|
|
351
|
+
};
|
|
352
|
+
//#endregion
|
|
353
|
+
//#region src/plural.ts
|
|
354
|
+
const plural = (count, singular, pluralForm) => count === 1 ? singular : pluralForm;
|
|
355
|
+
//#endregion
|
|
356
|
+
//#region src/scan/source-languages.ts
|
|
357
|
+
const languageOfExtension = {
|
|
358
|
+
".js": "javascript",
|
|
359
|
+
".jsx": "javascript",
|
|
360
|
+
".mjs": "javascript",
|
|
361
|
+
".cjs": "javascript",
|
|
362
|
+
".ts": "javascript",
|
|
363
|
+
".tsx": "javascript",
|
|
364
|
+
".mts": "javascript",
|
|
365
|
+
".cts": "javascript",
|
|
366
|
+
".vue": "javascript",
|
|
367
|
+
".svelte": "javascript",
|
|
368
|
+
".dart": "dart",
|
|
369
|
+
".swift": "swift",
|
|
370
|
+
".kt": "kotlin",
|
|
371
|
+
".java": "java",
|
|
372
|
+
".py": "python",
|
|
373
|
+
".cs": "csharp",
|
|
374
|
+
".go": "go"
|
|
375
|
+
};
|
|
376
|
+
const isSourceExtension = (extension) => Object.hasOwn(languageOfExtension, extension);
|
|
377
|
+
const languageSyntax = {
|
|
378
|
+
javascript: {
|
|
379
|
+
lineComment: "//",
|
|
380
|
+
blockComments: true,
|
|
381
|
+
angleBracketGenerics: true
|
|
382
|
+
},
|
|
383
|
+
dart: {
|
|
384
|
+
lineComment: "//",
|
|
385
|
+
blockComments: true,
|
|
386
|
+
angleBracketGenerics: true
|
|
387
|
+
},
|
|
388
|
+
swift: {
|
|
389
|
+
lineComment: "//",
|
|
390
|
+
blockComments: true,
|
|
391
|
+
angleBracketGenerics: true
|
|
392
|
+
},
|
|
393
|
+
kotlin: {
|
|
394
|
+
lineComment: "//",
|
|
395
|
+
blockComments: true,
|
|
396
|
+
angleBracketGenerics: true
|
|
397
|
+
},
|
|
398
|
+
java: {
|
|
399
|
+
lineComment: "//",
|
|
400
|
+
blockComments: true,
|
|
401
|
+
angleBracketGenerics: true
|
|
402
|
+
},
|
|
403
|
+
python: {
|
|
404
|
+
lineComment: "#",
|
|
405
|
+
blockComments: false,
|
|
406
|
+
angleBracketGenerics: false
|
|
407
|
+
},
|
|
408
|
+
csharp: {
|
|
409
|
+
lineComment: "//",
|
|
410
|
+
blockComments: true,
|
|
411
|
+
angleBracketGenerics: true
|
|
412
|
+
},
|
|
413
|
+
go: {
|
|
414
|
+
lineComment: "//",
|
|
415
|
+
blockComments: true,
|
|
416
|
+
angleBracketGenerics: false
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
//#endregion
|
|
420
|
+
//#region src/scan/source-code.ts
|
|
421
|
+
const closerOfOpener = {
|
|
422
|
+
"(": ")",
|
|
423
|
+
"[": "]",
|
|
424
|
+
"{": "}"
|
|
425
|
+
};
|
|
426
|
+
const closers = new Set(Object.values(closerOfOpener));
|
|
427
|
+
const quotes = new Set([
|
|
428
|
+
"\"",
|
|
429
|
+
"'",
|
|
430
|
+
"`"
|
|
431
|
+
]);
|
|
432
|
+
const OPENERS_SEARCHED_FOR_A_CLOSER = 8;
|
|
433
|
+
const isAsciiLetter = (character) => character >= "a" && character <= "z" || character >= "A" && character <= "Z";
|
|
434
|
+
const isDigit = (character) => character >= "0" && character <= "9";
|
|
435
|
+
const isIdentifierStart = (character) => isAsciiLetter(character) || character === "_" || character === "$" || character > "";
|
|
436
|
+
const isIdentifierPart = (character) => isIdentifierStart(character) || isDigit(character);
|
|
437
|
+
const isWhitespace = (character) => character === " " || character === " " || character === "\n" || character === "\r" || character === "\f";
|
|
438
|
+
const endOfLine = (source, start) => {
|
|
439
|
+
const newline = source.indexOf("\n", start);
|
|
440
|
+
return newline === -1 ? source.length : newline;
|
|
441
|
+
};
|
|
442
|
+
const endOfComment = (source, start, syntax) => {
|
|
443
|
+
if (source.startsWith(syntax.lineComment, start)) return endOfLine(source, start);
|
|
444
|
+
if (syntax.blockComments && source.startsWith("/*", start)) {
|
|
445
|
+
const close = source.indexOf("*/", start + 2);
|
|
446
|
+
return close === -1 ? source.length : close + 2;
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
const endOfStringLiteral = (source, start) => {
|
|
450
|
+
const quote = source.charAt(start);
|
|
451
|
+
const tripleQuote = quote.repeat(3);
|
|
452
|
+
if (quote !== "`" && source.startsWith(tripleQuote, start)) {
|
|
453
|
+
const close = source.indexOf(tripleQuote, start + 3);
|
|
454
|
+
return close === -1 ? source.length : close + 3;
|
|
455
|
+
}
|
|
456
|
+
let index = start + 1;
|
|
457
|
+
while (index < source.length) {
|
|
458
|
+
const character = source.charAt(index);
|
|
459
|
+
if (character === "\\") index += 2;
|
|
460
|
+
else if (character === quote) return index + 1;
|
|
461
|
+
else if (character === "\n" && quote !== "`") return index;
|
|
462
|
+
else index += 1;
|
|
463
|
+
}
|
|
464
|
+
return source.length;
|
|
465
|
+
};
|
|
466
|
+
const endOfCharactersWhere = (source, start, belongs) => {
|
|
467
|
+
let index = start;
|
|
468
|
+
while (index < source.length && belongs(index)) index += 1;
|
|
469
|
+
return index;
|
|
470
|
+
};
|
|
471
|
+
const endOfNumber = (source, start) => endOfCharactersWhere(source, start, (index) => isIdentifierPart(source.charAt(index)) || source.charAt(index) === "." && isDigit(source.charAt(index + 1)));
|
|
472
|
+
const tokenize = (source, syntax) => {
|
|
473
|
+
const tokens = [];
|
|
474
|
+
const push = (kind, start, end) => tokens.push({
|
|
475
|
+
kind,
|
|
476
|
+
text: source.slice(start, end),
|
|
477
|
+
start,
|
|
478
|
+
end
|
|
479
|
+
});
|
|
480
|
+
let index = 0;
|
|
481
|
+
while (index < source.length) {
|
|
482
|
+
const character = source.charAt(index);
|
|
483
|
+
const commentEnd = endOfComment(source, index, syntax);
|
|
484
|
+
let end;
|
|
485
|
+
if (isWhitespace(character)) end = index + 1;
|
|
486
|
+
else if (commentEnd !== void 0) end = commentEnd;
|
|
487
|
+
else if (quotes.has(character)) {
|
|
488
|
+
end = endOfStringLiteral(source, index);
|
|
489
|
+
push("string", index, end);
|
|
490
|
+
} else if (isIdentifierStart(character)) {
|
|
491
|
+
end = endOfCharactersWhere(source, index, (position) => isIdentifierPart(source.charAt(position)));
|
|
492
|
+
push("identifier", index, end);
|
|
493
|
+
} else if (isDigit(character)) {
|
|
494
|
+
end = endOfNumber(source, index);
|
|
495
|
+
push("number", index, end);
|
|
496
|
+
} else {
|
|
497
|
+
end = index + 1;
|
|
498
|
+
push("punctuation", index, end);
|
|
499
|
+
}
|
|
500
|
+
index = end;
|
|
501
|
+
}
|
|
502
|
+
return tokens;
|
|
503
|
+
};
|
|
504
|
+
const matchedGroups = (tokens) => {
|
|
505
|
+
const closerIndex = new Int32Array(tokens.length).fill(-1);
|
|
506
|
+
const openerIndex = new Int32Array(tokens.length).fill(-1);
|
|
507
|
+
const openOpeners = [];
|
|
508
|
+
tokens.forEach((token, index) => {
|
|
509
|
+
if (token.kind !== "punctuation") return;
|
|
510
|
+
if (Object.hasOwn(closerOfOpener, token.text)) {
|
|
511
|
+
openOpeners.push(index);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (!closers.has(token.text)) return;
|
|
515
|
+
const lowestSearched = Math.max(0, openOpeners.length - OPENERS_SEARCHED_FOR_A_CLOSER);
|
|
516
|
+
for (let position = openOpeners.length - 1; position >= lowestSearched; position -= 1) {
|
|
517
|
+
const opener = openOpeners[position];
|
|
518
|
+
if (closerOfOpener[tokens[opener].text] === token.text) {
|
|
519
|
+
closerIndex[opener] = index;
|
|
520
|
+
openerIndex[index] = opener;
|
|
521
|
+
openOpeners.length = position;
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
return {
|
|
527
|
+
closerIndex,
|
|
528
|
+
openerIndex
|
|
529
|
+
};
|
|
530
|
+
};
|
|
531
|
+
var SourceCode = class {
|
|
532
|
+
tokens;
|
|
533
|
+
syntax;
|
|
534
|
+
groups;
|
|
535
|
+
lineStarts;
|
|
536
|
+
constructor(source, language) {
|
|
537
|
+
this.source = source;
|
|
538
|
+
this.language = language;
|
|
539
|
+
this.syntax = languageSyntax[language];
|
|
540
|
+
this.tokens = tokenize(source, this.syntax);
|
|
541
|
+
this.groups = matchedGroups(this.tokens);
|
|
542
|
+
}
|
|
543
|
+
token(index) {
|
|
544
|
+
return this.tokens[index];
|
|
545
|
+
}
|
|
546
|
+
isPunctuation(index, text) {
|
|
547
|
+
const token = this.tokens[index];
|
|
548
|
+
return token?.kind === "punctuation" && token.text === text;
|
|
549
|
+
}
|
|
550
|
+
isIdentifier(index, text) {
|
|
551
|
+
const token = this.tokens[index];
|
|
552
|
+
return token?.kind === "identifier" && token.text === text;
|
|
553
|
+
}
|
|
554
|
+
openerOfLastToken(range, closer) {
|
|
555
|
+
const opener = this.groups.openerIndex[range.last] ?? -1;
|
|
556
|
+
return this.isPunctuation(range.last, closer) && opener >= range.first ? opener : void 0;
|
|
557
|
+
}
|
|
558
|
+
firstTopLevelIndex(range, isWanted) {
|
|
559
|
+
let index = range.first;
|
|
560
|
+
while (index <= range.last) {
|
|
561
|
+
if (isWanted(index)) return index;
|
|
562
|
+
index = (this.closerOf(index) ?? index) + 1;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
closerOf(openerIndex) {
|
|
566
|
+
const closer = this.groups.closerIndex[openerIndex] ?? -1;
|
|
567
|
+
return closer === -1 ? void 0 : closer;
|
|
568
|
+
}
|
|
569
|
+
textOf(range) {
|
|
570
|
+
const first = this.tokens[range.first];
|
|
571
|
+
const last = this.tokens[range.last];
|
|
572
|
+
return this.source.slice(first.start, last.end);
|
|
573
|
+
}
|
|
574
|
+
commaSeparatedItems(openerIndex) {
|
|
575
|
+
const closer = this.closerOf(openerIndex);
|
|
576
|
+
if (closer === void 0) return;
|
|
577
|
+
const items = [];
|
|
578
|
+
let itemStart = openerIndex + 1;
|
|
579
|
+
let angleBracketDepth = 0;
|
|
580
|
+
let index = openerIndex + 1;
|
|
581
|
+
while (index < closer) {
|
|
582
|
+
const token = this.tokens[index];
|
|
583
|
+
if (token.kind === "punctuation") {
|
|
584
|
+
if (token.text === "," && angleBracketDepth === 0) {
|
|
585
|
+
items.push({
|
|
586
|
+
first: itemStart,
|
|
587
|
+
last: index - 1
|
|
588
|
+
});
|
|
589
|
+
itemStart = index + 1;
|
|
590
|
+
} else if (this.opensTypeArguments(index)) angleBracketDepth += 1;
|
|
591
|
+
else if (token.text === ">" && angleBracketDepth > 0) angleBracketDepth -= 1;
|
|
592
|
+
}
|
|
593
|
+
index = (this.closerOf(index) ?? index) + 1;
|
|
594
|
+
}
|
|
595
|
+
if (itemStart < closer) items.push({
|
|
596
|
+
first: itemStart,
|
|
597
|
+
last: closer - 1
|
|
598
|
+
});
|
|
599
|
+
return items;
|
|
600
|
+
}
|
|
601
|
+
lineOf(tokenIndex) {
|
|
602
|
+
const offset = this.tokens[tokenIndex].start;
|
|
603
|
+
const lineStarts = this.knownLineStarts();
|
|
604
|
+
let low = 0;
|
|
605
|
+
let high = lineStarts.length - 1;
|
|
606
|
+
while (low < high) {
|
|
607
|
+
const middle = Math.ceil((low + high) / 2);
|
|
608
|
+
if (lineStarts[middle] <= offset) low = middle;
|
|
609
|
+
else high = middle - 1;
|
|
610
|
+
}
|
|
611
|
+
return low + 1;
|
|
612
|
+
}
|
|
613
|
+
startsLine(tokenIndex) {
|
|
614
|
+
const token = this.tokens[tokenIndex];
|
|
615
|
+
const previous = this.tokens[tokenIndex - 1];
|
|
616
|
+
return token !== void 0 && (previous === void 0 || this.source.slice(previous.end, token.start).includes("\n"));
|
|
617
|
+
}
|
|
618
|
+
characterAfter(tokenIndex) {
|
|
619
|
+
const token = this.tokens[tokenIndex];
|
|
620
|
+
return token === void 0 ? "" : this.source.charAt(token.end);
|
|
621
|
+
}
|
|
622
|
+
opensTypeArguments(index) {
|
|
623
|
+
const previous = this.tokens[index - 1];
|
|
624
|
+
const current = this.tokens[index];
|
|
625
|
+
return this.syntax.angleBracketGenerics && current?.kind === "punctuation" && current.text === "<" && previous?.kind === "identifier" && previous.end === current.start;
|
|
626
|
+
}
|
|
627
|
+
knownLineStarts() {
|
|
628
|
+
if (this.lineStarts === void 0) {
|
|
629
|
+
const lineStarts = [0];
|
|
630
|
+
for (let index = 0; index < this.source.length; index += 1) if (this.source.charAt(index) === "\n") lineStarts.push(index + 1);
|
|
631
|
+
this.lineStarts = lineStarts;
|
|
632
|
+
}
|
|
633
|
+
return this.lineStarts;
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
//#endregion
|
|
637
|
+
//#region src/scan/literals.ts
|
|
638
|
+
const MINIMUM_KEY_LENGTH = 4;
|
|
639
|
+
const MAXIMUM_KEY_LENGTH = 150;
|
|
640
|
+
const NUMBER_LITERAL = /^-?\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d+)?[fFdDmMlL]?$/;
|
|
641
|
+
const FLOAT_MARKERS = [
|
|
642
|
+
".",
|
|
643
|
+
"e",
|
|
644
|
+
"E",
|
|
645
|
+
"f",
|
|
646
|
+
"F",
|
|
647
|
+
"d",
|
|
648
|
+
"D",
|
|
649
|
+
"m",
|
|
650
|
+
"M"
|
|
651
|
+
];
|
|
652
|
+
const isAlphanumeric = (character) => isDigit(character) || isAsciiLetter(character);
|
|
653
|
+
const isKeyCharacter = (character) => isAlphanumeric(character) || character === "." || character === "_" || character === "-";
|
|
654
|
+
const fitsKeyGrammar = (text) => text.length >= MINIMUM_KEY_LENGTH && text.length <= MAXIMUM_KEY_LENGTH && isAlphanumeric(text.charAt(0)) && [...text].every(isKeyCharacter);
|
|
655
|
+
const singleToken = (code, range) => range.first === range.last ? code.token(range.first) : void 0;
|
|
656
|
+
const plainStringContent = (code, range) => {
|
|
657
|
+
const token = singleToken(code, range);
|
|
658
|
+
if (token?.kind !== "string" || token.text.length < 2) return;
|
|
659
|
+
const quote = token.text.charAt(0);
|
|
660
|
+
const content = token.text.slice(1, -1);
|
|
661
|
+
return token.text.endsWith(quote) && !content.includes(quote) && !content.includes("\\") && !content.includes("$") ? content : void 0;
|
|
662
|
+
};
|
|
663
|
+
const literalTextOf = (code, expression) => expression.isName ? code.textOf(expression.range) : plainStringContent(code, expression.range);
|
|
664
|
+
const keyLiteralOf = (code, expression) => {
|
|
665
|
+
const content = literalTextOf(code, expression);
|
|
666
|
+
if (content === void 0) return { kind: "notLiteral" };
|
|
667
|
+
return fitsKeyGrammar(content) ? {
|
|
668
|
+
kind: "key",
|
|
669
|
+
key: content
|
|
670
|
+
} : { kind: "outsideKeyGrammar" };
|
|
671
|
+
};
|
|
672
|
+
const isLiteralGroup = (code, range) => (code.isPunctuation(range.first, "{") || code.isPunctuation(range.first, "[")) && code.closerOf(range.first) === range.last;
|
|
673
|
+
const numberLiteralOf = (text) => {
|
|
674
|
+
if (!NUMBER_LITERAL.test(text)) return;
|
|
675
|
+
const value = Number(text.replace(/[_fFdDmMlL]/g, ""));
|
|
676
|
+
if (!Number.isFinite(value)) return;
|
|
677
|
+
return FLOAT_MARKERS.some((marker) => text.includes(marker)) ? {
|
|
678
|
+
type: "float",
|
|
679
|
+
value
|
|
680
|
+
} : {
|
|
681
|
+
type: "integer",
|
|
682
|
+
value
|
|
683
|
+
};
|
|
684
|
+
};
|
|
685
|
+
const inCodeDefaultValueOf = (code, range) => {
|
|
686
|
+
const text = code.textOf(range);
|
|
687
|
+
if (text === "true" || text === "True") return {
|
|
688
|
+
type: "boolean",
|
|
689
|
+
value: true
|
|
690
|
+
};
|
|
691
|
+
if (text === "false" || text === "False") return {
|
|
692
|
+
type: "boolean",
|
|
693
|
+
value: false
|
|
694
|
+
};
|
|
695
|
+
const stringContent = plainStringContent(code, range);
|
|
696
|
+
if (stringContent !== void 0) return {
|
|
697
|
+
type: "string",
|
|
698
|
+
value: stringContent
|
|
699
|
+
};
|
|
700
|
+
if (isLiteralGroup(code, range)) return {
|
|
701
|
+
type: "json",
|
|
702
|
+
value: text
|
|
703
|
+
};
|
|
704
|
+
return numberLiteralOf(text);
|
|
705
|
+
};
|
|
706
|
+
//#endregion
|
|
707
|
+
//#region src/scan/constant-index.ts
|
|
708
|
+
const TOKENS_ENDING_AN_ASSIGNED_LITERAL = new Set([
|
|
709
|
+
";",
|
|
710
|
+
",",
|
|
711
|
+
")",
|
|
712
|
+
"}",
|
|
713
|
+
"]"
|
|
714
|
+
]);
|
|
715
|
+
const endsAssignedLiteral = (code, stringIndex) => {
|
|
716
|
+
const next = code.token(stringIndex + 1);
|
|
717
|
+
return next === void 0 || next.kind === "punctuation" && TOKENS_ENDING_AN_ASSIGNED_LITERAL.has(next.text) || code.isIdentifier(stringIndex + 1, "as") || code.startsLine(stringIndex + 1);
|
|
718
|
+
};
|
|
719
|
+
const identifierAt = (code, index) => {
|
|
720
|
+
const token = code.token(index);
|
|
721
|
+
return token?.kind === "identifier" ? token.text : void 0;
|
|
722
|
+
};
|
|
723
|
+
const nameAssignedAt = (code, equalsIndex) => {
|
|
724
|
+
const nameOrType = identifierAt(code, equalsIndex - 1);
|
|
725
|
+
if (nameOrType !== void 0) return (code.isPunctuation(equalsIndex - 2, ":") ? identifierAt(code, equalsIndex - 3) : void 0) ?? nameOrType;
|
|
726
|
+
return code.isPunctuation(equalsIndex - 1, ":") && code.characterAfter(equalsIndex - 1) === "=" ? identifierAt(code, equalsIndex - 2) : void 0;
|
|
727
|
+
};
|
|
728
|
+
const nameHoldingLiteralAt = (code, stringIndex) => {
|
|
729
|
+
if (code.isPunctuation(stringIndex - 1, "=")) return nameAssignedAt(code, stringIndex - 1);
|
|
730
|
+
return code.isPunctuation(stringIndex - 1, ":") ? identifierAt(code, stringIndex - 2) : void 0;
|
|
731
|
+
};
|
|
732
|
+
const constantAssignmentsIn = (code) => code.tokens.flatMap((token, index) => {
|
|
733
|
+
if (token.kind !== "string" || !endsAssignedLiteral(code, index)) return [];
|
|
734
|
+
const key = plainStringContent(code, {
|
|
735
|
+
first: index,
|
|
736
|
+
last: index
|
|
737
|
+
});
|
|
738
|
+
if (key === void 0 || !fitsKeyGrammar(key)) return [];
|
|
739
|
+
const name = nameHoldingLiteralAt(code, index);
|
|
740
|
+
return name === void 0 ? [] : [{
|
|
741
|
+
name,
|
|
742
|
+
key,
|
|
743
|
+
tokenIndex: index
|
|
744
|
+
}];
|
|
745
|
+
});
|
|
746
|
+
var ConstantIndex = class {
|
|
747
|
+
keyOfName = /* @__PURE__ */ new Map();
|
|
748
|
+
add(assignment) {
|
|
749
|
+
const indexed = this.keyOfName.get(assignment.name);
|
|
750
|
+
if (indexed === void 0) this.keyOfName.set(assignment.name, {
|
|
751
|
+
kind: "key",
|
|
752
|
+
key: assignment.key
|
|
753
|
+
});
|
|
754
|
+
else if (indexed.kind === "key" && indexed.key !== assignment.key) this.keyOfName.set(assignment.name, { kind: "ambiguous" });
|
|
755
|
+
}
|
|
756
|
+
keyOf(name) {
|
|
757
|
+
const indexed = this.keyOfName.get(name);
|
|
758
|
+
return indexed?.kind === "key" ? indexed.key : void 0;
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
//#endregion
|
|
762
|
+
//#region src/scan/known-key-texts.ts
|
|
763
|
+
const knownKeyTextsIn = (code, projectKeys, tokensReadByCalls) => code.tokens.flatMap((token, index) => {
|
|
764
|
+
if (token.kind !== "string" || tokensReadByCalls.has(index)) return [];
|
|
765
|
+
const content = plainStringContent(code, {
|
|
766
|
+
first: index,
|
|
767
|
+
last: index
|
|
768
|
+
});
|
|
769
|
+
return content !== void 0 && projectKeys.has(content) ? [{
|
|
770
|
+
tokenIndex: index,
|
|
771
|
+
key: content
|
|
772
|
+
}] : [];
|
|
773
|
+
});
|
|
774
|
+
//#endregion
|
|
775
|
+
//#region src/scan/nearest-declarations.ts
|
|
776
|
+
var NearestDeclarations = class {
|
|
777
|
+
declarationOfDirectory = /* @__PURE__ */ new Map();
|
|
778
|
+
constructor(highestDirectory, declaredIn) {
|
|
779
|
+
this.highestDirectory = highestDirectory;
|
|
780
|
+
this.declaredIn = declaredIn;
|
|
781
|
+
}
|
|
782
|
+
declarationAbove(file) {
|
|
783
|
+
return this.declarationAtOrAbove(dirname(file));
|
|
784
|
+
}
|
|
785
|
+
declarationAtOrAbove(directory) {
|
|
786
|
+
const declarationLookedUpBefore = this.declarationOfDirectory.get(directory);
|
|
787
|
+
if (declarationLookedUpBefore !== void 0) return declarationLookedUpBefore;
|
|
788
|
+
const parent = dirname(directory);
|
|
789
|
+
const isLastSearched = directory === this.highestDirectory || parent === directory;
|
|
790
|
+
const declaration = this.declaredIn(directory).then((declarationHere) => declarationHere ?? (isLastSearched ? void 0 : this.declarationAtOrAbove(parent)));
|
|
791
|
+
this.declarationOfDirectory.set(directory, declaration);
|
|
792
|
+
return declaration;
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
//#endregion
|
|
796
|
+
//#region src/scan/scanned-files.ts
|
|
797
|
+
const EXCLUDED_DIRECTORY_NAMES = new Set([
|
|
798
|
+
".git",
|
|
799
|
+
"node_modules",
|
|
800
|
+
"vendor",
|
|
801
|
+
"build",
|
|
802
|
+
"dist",
|
|
803
|
+
"target"
|
|
804
|
+
]);
|
|
805
|
+
const MAXIMUM_FILE_BYTES = 1024 * 1024;
|
|
806
|
+
const BYTES_CHECKED_FOR_BINARY_CONTENT = 8e3;
|
|
807
|
+
const slashSeparated = (path) => path.split(sep).join("/");
|
|
808
|
+
const isInside = (directory, path) => {
|
|
809
|
+
const pathFromDirectory = relative(directory, path);
|
|
810
|
+
return pathFromDirectory !== "" && !pathFromDirectory.startsWith("..") && !isAbsolute(pathFromDirectory);
|
|
811
|
+
};
|
|
812
|
+
const scanRootAt = async (workingDirectory, path) => {
|
|
813
|
+
const rootPath = resolve(workingDirectory, path ?? ".");
|
|
814
|
+
let rootStats;
|
|
815
|
+
try {
|
|
816
|
+
rootStats = await stat(rootPath);
|
|
817
|
+
} catch (error) {
|
|
818
|
+
if (error.code === "ENOENT") throw new CliError(`There is no file or directory at ${path} to scan.`);
|
|
819
|
+
throw error;
|
|
820
|
+
}
|
|
821
|
+
return {
|
|
822
|
+
path: rootPath,
|
|
823
|
+
isDirectory: rootStats.isDirectory(),
|
|
824
|
+
description: path ?? "the current directory"
|
|
825
|
+
};
|
|
826
|
+
};
|
|
827
|
+
const gitignoreRulesIn = async (directory) => {
|
|
828
|
+
const contents = await readFileIfPresent(join(directory, ".gitignore"));
|
|
829
|
+
return contents === void 0 ? [] : [{
|
|
830
|
+
directory,
|
|
831
|
+
rules: ignore().add(contents)
|
|
832
|
+
}];
|
|
833
|
+
};
|
|
834
|
+
const gitRepositoryRootAbove = async (path) => {
|
|
835
|
+
for (let directory = path;; directory = dirname(directory)) {
|
|
836
|
+
if (await pathExists(join(directory, ".git"))) return directory;
|
|
837
|
+
if (dirname(directory) === directory) return;
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
const ancestorDirectoriesBelow = (highestDirectory, path) => {
|
|
841
|
+
const ancestors = [];
|
|
842
|
+
for (let directory = dirname(path); isInside(highestDirectory, directory); directory = dirname(directory)) ancestors.unshift(directory);
|
|
843
|
+
return isInside(highestDirectory, path) ? [highestDirectory, ...ancestors] : [];
|
|
844
|
+
};
|
|
845
|
+
const isGitignored = (path, isDirectory, rulesInScope) => {
|
|
846
|
+
for (const { directory, rules } of [...rulesInScope].reverse()) {
|
|
847
|
+
if (!isInside(directory, path)) continue;
|
|
848
|
+
const pathFromDirectory = slashSeparated(relative(directory, path));
|
|
849
|
+
const result = rules.test(isDirectory ? `${pathFromDirectory}/` : pathFromDirectory);
|
|
850
|
+
if (result.ignored || result.unignored) return result.ignored;
|
|
851
|
+
}
|
|
852
|
+
return false;
|
|
853
|
+
};
|
|
854
|
+
const readFileWithinSizeLimit = async (path) => {
|
|
855
|
+
const stats = await stat(path).catch(() => void 0);
|
|
856
|
+
return stats?.isFile() && stats.size <= MAXIMUM_FILE_BYTES ? readFile(path, "utf8") : void 0;
|
|
857
|
+
};
|
|
858
|
+
const hasBinaryContent = (contents) => contents.subarray(0, BYTES_CHECKED_FOR_BINARY_CONTENT).includes(0);
|
|
859
|
+
async function* candidateFiles(directory, rulesInScope) {
|
|
860
|
+
const rulesHere = [...rulesInScope, ...await gitignoreRulesIn(directory)];
|
|
861
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
862
|
+
for (const entry of entries.sort((first, second) => first.name < second.name ? -1 : 1)) {
|
|
863
|
+
const path = join(directory, entry.name);
|
|
864
|
+
if (entry.isDirectory()) {
|
|
865
|
+
if (!EXCLUDED_DIRECTORY_NAMES.has(entry.name) && !isGitignored(path, true, rulesHere)) yield* candidateFiles(path, rulesHere);
|
|
866
|
+
} else if (entry.isFile() && !isGitignored(path, false, rulesHere)) yield path;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
async function* scannedFiles(scope) {
|
|
870
|
+
const isIncluded = scope.include === "everyFile" ? () => true : picomatch([...scope.include], { dot: true });
|
|
871
|
+
const isExcluded = picomatch([...scope.exclude], { dot: true });
|
|
872
|
+
const highestGitignoreDirectory = await gitRepositoryRootAbove(scope.root.path) ?? scope.workingDirectory;
|
|
873
|
+
const ancestorRules = (await Promise.all(ancestorDirectoriesBelow(highestGitignoreDirectory, scope.root.path).map(gitignoreRulesIn))).flat();
|
|
874
|
+
const candidates = scope.root.isDirectory ? candidateFiles(scope.root.path, ancestorRules) : (async function* () {
|
|
875
|
+
if (!isGitignored(scope.root.path, false, ancestorRules)) yield scope.root.path;
|
|
876
|
+
})();
|
|
877
|
+
for await (const path of candidates) {
|
|
878
|
+
const extension = extname(path);
|
|
879
|
+
const displayPath = slashSeparated(relative(scope.workingDirectory, path));
|
|
880
|
+
if (!isSourceExtension(extension) || !isIncluded(displayPath) || isExcluded(displayPath)) continue;
|
|
881
|
+
if ((await stat(path)).size > MAXIMUM_FILE_BYTES) continue;
|
|
882
|
+
const contents = await readFile(path);
|
|
883
|
+
if (!hasBinaryContent(contents)) yield {
|
|
884
|
+
path,
|
|
885
|
+
displayPath,
|
|
886
|
+
extension,
|
|
887
|
+
contents: contents.toString("utf8")
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
//#endregion
|
|
892
|
+
//#region src/scan/path-aliases.ts
|
|
893
|
+
const WILDCARD = "*";
|
|
894
|
+
const EXACT_MATCH_RANK = Number.POSITIVE_INFINITY;
|
|
895
|
+
const JSON_EXTENSION = ".json";
|
|
896
|
+
const BYTE_ORDER_MARK = "";
|
|
897
|
+
const TYPESCRIPT_CONFIG_FILE = "tsconfig.json";
|
|
898
|
+
const CONFIG_FILE_NAMES = [TYPESCRIPT_CONFIG_FILE, "jsconfig.json"];
|
|
899
|
+
const NO_ALIAS_OPTIONS = {
|
|
900
|
+
paths: void 0,
|
|
901
|
+
baseUrl: void 0
|
|
902
|
+
};
|
|
903
|
+
const TypeScriptConfigSchema = v.object({
|
|
904
|
+
extends: v.optional(v.union([v.string(), v.array(v.string())])),
|
|
905
|
+
compilerOptions: v.optional(v.object({
|
|
906
|
+
baseUrl: v.optional(v.string()),
|
|
907
|
+
paths: v.optional(v.record(v.string(), v.array(v.string())))
|
|
908
|
+
})),
|
|
909
|
+
references: v.optional(v.array(v.object({ path: v.string() })))
|
|
910
|
+
});
|
|
911
|
+
const pathPatternsOf = (targetsOfPattern) => Object.entries(targetsOfPattern).flatMap(([key, targets]) => {
|
|
912
|
+
const wildcard = key.indexOf(WILDCARD);
|
|
913
|
+
if (wildcard === -1) return [{
|
|
914
|
+
kind: "exact",
|
|
915
|
+
key,
|
|
916
|
+
targets
|
|
917
|
+
}];
|
|
918
|
+
return key.includes(WILDCARD, wildcard + 1) ? [] : [{
|
|
919
|
+
kind: "wildcard",
|
|
920
|
+
prefix: key.slice(0, wildcard),
|
|
921
|
+
suffix: key.slice(wildcard + 1),
|
|
922
|
+
targets
|
|
923
|
+
}];
|
|
924
|
+
});
|
|
925
|
+
const aliasMatchOf = (pattern, specifier) => {
|
|
926
|
+
switch (pattern.kind) {
|
|
927
|
+
case "exact": return specifier === pattern.key ? {
|
|
928
|
+
rank: EXACT_MATCH_RANK,
|
|
929
|
+
targets: pattern.targets
|
|
930
|
+
} : void 0;
|
|
931
|
+
case "wildcard": {
|
|
932
|
+
const fits = specifier.length >= pattern.prefix.length + pattern.suffix.length && specifier.startsWith(pattern.prefix) && specifier.endsWith(pattern.suffix);
|
|
933
|
+
const wildcardText = specifier.slice(pattern.prefix.length, specifier.length - pattern.suffix.length);
|
|
934
|
+
return fits ? {
|
|
935
|
+
rank: pattern.prefix.length,
|
|
936
|
+
targets: pattern.targets.map((target) => target.replace(WILDCARD, () => wildcardText))
|
|
937
|
+
} : void 0;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
const bestAliasMatch = (patterns, specifier) => patterns.reduce((best, pattern) => {
|
|
942
|
+
const match = aliasMatchOf(pattern, specifier);
|
|
943
|
+
return match !== void 0 && (best === void 0 || match.rank > best.rank) ? match : best;
|
|
944
|
+
}, void 0);
|
|
945
|
+
const mappedModulePaths = (pathMapping, specifier) => bestAliasMatch(pathMapping.patterns, specifier)?.targets.map((target) => resolve(pathMapping.targetDirectory, target));
|
|
946
|
+
const modulePathsIn = ({ pathMapping, baseUrl }, specifier) => {
|
|
947
|
+
const mappedPaths = pathMapping === void 0 ? void 0 : mappedModulePaths(pathMapping, specifier);
|
|
948
|
+
if (mappedPaths !== void 0) return mappedPaths;
|
|
949
|
+
return baseUrl === void 0 ? [] : [resolve(baseUrl, specifier)];
|
|
950
|
+
};
|
|
951
|
+
const isTrailingComma = (code, index) => code.isPunctuation(index, ",") && (code.isPunctuation(index + 1, "}") || code.isPunctuation(index + 1, "]"));
|
|
952
|
+
const jsonOfConfigFile = (contents) => {
|
|
953
|
+
const code = new SourceCode(contents.startsWith(BYTE_ORDER_MARK) ? contents.slice(1) : contents, "javascript");
|
|
954
|
+
return code.tokens.map((token, index) => {
|
|
955
|
+
return " ".repeat(token.start - (code.token(index - 1)?.end ?? 0)) + (isTrailingComma(code, index) ? " " : token.text);
|
|
956
|
+
}).join("");
|
|
957
|
+
};
|
|
958
|
+
const isRelativeConfigPath = (path) => path.startsWith("./") || path.startsWith("../");
|
|
959
|
+
const extendedConfigFileAt = async (path) => await fileExists(path) || path.endsWith(JSON_EXTENSION) ? path : `${path}${JSON_EXTENSION}`;
|
|
960
|
+
const referencedConfigFileAt = (path) => path.endsWith(JSON_EXTENSION) ? path : join(path, TYPESCRIPT_CONFIG_FILE);
|
|
961
|
+
const typeScriptConfigAt = async (configFile) => {
|
|
962
|
+
const contents = await readFileWithinSizeLimit(configFile);
|
|
963
|
+
const config = v.safeParse(TypeScriptConfigSchema, contents === void 0 ? void 0 : parseJson(jsonOfConfigFile(contents)));
|
|
964
|
+
if (!config.success) return {
|
|
965
|
+
aliasOptions: NO_ALIAS_OPTIONS,
|
|
966
|
+
extendedConfigFiles: [],
|
|
967
|
+
referencedConfigFiles: []
|
|
968
|
+
};
|
|
969
|
+
const { compilerOptions, extends: extendedConfigs, references } = config.output;
|
|
970
|
+
const configDirectory = dirname(configFile);
|
|
971
|
+
const relativeExtendedConfigs = [extendedConfigs ?? []].flat().filter(isRelativeConfigPath);
|
|
972
|
+
return {
|
|
973
|
+
aliasOptions: {
|
|
974
|
+
paths: compilerOptions?.paths === void 0 ? void 0 : {
|
|
975
|
+
targetsOfPattern: compilerOptions.paths,
|
|
976
|
+
configDirectory
|
|
977
|
+
},
|
|
978
|
+
baseUrl: compilerOptions?.baseUrl === void 0 ? void 0 : resolve(configDirectory, compilerOptions.baseUrl)
|
|
979
|
+
},
|
|
980
|
+
extendedConfigFiles: await Promise.all(relativeExtendedConfigs.map((extendedConfig) => extendedConfigFileAt(resolve(configDirectory, extendedConfig)))),
|
|
981
|
+
referencedConfigFiles: (references ?? []).map((reference) => referencedConfigFileAt(resolve(configDirectory, reference.path)))
|
|
982
|
+
};
|
|
983
|
+
};
|
|
984
|
+
const ownOrInherited = (own, inherited) => ({
|
|
985
|
+
paths: own.paths ?? inherited.paths,
|
|
986
|
+
baseUrl: own.baseUrl ?? inherited.baseUrl
|
|
987
|
+
});
|
|
988
|
+
const declaresAliases = (aliasOptions) => aliasOptions.paths !== void 0 || aliasOptions.baseUrl !== void 0;
|
|
989
|
+
const aliasRulesOf = ({ paths, baseUrl }) => ({
|
|
990
|
+
pathMapping: paths === void 0 ? void 0 : {
|
|
991
|
+
patterns: pathPatternsOf(paths.targetsOfPattern),
|
|
992
|
+
targetDirectory: baseUrl ?? paths.configDirectory
|
|
993
|
+
},
|
|
994
|
+
baseUrl
|
|
995
|
+
});
|
|
996
|
+
var TypeScriptConfigs = class {
|
|
997
|
+
configOfFile = /* @__PURE__ */ new Map();
|
|
998
|
+
aliasOptionsOfFile = /* @__PURE__ */ new Map();
|
|
999
|
+
configAt(configFile) {
|
|
1000
|
+
const configReadBefore = this.configOfFile.get(configFile);
|
|
1001
|
+
if (configReadBefore !== void 0) return configReadBefore;
|
|
1002
|
+
const config = typeScriptConfigAt(configFile);
|
|
1003
|
+
this.configOfFile.set(configFile, config);
|
|
1004
|
+
return config;
|
|
1005
|
+
}
|
|
1006
|
+
async aliasOptionsAt(configFile, configFilesExtending = /* @__PURE__ */ new Set()) {
|
|
1007
|
+
const aliasOptionsReadBefore = this.aliasOptionsOfFile.get(configFile);
|
|
1008
|
+
if (aliasOptionsReadBefore !== void 0) return aliasOptionsReadBefore;
|
|
1009
|
+
const config = await this.configAt(configFile);
|
|
1010
|
+
const extendsChain = new Set([...configFilesExtending, configFile]);
|
|
1011
|
+
let inherited = NO_ALIAS_OPTIONS;
|
|
1012
|
+
for (const extendedConfigFile of config.extendedConfigFiles) if (!extendsChain.has(extendedConfigFile)) inherited = ownOrInherited(await this.aliasOptionsAt(extendedConfigFile, extendsChain), inherited);
|
|
1013
|
+
const aliasOptions = ownOrInherited(config.aliasOptions, inherited);
|
|
1014
|
+
this.aliasOptionsOfFile.set(configFile, aliasOptions);
|
|
1015
|
+
return aliasOptions;
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
const configFileIn = async (directory) => {
|
|
1019
|
+
for (const configFileName of CONFIG_FILE_NAMES) {
|
|
1020
|
+
const configFile = join(directory, configFileName);
|
|
1021
|
+
if (await fileExists(configFile)) return configFile;
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
const aliasRulesDeclaredIn = async (directory) => {
|
|
1025
|
+
const configFile = await configFileIn(directory);
|
|
1026
|
+
if (configFile === void 0) return;
|
|
1027
|
+
const configs = new TypeScriptConfigs();
|
|
1028
|
+
const aliasOptions = await configs.aliasOptionsAt(configFile);
|
|
1029
|
+
if (declaresAliases(aliasOptions)) return [aliasRulesOf(aliasOptions)];
|
|
1030
|
+
const { referencedConfigFiles } = await configs.configAt(configFile);
|
|
1031
|
+
return Promise.all(referencedConfigFiles.map(async (referencedConfigFile) => {
|
|
1032
|
+
const { pathMapping } = aliasRulesOf(await configs.aliasOptionsAt(referencedConfigFile));
|
|
1033
|
+
return {
|
|
1034
|
+
pathMapping,
|
|
1035
|
+
baseUrl: void 0
|
|
1036
|
+
};
|
|
1037
|
+
}));
|
|
1038
|
+
};
|
|
1039
|
+
var PathAliases = class {
|
|
1040
|
+
aliasRules;
|
|
1041
|
+
constructor(highestDirectory) {
|
|
1042
|
+
this.aliasRules = new NearestDeclarations(highestDirectory, aliasRulesDeclaredIn);
|
|
1043
|
+
}
|
|
1044
|
+
async modulePathsFor(specifier, importingFile) {
|
|
1045
|
+
return (await this.aliasRules.declarationAbove(importingFile) ?? []).map((rules) => modulePathsIn(rules, specifier)).find((modulePaths) => modulePaths.length > 0) ?? [];
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
//#endregion
|
|
1049
|
+
//#region src/scan/local-imports.ts
|
|
1050
|
+
const JAVASCRIPT_SOURCE_EXTENSIONS = [
|
|
1051
|
+
".ts",
|
|
1052
|
+
".tsx",
|
|
1053
|
+
".js",
|
|
1054
|
+
".jsx",
|
|
1055
|
+
".mjs",
|
|
1056
|
+
".cjs",
|
|
1057
|
+
".mts",
|
|
1058
|
+
".cts",
|
|
1059
|
+
".vue",
|
|
1060
|
+
".svelte"
|
|
1061
|
+
];
|
|
1062
|
+
const TYPESCRIPT_EXTENSIONS_FOR = {
|
|
1063
|
+
".js": [".ts", ".tsx"],
|
|
1064
|
+
".jsx": [".tsx"],
|
|
1065
|
+
".mjs": [".mts"],
|
|
1066
|
+
".cjs": [".cts"]
|
|
1067
|
+
};
|
|
1068
|
+
const PYTHON_PACKAGE_FILE = "__init__.py";
|
|
1069
|
+
const DART_PACKAGE_SCHEME = "package:";
|
|
1070
|
+
const DART_LIBRARY_SCHEME = "dart:";
|
|
1071
|
+
const DART_IMPORT_DIRECTIVES = [
|
|
1072
|
+
"import",
|
|
1073
|
+
"export",
|
|
1074
|
+
"part"
|
|
1075
|
+
];
|
|
1076
|
+
const DART_RE_EXPORT_DIRECTIVES = ["export"];
|
|
1077
|
+
const SWIFT_PACKAGE_MANIFEST = "Package.swift";
|
|
1078
|
+
const SWIFT_PACKAGE_TARGETS_DIRECTORIES = ["Sources", "Tests"];
|
|
1079
|
+
const XCODE_PROJECT_EXTENSION = ".xcodeproj";
|
|
1080
|
+
const stringAt = (code, index) => code.token(index)?.kind === "string" ? plainStringContent(code, {
|
|
1081
|
+
first: index,
|
|
1082
|
+
last: index
|
|
1083
|
+
}) : void 0;
|
|
1084
|
+
const unquoted = (text) => {
|
|
1085
|
+
const trimmed = text.trim();
|
|
1086
|
+
const quote = trimmed.charAt(0);
|
|
1087
|
+
return (quote === "\"" || quote === "'") && trimmed.endsWith(quote) ? trimmed.slice(1, -1) : trimmed;
|
|
1088
|
+
};
|
|
1089
|
+
const directoriesUpFrom = (directory, highestDirectory) => {
|
|
1090
|
+
const directories = [directory];
|
|
1091
|
+
let current = directory;
|
|
1092
|
+
while (current !== highestDirectory && dirname(current) !== current) {
|
|
1093
|
+
current = dirname(current);
|
|
1094
|
+
directories.push(current);
|
|
1095
|
+
}
|
|
1096
|
+
return directories;
|
|
1097
|
+
};
|
|
1098
|
+
const ancestorDirectory = (directory, levelsUp) => {
|
|
1099
|
+
let ancestor = directory;
|
|
1100
|
+
for (let level = 0; level < levelsUp; level += 1) ancestor = dirname(ancestor);
|
|
1101
|
+
return ancestor;
|
|
1102
|
+
};
|
|
1103
|
+
const isRelativeSpecifier = (specifier) => specifier === "." || specifier === ".." || specifier.startsWith("./") || specifier.startsWith("../");
|
|
1104
|
+
const javascriptSpecifiers = (code) => code.tokens.flatMap((_token, index) => {
|
|
1105
|
+
const specifier = code.isIdentifier(index - 1, "from") || code.isIdentifier(index - 1, "import") || code.isPunctuation(index - 1, "(") && (code.isIdentifier(index - 2, "import") || code.isIdentifier(index - 2, "require")) ? stringAt(code, index) : void 0;
|
|
1106
|
+
return specifier === void 0 ? [] : [specifier];
|
|
1107
|
+
});
|
|
1108
|
+
const indexAfterReExportedNames = (code, exportIndex) => {
|
|
1109
|
+
const namesIndex = exportIndex + 1;
|
|
1110
|
+
if (code.isPunctuation(namesIndex, "{")) {
|
|
1111
|
+
const closer = code.closerOf(namesIndex);
|
|
1112
|
+
return closer === void 0 ? void 0 : closer + 1;
|
|
1113
|
+
}
|
|
1114
|
+
if (code.isPunctuation(namesIndex, "*")) return code.isIdentifier(namesIndex + 1, "as") ? namesIndex + 3 : namesIndex + 1;
|
|
1115
|
+
};
|
|
1116
|
+
const javascriptReExportSpecifiers = (code) => code.tokens.flatMap((_token, index) => {
|
|
1117
|
+
const fromIndex = code.isIdentifier(index, "export") ? indexAfterReExportedNames(code, index) : void 0;
|
|
1118
|
+
const specifier = fromIndex !== void 0 && code.isIdentifier(fromIndex, "from") ? stringAt(code, fromIndex + 1) : void 0;
|
|
1119
|
+
return specifier === void 0 ? [] : [specifier];
|
|
1120
|
+
});
|
|
1121
|
+
const javascriptCandidates = (path) => {
|
|
1122
|
+
const extension = extname(path);
|
|
1123
|
+
const withoutExtension = path.slice(0, path.length - extension.length);
|
|
1124
|
+
return [
|
|
1125
|
+
...JAVASCRIPT_SOURCE_EXTENSIONS.includes(extension) ? [path] : [],
|
|
1126
|
+
...(TYPESCRIPT_EXTENSIONS_FOR[extension] ?? []).map((typescriptExtension) => withoutExtension + typescriptExtension),
|
|
1127
|
+
...JAVASCRIPT_SOURCE_EXTENSIONS.map((sourceExtension) => path + sourceExtension),
|
|
1128
|
+
...JAVASCRIPT_SOURCE_EXTENSIONS.map((sourceExtension) => join(path, `index${sourceExtension}`))
|
|
1129
|
+
];
|
|
1130
|
+
};
|
|
1131
|
+
const pythonImportedNames = (code, start) => {
|
|
1132
|
+
const names = [];
|
|
1133
|
+
let isInsideParentheses = false;
|
|
1134
|
+
for (let index = start; index < code.tokens.length; index += 1) {
|
|
1135
|
+
if (index > start && code.startsLine(index) && !isInsideParentheses) break;
|
|
1136
|
+
if (code.isPunctuation(index, "(")) isInsideParentheses = true;
|
|
1137
|
+
else if (code.isPunctuation(index, ")")) break;
|
|
1138
|
+
else if (code.isIdentifier(index, "as")) index += 1;
|
|
1139
|
+
else {
|
|
1140
|
+
const token = code.token(index);
|
|
1141
|
+
if (token?.kind === "identifier") names.push(token.text);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return names;
|
|
1145
|
+
};
|
|
1146
|
+
const pythonFromImport = (code, fromIndex) => {
|
|
1147
|
+
let index = fromIndex + 1;
|
|
1148
|
+
let level = 0;
|
|
1149
|
+
while (code.isPunctuation(index, ".")) {
|
|
1150
|
+
level += 1;
|
|
1151
|
+
index += 1;
|
|
1152
|
+
}
|
|
1153
|
+
const modulePath = [];
|
|
1154
|
+
for (let token = code.token(index); token?.kind === "identifier" && token.text !== "import"; token = code.token(index)) {
|
|
1155
|
+
modulePath.push(token.text);
|
|
1156
|
+
index += code.isPunctuation(index + 1, ".") ? 2 : 1;
|
|
1157
|
+
}
|
|
1158
|
+
return code.isIdentifier(index, "import") ? {
|
|
1159
|
+
level,
|
|
1160
|
+
modulePath,
|
|
1161
|
+
names: pythonImportedNames(code, index + 1)
|
|
1162
|
+
} : void 0;
|
|
1163
|
+
};
|
|
1164
|
+
const pythonPlainImports = (code, importIndex) => {
|
|
1165
|
+
const imports = [];
|
|
1166
|
+
let modulePath = [];
|
|
1167
|
+
const endModule = () => {
|
|
1168
|
+
if (modulePath.length > 0) imports.push({
|
|
1169
|
+
level: 0,
|
|
1170
|
+
modulePath,
|
|
1171
|
+
names: []
|
|
1172
|
+
});
|
|
1173
|
+
modulePath = [];
|
|
1174
|
+
};
|
|
1175
|
+
for (let index = importIndex + 1; index < code.tokens.length && !code.startsLine(index); index += 1) if (code.isIdentifier(index, "as")) {
|
|
1176
|
+
endModule();
|
|
1177
|
+
index += 1;
|
|
1178
|
+
} else if (code.isPunctuation(index, ",")) endModule();
|
|
1179
|
+
else {
|
|
1180
|
+
const token = code.token(index);
|
|
1181
|
+
if (token?.kind === "identifier") modulePath.push(token.text);
|
|
1182
|
+
}
|
|
1183
|
+
endModule();
|
|
1184
|
+
return imports;
|
|
1185
|
+
};
|
|
1186
|
+
const pythonImports = (code) => code.tokens.flatMap((_token, index) => {
|
|
1187
|
+
if (!code.startsLine(index)) return [];
|
|
1188
|
+
if (code.isIdentifier(index, "from")) {
|
|
1189
|
+
const fromImport = pythonFromImport(code, index);
|
|
1190
|
+
return fromImport === void 0 ? [] : [fromImport];
|
|
1191
|
+
}
|
|
1192
|
+
return code.isIdentifier(index, "import") ? pythonPlainImports(code, index) : [];
|
|
1193
|
+
});
|
|
1194
|
+
const isRelativePythonImport = (pythonImport) => pythonImport.level > 0;
|
|
1195
|
+
const pythonModuleFiles = (base, modulePath) => {
|
|
1196
|
+
if (modulePath.length === 0) return [];
|
|
1197
|
+
const path = join(base, ...modulePath);
|
|
1198
|
+
return [`${path}.py`, join(path, PYTHON_PACKAGE_FILE)];
|
|
1199
|
+
};
|
|
1200
|
+
const dartPackageDeclaredIn = async (directory) => {
|
|
1201
|
+
const nameLine = (await readFileIfPresent(join(directory, "pubspec.yaml")))?.split("\n").find((line) => line.startsWith("name:"));
|
|
1202
|
+
return nameLine === void 0 ? void 0 : {
|
|
1203
|
+
root: directory,
|
|
1204
|
+
name: unquoted(nameLine.slice(5))
|
|
1205
|
+
};
|
|
1206
|
+
};
|
|
1207
|
+
const goModuleDeclaredIn = async (directory) => {
|
|
1208
|
+
const moduleLine = (await readFileIfPresent(join(directory, "go.mod")))?.split("\n").find((line) => line.startsWith("module "));
|
|
1209
|
+
return moduleLine === void 0 ? void 0 : {
|
|
1210
|
+
root: directory,
|
|
1211
|
+
path: unquoted(moduleLine.slice(7))
|
|
1212
|
+
};
|
|
1213
|
+
};
|
|
1214
|
+
const dartSpecifiers = (code, directives) => code.tokens.flatMap((_token, index) => {
|
|
1215
|
+
const specifier = directives.some((directive) => code.isIdentifier(index - 1, directive)) ? stringAt(code, index) : void 0;
|
|
1216
|
+
return specifier === void 0 || specifier.startsWith(DART_LIBRARY_SCHEME) ? [] : [specifier];
|
|
1217
|
+
});
|
|
1218
|
+
const goImportPaths = (code) => code.tokens.flatMap((_token, index) => {
|
|
1219
|
+
if (!code.isIdentifier(index, "import")) return [];
|
|
1220
|
+
const lastIndex = (code.isPunctuation(index + 1, "(") ? code.closerOf(index + 1) : void 0) ?? index + 1;
|
|
1221
|
+
const paths = [];
|
|
1222
|
+
for (let pathIndex = index + 1; pathIndex <= lastIndex; pathIndex += 1) {
|
|
1223
|
+
const path = stringAt(code, pathIndex);
|
|
1224
|
+
if (path !== void 0) paths.push(path);
|
|
1225
|
+
}
|
|
1226
|
+
return paths;
|
|
1227
|
+
});
|
|
1228
|
+
const swiftProjectDeclaredIn = async (directory) => {
|
|
1229
|
+
const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
1230
|
+
if (entries.some((entry) => entry.isFile() && entry.name === SWIFT_PACKAGE_MANIFEST)) return {
|
|
1231
|
+
kind: "swiftPackage",
|
|
1232
|
+
root: directory
|
|
1233
|
+
};
|
|
1234
|
+
return entries.some((entry) => entry.isDirectory() && extname(entry.name) === XCODE_PROJECT_EXTENSION) ? {
|
|
1235
|
+
kind: "xcodeProject",
|
|
1236
|
+
root: directory
|
|
1237
|
+
} : void 0;
|
|
1238
|
+
};
|
|
1239
|
+
const swiftPackageTargetOf = (packageRoot, directory) => {
|
|
1240
|
+
const [targetsDirectory, targetName] = relative(packageRoot, directory).split(sep);
|
|
1241
|
+
if (targetsDirectory === void 0 || !SWIFT_PACKAGE_TARGETS_DIRECTORIES.includes(targetsDirectory)) return packageRoot;
|
|
1242
|
+
return targetName === void 0 ? join(packageRoot, targetsDirectory) : join(packageRoot, targetsDirectory, targetName);
|
|
1243
|
+
};
|
|
1244
|
+
const goPackageNameIn = (code) => {
|
|
1245
|
+
const name = code.token(1);
|
|
1246
|
+
return code.isIdentifier(0, "package") && name?.kind === "identifier" ? name.text : void 0;
|
|
1247
|
+
};
|
|
1248
|
+
const goPackageNameOfFile = async (file) => {
|
|
1249
|
+
const contents = await readFileWithinSizeLimit(file);
|
|
1250
|
+
return contents === void 0 ? void 0 : goPackageNameIn(new SourceCode(contents, "go"));
|
|
1251
|
+
};
|
|
1252
|
+
var LocalImports = class {
|
|
1253
|
+
dartPackages;
|
|
1254
|
+
goModules;
|
|
1255
|
+
swiftProjects;
|
|
1256
|
+
pathAliases;
|
|
1257
|
+
goFilesOfDirectory = /* @__PURE__ */ new Map();
|
|
1258
|
+
fileExistence = /* @__PURE__ */ new Map();
|
|
1259
|
+
constructor(highestDirectory) {
|
|
1260
|
+
this.highestDirectory = highestDirectory;
|
|
1261
|
+
this.dartPackages = new NearestDeclarations(highestDirectory, dartPackageDeclaredIn);
|
|
1262
|
+
this.goModules = new NearestDeclarations(highestDirectory, goModuleDeclaredIn);
|
|
1263
|
+
this.swiftProjects = new NearestDeclarations(highestDirectory, swiftProjectDeclaredIn);
|
|
1264
|
+
this.pathAliases = new PathAliases(highestDirectory);
|
|
1265
|
+
}
|
|
1266
|
+
filesImportedBy(code, file) {
|
|
1267
|
+
switch (code.language) {
|
|
1268
|
+
case "javascript": return this.javascriptFilesAt(javascriptSpecifiers(code), file);
|
|
1269
|
+
case "python": return this.pythonFilesAt(pythonImports(code), file);
|
|
1270
|
+
case "dart": return this.dartFilesAt(dartSpecifiers(code, DART_IMPORT_DIRECTIVES), file);
|
|
1271
|
+
case "go": return this.goFilesImportedBy(code, file);
|
|
1272
|
+
case "swift":
|
|
1273
|
+
case "kotlin":
|
|
1274
|
+
case "java":
|
|
1275
|
+
case "csharp": return Promise.resolve([]);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
filesOfSamePackageOrTarget(code, file) {
|
|
1279
|
+
switch (code.language) {
|
|
1280
|
+
case "go": return this.goFilesOfSamePackage(code, file);
|
|
1281
|
+
case "swift": return this.swiftFilesOfSameTarget(file);
|
|
1282
|
+
case "javascript":
|
|
1283
|
+
case "python":
|
|
1284
|
+
case "dart":
|
|
1285
|
+
case "kotlin":
|
|
1286
|
+
case "java":
|
|
1287
|
+
case "csharp": return Promise.resolve([]);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
filesReExportedBy(code, file) {
|
|
1291
|
+
switch (code.language) {
|
|
1292
|
+
case "javascript": return this.javascriptFilesAt(javascriptReExportSpecifiers(code), file);
|
|
1293
|
+
case "python": return basename(file) === PYTHON_PACKAGE_FILE ? this.pythonFilesAt(pythonImports(code).filter(isRelativePythonImport), file) : Promise.resolve([]);
|
|
1294
|
+
case "dart": return this.dartFilesAt(dartSpecifiers(code, DART_RE_EXPORT_DIRECTIVES), file);
|
|
1295
|
+
case "go":
|
|
1296
|
+
case "swift":
|
|
1297
|
+
case "kotlin":
|
|
1298
|
+
case "java":
|
|
1299
|
+
case "csharp": return Promise.resolve([]);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
fileExistsAt(path) {
|
|
1303
|
+
const existenceCheckedBefore = this.fileExistence.get(path);
|
|
1304
|
+
if (existenceCheckedBefore !== void 0) return existenceCheckedBefore;
|
|
1305
|
+
const existence = fileExists(path);
|
|
1306
|
+
this.fileExistence.set(path, existence);
|
|
1307
|
+
return existence;
|
|
1308
|
+
}
|
|
1309
|
+
async firstExistingFile(candidates) {
|
|
1310
|
+
for (const candidate of candidates) if (await this.fileExistsAt(candidate)) return candidate;
|
|
1311
|
+
}
|
|
1312
|
+
async existingFiles(candidates) {
|
|
1313
|
+
const existing = [];
|
|
1314
|
+
for (const candidate of candidates) if (await this.fileExistsAt(candidate)) existing.push(candidate);
|
|
1315
|
+
return existing;
|
|
1316
|
+
}
|
|
1317
|
+
async javascriptFilesAt(specifiers, file) {
|
|
1318
|
+
const files = [];
|
|
1319
|
+
for (const specifier of specifiers) {
|
|
1320
|
+
const modulePaths = isRelativeSpecifier(specifier) ? [join(dirname(file), specifier)] : await this.pathAliases.modulePathsFor(specifier, file);
|
|
1321
|
+
const importedFile = await this.firstExistingFile(modulePaths.flatMap(javascriptCandidates));
|
|
1322
|
+
if (importedFile !== void 0) files.push(importedFile);
|
|
1323
|
+
}
|
|
1324
|
+
return files;
|
|
1325
|
+
}
|
|
1326
|
+
async pythonFilesAt(imports, file) {
|
|
1327
|
+
const files = [];
|
|
1328
|
+
for (const { level, modulePath, names } of imports) {
|
|
1329
|
+
const bases = level === 0 ? directoriesUpFrom(dirname(file), this.highestDirectory) : [ancestorDirectory(dirname(file), level - 1)];
|
|
1330
|
+
for (const base of bases) {
|
|
1331
|
+
const moduleFiles = await this.existingFiles([...pythonModuleFiles(base, modulePath), ...names.flatMap((name) => pythonModuleFiles(base, [...modulePath, name]))]);
|
|
1332
|
+
if (moduleFiles.length > 0) {
|
|
1333
|
+
files.push(...moduleFiles);
|
|
1334
|
+
break;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return files;
|
|
1339
|
+
}
|
|
1340
|
+
async dartFilesAt(specifiers, file) {
|
|
1341
|
+
const ownPackage = await this.dartPackages.declarationAbove(file);
|
|
1342
|
+
const candidates = specifiers.flatMap((specifier) => {
|
|
1343
|
+
if (!specifier.startsWith(DART_PACKAGE_SCHEME)) return [join(dirname(file), specifier)];
|
|
1344
|
+
const [packageName, ...pathInPackage] = specifier.slice(8).split("/");
|
|
1345
|
+
return ownPackage !== void 0 && packageName === ownPackage.name ? [join(ownPackage.root, "lib", ...pathInPackage)] : [];
|
|
1346
|
+
});
|
|
1347
|
+
return this.existingFiles(candidates);
|
|
1348
|
+
}
|
|
1349
|
+
async goFilesImportedBy(code, file) {
|
|
1350
|
+
const module = await this.goModules.declarationAbove(file);
|
|
1351
|
+
if (module === void 0) return [];
|
|
1352
|
+
const packageDirectories = goImportPaths(code).filter((path) => path === module.path || path.startsWith(`${module.path}/`)).map((path) => join(module.root, path.slice(module.path.length)));
|
|
1353
|
+
return (await Promise.all(packageDirectories.map((directory) => this.goFilesIn(directory)))).flat();
|
|
1354
|
+
}
|
|
1355
|
+
async goFilesOfSamePackage(code, file) {
|
|
1356
|
+
const packageName = goPackageNameIn(code);
|
|
1357
|
+
if (packageName === void 0) return [];
|
|
1358
|
+
const files = await this.goFilesIn(dirname(file));
|
|
1359
|
+
const packageNames = await Promise.all(files.map(goPackageNameOfFile));
|
|
1360
|
+
return files.filter((_file, index) => packageNames[index] === packageName);
|
|
1361
|
+
}
|
|
1362
|
+
async swiftFilesOfSameTarget(file) {
|
|
1363
|
+
const target = await this.swiftTargetOfDirectory(dirname(file));
|
|
1364
|
+
return this.swiftFilesOfTargetIn(target, target);
|
|
1365
|
+
}
|
|
1366
|
+
async swiftFilesOfTargetIn(target, directory) {
|
|
1367
|
+
const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
1368
|
+
const files = entries.filter((entry) => entry.isFile() && extname(entry.name) === ".swift").map((entry) => join(directory, entry.name));
|
|
1369
|
+
const filesOfSubdirectories = await Promise.all(entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
|
|
1370
|
+
const subdirectory = join(directory, entry.name);
|
|
1371
|
+
return await this.swiftTargetOfDirectory(subdirectory) === target ? this.swiftFilesOfTargetIn(target, subdirectory) : [];
|
|
1372
|
+
}));
|
|
1373
|
+
return [...files, ...filesOfSubdirectories.flat()];
|
|
1374
|
+
}
|
|
1375
|
+
async swiftTargetOfDirectory(directory) {
|
|
1376
|
+
const project = await this.swiftProjects.declarationAtOrAbove(directory);
|
|
1377
|
+
if (project === void 0) return directory;
|
|
1378
|
+
switch (project.kind) {
|
|
1379
|
+
case "swiftPackage": return swiftPackageTargetOf(project.root, directory);
|
|
1380
|
+
case "xcodeProject": return project.root;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
goFilesIn(directory) {
|
|
1384
|
+
const filesListedBefore = this.goFilesOfDirectory.get(directory);
|
|
1385
|
+
if (filesListedBefore !== void 0) return filesListedBefore;
|
|
1386
|
+
const files = readdir(directory, { withFileTypes: true }).then((entries) => entries.filter((entry) => entry.isFile() && extname(entry.name) === ".go").map((entry) => join(directory, entry.name)), () => []);
|
|
1387
|
+
this.goFilesOfDirectory.set(directory, files);
|
|
1388
|
+
return files;
|
|
1389
|
+
}
|
|
1390
|
+
};
|
|
1391
|
+
//#endregion
|
|
1392
|
+
//#region src/scan/project-areas.ts
|
|
1393
|
+
const GRADLE_BUILD_FILES = ["build.gradle.kts", "build.gradle"];
|
|
1394
|
+
const MAVEN_BUILD_FILE = "pom.xml";
|
|
1395
|
+
const ANDROID_MODULE_MARKERS = [
|
|
1396
|
+
"com.android.",
|
|
1397
|
+
"android {",
|
|
1398
|
+
"android{"
|
|
1399
|
+
];
|
|
1400
|
+
const NUXT_CONFIG_FILES = [
|
|
1401
|
+
"nuxt.config.ts",
|
|
1402
|
+
"nuxt.config.js",
|
|
1403
|
+
"nuxt.config.mjs",
|
|
1404
|
+
"nuxt.config.mts"
|
|
1405
|
+
];
|
|
1406
|
+
const NUXT_SERVER_DIRECTORY = "server";
|
|
1407
|
+
const buildModuleDeclaredIn = async (directory) => {
|
|
1408
|
+
for (const buildFile of GRADLE_BUILD_FILES) {
|
|
1409
|
+
const contents = await readFileIfPresent(join(directory, buildFile));
|
|
1410
|
+
if (contents !== void 0) return ANDROID_MODULE_MARKERS.some((marker) => contents.includes(marker)) ? "android" : "jvm";
|
|
1411
|
+
}
|
|
1412
|
+
return await readFileIfPresent(join(directory, MAVEN_BUILD_FILE)) === void 0 ? void 0 : "jvm";
|
|
1413
|
+
};
|
|
1414
|
+
const nuxtAppRootDeclaredIn = async (directory) => {
|
|
1415
|
+
for (const configFile of NUXT_CONFIG_FILES) if (await fileExists(join(directory, configFile))) return directory;
|
|
1416
|
+
};
|
|
1417
|
+
var ProjectAreas = class {
|
|
1418
|
+
buildModules;
|
|
1419
|
+
nuxtAppRoots;
|
|
1420
|
+
constructor(highestDirectory) {
|
|
1421
|
+
this.buildModules = new NearestDeclarations(highestDirectory, buildModuleDeclaredIn);
|
|
1422
|
+
this.nuxtAppRoots = new NearestDeclarations(highestDirectory, nuxtAppRootDeclaredIn);
|
|
1423
|
+
}
|
|
1424
|
+
async contain(area, file) {
|
|
1425
|
+
switch (area) {
|
|
1426
|
+
case "android-module": return await this.buildModuleOf(file) === "android";
|
|
1427
|
+
case "jvm-module": return await this.buildModuleOf(file) === "jvm";
|
|
1428
|
+
case "nuxt-app": return !await this.isInNuxtServerDirectory(file);
|
|
1429
|
+
case "nuxt-server": return this.isInNuxtServerDirectory(file);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
async buildModuleOf(file) {
|
|
1433
|
+
return await this.buildModules.declarationAbove(file) ?? "jvm";
|
|
1434
|
+
}
|
|
1435
|
+
async isInNuxtServerDirectory(file) {
|
|
1436
|
+
const appRoot = await this.nuxtAppRoots.declarationAbove(file);
|
|
1437
|
+
return appRoot !== void 0 && relative(appRoot, file).split(sep)[0] === NUXT_SERVER_DIRECTORY;
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
//#endregion
|
|
1441
|
+
//#region src/scan/sdk-families.ts
|
|
1442
|
+
const sdkTypes = ["client", "server"];
|
|
1443
|
+
const javascriptExtensions = [
|
|
1444
|
+
".js",
|
|
1445
|
+
".jsx",
|
|
1446
|
+
".mjs",
|
|
1447
|
+
".cjs",
|
|
1448
|
+
".ts",
|
|
1449
|
+
".tsx",
|
|
1450
|
+
".mts",
|
|
1451
|
+
".cts"
|
|
1452
|
+
];
|
|
1453
|
+
const browserExtensions = [
|
|
1454
|
+
...javascriptExtensions,
|
|
1455
|
+
".vue",
|
|
1456
|
+
".svelte"
|
|
1457
|
+
];
|
|
1458
|
+
const vueExtensions = [...javascriptExtensions, ".vue"];
|
|
1459
|
+
const read = (name, options = {}) => ({
|
|
1460
|
+
name,
|
|
1461
|
+
form: "method",
|
|
1462
|
+
argumentCount: options.argumentCount,
|
|
1463
|
+
keyArgument: {
|
|
1464
|
+
kind: "key",
|
|
1465
|
+
slot: options.key ?? { position: 0 }
|
|
1466
|
+
},
|
|
1467
|
+
inCodeDefault: options.inCodeDefault ?? { position: 1 },
|
|
1468
|
+
getterType: options.getterType,
|
|
1469
|
+
test: false
|
|
1470
|
+
});
|
|
1471
|
+
const readFunction = (name, options = {}) => ({
|
|
1472
|
+
...read(name, options),
|
|
1473
|
+
form: "function"
|
|
1474
|
+
});
|
|
1475
|
+
const typedReads = (namesByGetterType, options = {}) => Object.entries(namesByGetterType).flatMap(([getterType, names]) => names.map((name) => read(name, {
|
|
1476
|
+
...options,
|
|
1477
|
+
getterType
|
|
1478
|
+
})));
|
|
1479
|
+
const keyOnly = (name, key = { position: 0 }) => ({
|
|
1480
|
+
name,
|
|
1481
|
+
form: "method",
|
|
1482
|
+
keyArgument: {
|
|
1483
|
+
kind: "key",
|
|
1484
|
+
slot: key
|
|
1485
|
+
},
|
|
1486
|
+
test: false
|
|
1487
|
+
});
|
|
1488
|
+
const testMethod = (name, key = { position: 0 }) => ({
|
|
1489
|
+
...keyOnly(name, key),
|
|
1490
|
+
test: true
|
|
1491
|
+
});
|
|
1492
|
+
const testMap = (name, form, slot = { position: 0 }, property) => ({
|
|
1493
|
+
name,
|
|
1494
|
+
form,
|
|
1495
|
+
keyArgument: {
|
|
1496
|
+
kind: "mapKeys",
|
|
1497
|
+
slot,
|
|
1498
|
+
property
|
|
1499
|
+
},
|
|
1500
|
+
test: true
|
|
1501
|
+
});
|
|
1502
|
+
const testAnnotation = (name) => ({
|
|
1503
|
+
name,
|
|
1504
|
+
form: "annotation",
|
|
1505
|
+
keyArgument: {
|
|
1506
|
+
kind: "key",
|
|
1507
|
+
slot: { label: "key" }
|
|
1508
|
+
},
|
|
1509
|
+
test: true
|
|
1510
|
+
});
|
|
1511
|
+
const javascriptClientCalls = [
|
|
1512
|
+
read("getValue"),
|
|
1513
|
+
read("watch"),
|
|
1514
|
+
keyOnly("unwatch")
|
|
1515
|
+
];
|
|
1516
|
+
const javascriptTestClientCalls = [
|
|
1517
|
+
testMap("createTestClient", "function", { position: 0 }, "values"),
|
|
1518
|
+
testMethod("setValue"),
|
|
1519
|
+
testMethod("removeValue"),
|
|
1520
|
+
testMap("replaceValues", "method")
|
|
1521
|
+
];
|
|
1522
|
+
const openFeatureJavascriptGetters = typedReads({
|
|
1523
|
+
boolean: ["getBooleanValue", "getBooleanDetails"],
|
|
1524
|
+
string: ["getStringValue", "getStringDetails"],
|
|
1525
|
+
number: ["getNumberValue", "getNumberDetails"],
|
|
1526
|
+
json: ["getObjectValue", "getObjectDetails"]
|
|
1527
|
+
});
|
|
1528
|
+
const pythonLabels = {
|
|
1529
|
+
key: {
|
|
1530
|
+
position: 0,
|
|
1531
|
+
label: "config_key"
|
|
1532
|
+
},
|
|
1533
|
+
inCodeDefault: {
|
|
1534
|
+
position: 1,
|
|
1535
|
+
label: "default"
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
const openFeaturePythonLabels = {
|
|
1539
|
+
key: {
|
|
1540
|
+
position: 0,
|
|
1541
|
+
label: "flag_key"
|
|
1542
|
+
},
|
|
1543
|
+
inCodeDefault: {
|
|
1544
|
+
position: 1,
|
|
1545
|
+
label: "default_value"
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
const csharpLabels = {
|
|
1549
|
+
key: {
|
|
1550
|
+
position: 0,
|
|
1551
|
+
label: "configKey"
|
|
1552
|
+
},
|
|
1553
|
+
inCodeDefault: {
|
|
1554
|
+
position: 1,
|
|
1555
|
+
label: "defaultValue"
|
|
1556
|
+
}
|
|
1557
|
+
};
|
|
1558
|
+
const openFeatureCsharpLabels = {
|
|
1559
|
+
key: {
|
|
1560
|
+
position: 0,
|
|
1561
|
+
label: "flagKey"
|
|
1562
|
+
},
|
|
1563
|
+
inCodeDefault: {
|
|
1564
|
+
position: 1,
|
|
1565
|
+
label: "defaultValue"
|
|
1566
|
+
}
|
|
1567
|
+
};
|
|
1568
|
+
const openFeatureGoArguments = {
|
|
1569
|
+
key: { position: 1 },
|
|
1570
|
+
inCodeDefault: { position: 2 },
|
|
1571
|
+
argumentCount: { minimum: 4 }
|
|
1572
|
+
};
|
|
1573
|
+
const kotlinLabels = {
|
|
1574
|
+
key: {
|
|
1575
|
+
position: 0,
|
|
1576
|
+
label: "key"
|
|
1577
|
+
},
|
|
1578
|
+
inCodeDefault: {
|
|
1579
|
+
position: 1,
|
|
1580
|
+
label: "defaultValue"
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
const swiftLabels = {
|
|
1584
|
+
key: { label: "for" },
|
|
1585
|
+
inCodeDefault: { label: "default" }
|
|
1586
|
+
};
|
|
1587
|
+
const openFeatureSwiftLabels = {
|
|
1588
|
+
key: { label: "key" },
|
|
1589
|
+
inCodeDefault: { label: "defaultValue" }
|
|
1590
|
+
};
|
|
1591
|
+
const sdkFamilies = {
|
|
1592
|
+
"js-client": {
|
|
1593
|
+
sdkType: "client",
|
|
1594
|
+
extensions: browserExtensions,
|
|
1595
|
+
importMarkers: ["@configdirector/client-sdk"],
|
|
1596
|
+
calls: [...javascriptClientCalls, ...javascriptTestClientCalls]
|
|
1597
|
+
},
|
|
1598
|
+
"js-server": {
|
|
1599
|
+
sdkType: "server",
|
|
1600
|
+
extensions: javascriptExtensions,
|
|
1601
|
+
importMarkers: ["@configdirector/server-sdk"],
|
|
1602
|
+
calls: [...javascriptClientCalls, ...javascriptTestClientCalls]
|
|
1603
|
+
},
|
|
1604
|
+
react: {
|
|
1605
|
+
sdkType: "client",
|
|
1606
|
+
extensions: javascriptExtensions,
|
|
1607
|
+
importMarkers: ["@configdirector/react-web-sdk"],
|
|
1608
|
+
calls: [
|
|
1609
|
+
readFunction("useConfigValue"),
|
|
1610
|
+
...javascriptClientCalls,
|
|
1611
|
+
...javascriptTestClientCalls
|
|
1612
|
+
]
|
|
1613
|
+
},
|
|
1614
|
+
"react-native": {
|
|
1615
|
+
sdkType: "client",
|
|
1616
|
+
extensions: javascriptExtensions,
|
|
1617
|
+
importMarkers: ["@configdirector/react-native-sdk"],
|
|
1618
|
+
calls: [
|
|
1619
|
+
readFunction("useConfigValue"),
|
|
1620
|
+
...javascriptClientCalls,
|
|
1621
|
+
...javascriptTestClientCalls
|
|
1622
|
+
]
|
|
1623
|
+
},
|
|
1624
|
+
vue: {
|
|
1625
|
+
sdkType: "client",
|
|
1626
|
+
extensions: vueExtensions,
|
|
1627
|
+
importMarkers: ["@configdirector/vue-sdk"],
|
|
1628
|
+
calls: [
|
|
1629
|
+
readFunction("useConfigValue"),
|
|
1630
|
+
...javascriptClientCalls,
|
|
1631
|
+
...javascriptTestClientCalls
|
|
1632
|
+
]
|
|
1633
|
+
},
|
|
1634
|
+
"nuxt-client": {
|
|
1635
|
+
sdkType: "client",
|
|
1636
|
+
extensions: vueExtensions,
|
|
1637
|
+
importMarkers: [
|
|
1638
|
+
"useConfigDirectorValue",
|
|
1639
|
+
"useConfigDirectorClient",
|
|
1640
|
+
"@configdirector/nuxt-sdk"
|
|
1641
|
+
],
|
|
1642
|
+
projectArea: "nuxt-app",
|
|
1643
|
+
calls: [
|
|
1644
|
+
readFunction("useConfigDirectorValue"),
|
|
1645
|
+
...javascriptClientCalls,
|
|
1646
|
+
...javascriptTestClientCalls
|
|
1647
|
+
]
|
|
1648
|
+
},
|
|
1649
|
+
"nuxt-server": {
|
|
1650
|
+
sdkType: "server",
|
|
1651
|
+
extensions: javascriptExtensions,
|
|
1652
|
+
importMarkers: ["useConfigDirectorClient"],
|
|
1653
|
+
projectArea: "nuxt-server",
|
|
1654
|
+
calls: javascriptClientCalls
|
|
1655
|
+
},
|
|
1656
|
+
"nextjs-client": {
|
|
1657
|
+
sdkType: "client",
|
|
1658
|
+
extensions: javascriptExtensions,
|
|
1659
|
+
importMarkers: ["@configdirector/nextjs-sdk/client"],
|
|
1660
|
+
calls: [
|
|
1661
|
+
readFunction("useConfigValue"),
|
|
1662
|
+
...javascriptClientCalls,
|
|
1663
|
+
...javascriptTestClientCalls
|
|
1664
|
+
]
|
|
1665
|
+
},
|
|
1666
|
+
"nextjs-server": {
|
|
1667
|
+
sdkType: "server",
|
|
1668
|
+
extensions: javascriptExtensions,
|
|
1669
|
+
importMarkers: ["@configdirector/nextjs-sdk/server"],
|
|
1670
|
+
calls: [...javascriptClientCalls, ...javascriptTestClientCalls]
|
|
1671
|
+
},
|
|
1672
|
+
"openfeature-web": {
|
|
1673
|
+
sdkType: "client",
|
|
1674
|
+
extensions: browserExtensions,
|
|
1675
|
+
importMarkers: ["@openfeature/web-sdk"],
|
|
1676
|
+
calls: openFeatureJavascriptGetters
|
|
1677
|
+
},
|
|
1678
|
+
"openfeature-node": {
|
|
1679
|
+
sdkType: "server",
|
|
1680
|
+
extensions: javascriptExtensions,
|
|
1681
|
+
importMarkers: ["@openfeature/server-sdk"],
|
|
1682
|
+
calls: openFeatureJavascriptGetters
|
|
1683
|
+
},
|
|
1684
|
+
flutter: {
|
|
1685
|
+
sdkType: "client",
|
|
1686
|
+
extensions: [".dart"],
|
|
1687
|
+
importMarkers: ["package:configdirector_flutter_client_sdk/"],
|
|
1688
|
+
calls: [
|
|
1689
|
+
read("getValue"),
|
|
1690
|
+
read("evaluate"),
|
|
1691
|
+
read("watch"),
|
|
1692
|
+
keyOnly("unwatch"),
|
|
1693
|
+
testMap("createTestClient", "function", { label: "values" }),
|
|
1694
|
+
testMethod("setValue"),
|
|
1695
|
+
testMethod("removeValue"),
|
|
1696
|
+
testMap("replaceValues", "method")
|
|
1697
|
+
]
|
|
1698
|
+
},
|
|
1699
|
+
"openfeature-flutter": {
|
|
1700
|
+
sdkType: "client",
|
|
1701
|
+
extensions: [".dart"],
|
|
1702
|
+
importMarkers: ["package:openfeature_dart_client_sdk/"],
|
|
1703
|
+
calls: typedReads({
|
|
1704
|
+
boolean: ["getBooleanValue", "getBooleanDetails"],
|
|
1705
|
+
string: ["getStringValue", "getStringDetails"],
|
|
1706
|
+
integer: ["getIntegerValue", "getIntegerDetails"],
|
|
1707
|
+
float: ["getDoubleValue", "getDoubleDetails"],
|
|
1708
|
+
json: ["getStructureValue", "getStructureDetails"]
|
|
1709
|
+
})
|
|
1710
|
+
},
|
|
1711
|
+
swift: {
|
|
1712
|
+
sdkType: "client",
|
|
1713
|
+
extensions: [".swift"],
|
|
1714
|
+
importMarkers: ["import ConfigDirector"],
|
|
1715
|
+
providerPackagesContainingMarkers: ["ConfigDirectorOpenFeatureProvider"],
|
|
1716
|
+
calls: [
|
|
1717
|
+
read("value", swiftLabels),
|
|
1718
|
+
read("values", swiftLabels),
|
|
1719
|
+
testMap("makeTestClient", "function", { label: "values" }),
|
|
1720
|
+
testMethod("setValue", { label: "for" }),
|
|
1721
|
+
testMethod("removeValue", { label: "for" }),
|
|
1722
|
+
testMap("replaceValues", "method")
|
|
1723
|
+
]
|
|
1724
|
+
},
|
|
1725
|
+
"openfeature-swift": {
|
|
1726
|
+
sdkType: "client",
|
|
1727
|
+
extensions: [".swift"],
|
|
1728
|
+
importMarkers: ["import OpenFeature"],
|
|
1729
|
+
calls: [
|
|
1730
|
+
...typedReads({
|
|
1731
|
+
boolean: ["getBooleanValue", "getBooleanDetails"],
|
|
1732
|
+
string: ["getStringValue", "getStringDetails"],
|
|
1733
|
+
integer: ["getIntegerValue", "getIntegerDetails"],
|
|
1734
|
+
float: ["getDoubleValue", "getDoubleDetails"],
|
|
1735
|
+
json: ["getObjectValue", "getObjectDetails"]
|
|
1736
|
+
}, openFeatureSwiftLabels),
|
|
1737
|
+
read("getValue", openFeatureSwiftLabels),
|
|
1738
|
+
read("getDetails", openFeatureSwiftLabels)
|
|
1739
|
+
]
|
|
1740
|
+
},
|
|
1741
|
+
android: {
|
|
1742
|
+
sdkType: "client",
|
|
1743
|
+
extensions: [".kt", ".java"],
|
|
1744
|
+
importMarkers: ["com.configdirector"],
|
|
1745
|
+
providerPackagesContainingMarkers: ["com.configdirector.openfeature"],
|
|
1746
|
+
projectArea: "android-module",
|
|
1747
|
+
calls: [
|
|
1748
|
+
read("value", kotlinLabels),
|
|
1749
|
+
read("values", kotlinLabels),
|
|
1750
|
+
read("evaluate", kotlinLabels),
|
|
1751
|
+
...typedReads({
|
|
1752
|
+
boolean: [
|
|
1753
|
+
"getBoolean",
|
|
1754
|
+
"evaluateBoolean",
|
|
1755
|
+
"watchBoolean"
|
|
1756
|
+
],
|
|
1757
|
+
string: [
|
|
1758
|
+
"getString",
|
|
1759
|
+
"evaluateString",
|
|
1760
|
+
"watchString"
|
|
1761
|
+
],
|
|
1762
|
+
integer: [
|
|
1763
|
+
"getInt",
|
|
1764
|
+
"evaluateInt",
|
|
1765
|
+
"watchInt"
|
|
1766
|
+
],
|
|
1767
|
+
float: [
|
|
1768
|
+
"getDouble",
|
|
1769
|
+
"evaluateDouble",
|
|
1770
|
+
"watchDouble"
|
|
1771
|
+
],
|
|
1772
|
+
json: [
|
|
1773
|
+
"getJsonObject",
|
|
1774
|
+
"getJsonArray",
|
|
1775
|
+
"evaluateJsonObject",
|
|
1776
|
+
"evaluateJsonArray",
|
|
1777
|
+
"watchJsonObject",
|
|
1778
|
+
"watchJsonArray"
|
|
1779
|
+
]
|
|
1780
|
+
}, kotlinLabels),
|
|
1781
|
+
readFunction("configValue", kotlinLabels),
|
|
1782
|
+
testMap("createTestClient", "function", {
|
|
1783
|
+
position: 0,
|
|
1784
|
+
label: "values"
|
|
1785
|
+
}),
|
|
1786
|
+
testMethod("setValue", {
|
|
1787
|
+
position: 0,
|
|
1788
|
+
label: "key"
|
|
1789
|
+
}),
|
|
1790
|
+
testMethod("removeValue", {
|
|
1791
|
+
position: 0,
|
|
1792
|
+
label: "key"
|
|
1793
|
+
}),
|
|
1794
|
+
testMap("replaceValues", "method")
|
|
1795
|
+
]
|
|
1796
|
+
},
|
|
1797
|
+
"openfeature-android": {
|
|
1798
|
+
sdkType: "client",
|
|
1799
|
+
extensions: [".kt", ".java"],
|
|
1800
|
+
importMarkers: ["dev.openfeature.kotlin.sdk"],
|
|
1801
|
+
calls: typedReads({
|
|
1802
|
+
boolean: ["getBooleanValue", "getBooleanDetails"],
|
|
1803
|
+
string: ["getStringValue", "getStringDetails"],
|
|
1804
|
+
integer: ["getIntegerValue", "getIntegerDetails"],
|
|
1805
|
+
float: ["getDoubleValue", "getDoubleDetails"],
|
|
1806
|
+
json: ["getObjectValue", "getObjectDetails"]
|
|
1807
|
+
}, kotlinLabels)
|
|
1808
|
+
},
|
|
1809
|
+
java: {
|
|
1810
|
+
sdkType: "server",
|
|
1811
|
+
extensions: [".java", ".kt"],
|
|
1812
|
+
importMarkers: ["com.configdirector"],
|
|
1813
|
+
providerPackagesContainingMarkers: ["com.configdirector.openfeature"],
|
|
1814
|
+
projectArea: "jvm-module",
|
|
1815
|
+
calls: [
|
|
1816
|
+
...typedReads({
|
|
1817
|
+
boolean: ["getBoolean", "watchBoolean"],
|
|
1818
|
+
string: ["getString", "watchString"],
|
|
1819
|
+
integer: ["getInteger", "watchInteger"],
|
|
1820
|
+
float: ["getDouble", "watchDouble"],
|
|
1821
|
+
json: [
|
|
1822
|
+
"getJsonObject",
|
|
1823
|
+
"getJsonArray",
|
|
1824
|
+
"watchJsonObject",
|
|
1825
|
+
"watchJsonArray"
|
|
1826
|
+
]
|
|
1827
|
+
}),
|
|
1828
|
+
read("getValue"),
|
|
1829
|
+
read("watch"),
|
|
1830
|
+
keyOnly("unwatch"),
|
|
1831
|
+
testMap("createTestClient", "function"),
|
|
1832
|
+
testMethod("setValue"),
|
|
1833
|
+
testMethod("setJsonValue"),
|
|
1834
|
+
testMethod("removeValue"),
|
|
1835
|
+
testMap("replaceValues", "method"),
|
|
1836
|
+
testAnnotation("BooleanConfigValue"),
|
|
1837
|
+
testAnnotation("StringConfigValue"),
|
|
1838
|
+
testAnnotation("IntegerConfigValue"),
|
|
1839
|
+
testAnnotation("FloatConfigValue"),
|
|
1840
|
+
testAnnotation("JsonConfigValue")
|
|
1841
|
+
]
|
|
1842
|
+
},
|
|
1843
|
+
"openfeature-java": {
|
|
1844
|
+
sdkType: "server",
|
|
1845
|
+
extensions: [".java", ".kt"],
|
|
1846
|
+
importMarkers: ["dev.openfeature.sdk"],
|
|
1847
|
+
calls: typedReads({
|
|
1848
|
+
boolean: ["getBooleanValue", "getBooleanDetails"],
|
|
1849
|
+
string: ["getStringValue", "getStringDetails"],
|
|
1850
|
+
integer: [
|
|
1851
|
+
"getIntegerValue",
|
|
1852
|
+
"getIntegerDetails",
|
|
1853
|
+
"getLongValue",
|
|
1854
|
+
"getLongDetails"
|
|
1855
|
+
],
|
|
1856
|
+
float: ["getDoubleValue", "getDoubleDetails"],
|
|
1857
|
+
json: ["getObjectValue", "getObjectDetails"]
|
|
1858
|
+
})
|
|
1859
|
+
},
|
|
1860
|
+
python: {
|
|
1861
|
+
sdkType: "server",
|
|
1862
|
+
extensions: [".py"],
|
|
1863
|
+
importMarkers: ["import configdirector", "from configdirector"],
|
|
1864
|
+
providerPackagesContainingMarkers: ["configdirector_openfeature"],
|
|
1865
|
+
calls: [
|
|
1866
|
+
read("get_value", pythonLabels),
|
|
1867
|
+
read("watch", pythonLabels),
|
|
1868
|
+
keyOnly("unwatch", pythonLabels.key),
|
|
1869
|
+
testMap("create_test_client", "function", {
|
|
1870
|
+
position: 0,
|
|
1871
|
+
label: "values"
|
|
1872
|
+
}),
|
|
1873
|
+
testMethod("set_value", {
|
|
1874
|
+
position: 0,
|
|
1875
|
+
label: "key"
|
|
1876
|
+
}),
|
|
1877
|
+
testMethod("remove_value", {
|
|
1878
|
+
position: 0,
|
|
1879
|
+
label: "key"
|
|
1880
|
+
}),
|
|
1881
|
+
testMap("replace_values", "method", {
|
|
1882
|
+
position: 0,
|
|
1883
|
+
label: "values"
|
|
1884
|
+
})
|
|
1885
|
+
]
|
|
1886
|
+
},
|
|
1887
|
+
"openfeature-python": {
|
|
1888
|
+
sdkType: "server",
|
|
1889
|
+
extensions: [".py"],
|
|
1890
|
+
importMarkers: ["import openfeature", "from openfeature"],
|
|
1891
|
+
calls: typedReads({
|
|
1892
|
+
boolean: [
|
|
1893
|
+
"get_boolean_value",
|
|
1894
|
+
"get_boolean_details",
|
|
1895
|
+
"get_boolean_value_async",
|
|
1896
|
+
"get_boolean_details_async"
|
|
1897
|
+
],
|
|
1898
|
+
string: [
|
|
1899
|
+
"get_string_value",
|
|
1900
|
+
"get_string_details",
|
|
1901
|
+
"get_string_value_async",
|
|
1902
|
+
"get_string_details_async"
|
|
1903
|
+
],
|
|
1904
|
+
integer: [
|
|
1905
|
+
"get_integer_value",
|
|
1906
|
+
"get_integer_details",
|
|
1907
|
+
"get_integer_value_async",
|
|
1908
|
+
"get_integer_details_async"
|
|
1909
|
+
],
|
|
1910
|
+
float: [
|
|
1911
|
+
"get_float_value",
|
|
1912
|
+
"get_float_details",
|
|
1913
|
+
"get_float_value_async",
|
|
1914
|
+
"get_float_details_async"
|
|
1915
|
+
],
|
|
1916
|
+
json: [
|
|
1917
|
+
"get_object_value",
|
|
1918
|
+
"get_object_details",
|
|
1919
|
+
"get_object_value_async",
|
|
1920
|
+
"get_object_details_async"
|
|
1921
|
+
]
|
|
1922
|
+
}, openFeaturePythonLabels)
|
|
1923
|
+
},
|
|
1924
|
+
dotnet: {
|
|
1925
|
+
sdkType: "server",
|
|
1926
|
+
extensions: [".cs"],
|
|
1927
|
+
importMarkers: [
|
|
1928
|
+
"using ConfigDirector",
|
|
1929
|
+
"ConfigDirectorClient",
|
|
1930
|
+
"ConfigDirectorTesting"
|
|
1931
|
+
],
|
|
1932
|
+
providerPackagesContainingMarkers: ["ConfigDirector.OpenFeature"],
|
|
1933
|
+
calls: [
|
|
1934
|
+
read("GetValue", csharpLabels),
|
|
1935
|
+
read("Watch", csharpLabels),
|
|
1936
|
+
...typedReads({ json: ["GetJsonValue", "WatchJson"] }, csharpLabels),
|
|
1937
|
+
keyOnly("Unwatch", csharpLabels.key),
|
|
1938
|
+
testMap("CreateTestClient", "function", {
|
|
1939
|
+
position: 0,
|
|
1940
|
+
label: "values"
|
|
1941
|
+
}),
|
|
1942
|
+
testMethod("SetValue", {
|
|
1943
|
+
position: 0,
|
|
1944
|
+
label: "key"
|
|
1945
|
+
}),
|
|
1946
|
+
testMethod("RemoveValue", {
|
|
1947
|
+
position: 0,
|
|
1948
|
+
label: "key"
|
|
1949
|
+
}),
|
|
1950
|
+
testMap("ReplaceValues", "method", {
|
|
1951
|
+
position: 0,
|
|
1952
|
+
label: "values"
|
|
1953
|
+
})
|
|
1954
|
+
]
|
|
1955
|
+
},
|
|
1956
|
+
"openfeature-dotnet": {
|
|
1957
|
+
sdkType: "server",
|
|
1958
|
+
extensions: [".cs"],
|
|
1959
|
+
importMarkers: ["using OpenFeature", "FeatureClient"],
|
|
1960
|
+
calls: typedReads({
|
|
1961
|
+
boolean: ["GetBooleanValueAsync", "GetBooleanDetailsAsync"],
|
|
1962
|
+
string: ["GetStringValueAsync", "GetStringDetailsAsync"],
|
|
1963
|
+
integer: ["GetIntegerValueAsync", "GetIntegerDetailsAsync"],
|
|
1964
|
+
float: ["GetDoubleValueAsync", "GetDoubleDetailsAsync"],
|
|
1965
|
+
json: ["GetObjectValueAsync", "GetObjectDetailsAsync"]
|
|
1966
|
+
}, openFeatureCsharpLabels)
|
|
1967
|
+
},
|
|
1968
|
+
go: {
|
|
1969
|
+
sdkType: "server",
|
|
1970
|
+
extensions: [".go"],
|
|
1971
|
+
importMarkers: ["github.com/ConfigDirector/go-sdks/configdirector"],
|
|
1972
|
+
providerPackagesContainingMarkers: ["github.com/ConfigDirector/go-sdks/configdirector-openfeature"],
|
|
1973
|
+
calls: [
|
|
1974
|
+
...typedReads({
|
|
1975
|
+
boolean: ["BoolValue", "BoolValueDetails"],
|
|
1976
|
+
string: ["StringValue", "StringValueDetails"],
|
|
1977
|
+
integer: ["IntValue", "IntValueDetails"],
|
|
1978
|
+
float: ["FloatValue", "FloatValueDetails"],
|
|
1979
|
+
json: ["JSONValue", "JSONValueDetails"]
|
|
1980
|
+
}, { argumentCount: {
|
|
1981
|
+
minimum: 3,
|
|
1982
|
+
maximum: 3
|
|
1983
|
+
} }),
|
|
1984
|
+
...typedReads({
|
|
1985
|
+
boolean: ["WatchBool"],
|
|
1986
|
+
string: ["WatchString"],
|
|
1987
|
+
integer: ["WatchInt"],
|
|
1988
|
+
float: ["WatchFloat"],
|
|
1989
|
+
json: ["WatchJSON"]
|
|
1990
|
+
}, { argumentCount: {
|
|
1991
|
+
minimum: 4,
|
|
1992
|
+
maximum: 4
|
|
1993
|
+
} }),
|
|
1994
|
+
testMap("NewTestClient", "function"),
|
|
1995
|
+
testMethod("SetValue"),
|
|
1996
|
+
testMethod("RemoveValue"),
|
|
1997
|
+
testMap("ReplaceValues", "method")
|
|
1998
|
+
]
|
|
1999
|
+
},
|
|
2000
|
+
"openfeature-go": {
|
|
2001
|
+
sdkType: "server",
|
|
2002
|
+
extensions: [".go"],
|
|
2003
|
+
importMarkers: ["github.com/open-feature/go-sdk/openfeature"],
|
|
2004
|
+
calls: typedReads({
|
|
2005
|
+
boolean: [
|
|
2006
|
+
"Boolean",
|
|
2007
|
+
"BooleanValue",
|
|
2008
|
+
"BooleanValueDetails"
|
|
2009
|
+
],
|
|
2010
|
+
string: [
|
|
2011
|
+
"String",
|
|
2012
|
+
"StringValue",
|
|
2013
|
+
"StringValueDetails"
|
|
2014
|
+
],
|
|
2015
|
+
integer: [
|
|
2016
|
+
"Int",
|
|
2017
|
+
"IntValue",
|
|
2018
|
+
"IntValueDetails"
|
|
2019
|
+
],
|
|
2020
|
+
float: [
|
|
2021
|
+
"Float",
|
|
2022
|
+
"FloatValue",
|
|
2023
|
+
"FloatValueDetails"
|
|
2024
|
+
],
|
|
2025
|
+
json: [
|
|
2026
|
+
"Object",
|
|
2027
|
+
"ObjectValue",
|
|
2028
|
+
"ObjectValueDetails"
|
|
2029
|
+
]
|
|
2030
|
+
}, openFeatureGoArguments)
|
|
2031
|
+
}
|
|
2032
|
+
};
|
|
2033
|
+
const allSdkFamilies = Object.keys(sdkFamilies);
|
|
2034
|
+
//#endregion
|
|
2035
|
+
//#region src/scan/map-literals.ts
|
|
2036
|
+
const KOTLIN_MAP_BUILDERS = new Set([
|
|
2037
|
+
"mapOf",
|
|
2038
|
+
"mutableMapOf",
|
|
2039
|
+
"hashMapOf",
|
|
2040
|
+
"linkedMapOf"
|
|
2041
|
+
]);
|
|
2042
|
+
const itemsOfGroup = (code, opener) => opener === void 0 ? void 0 : code.commaSeparatedItems(opener);
|
|
2043
|
+
const splitAt = (item, separator) => {
|
|
2044
|
+
if (separator === void 0 || separator === item.first || separator === item.last) return {
|
|
2045
|
+
key: item,
|
|
2046
|
+
keyIsName: false,
|
|
2047
|
+
value: void 0
|
|
2048
|
+
};
|
|
2049
|
+
return {
|
|
2050
|
+
key: {
|
|
2051
|
+
first: item.first,
|
|
2052
|
+
last: separator - 1
|
|
2053
|
+
},
|
|
2054
|
+
keyIsName: false,
|
|
2055
|
+
value: {
|
|
2056
|
+
first: separator + 1,
|
|
2057
|
+
last: item.last
|
|
2058
|
+
}
|
|
2059
|
+
};
|
|
2060
|
+
};
|
|
2061
|
+
const entriesSeparatedByColon = (code, items) => items.map((item) => splitAt(item, code.firstTopLevelIndex(item, (index) => code.isPunctuation(index, ":"))));
|
|
2062
|
+
const entriesInClosingBraces = (code, range) => {
|
|
2063
|
+
const items = itemsOfGroup(code, code.openerOfLastToken(range, "}"));
|
|
2064
|
+
return items === void 0 ? void 0 : entriesSeparatedByColon(code, items);
|
|
2065
|
+
};
|
|
2066
|
+
const javascriptEntry = (code, item) => {
|
|
2067
|
+
const first = code.token(item.first);
|
|
2068
|
+
if (item.first === item.last && first?.kind === "identifier") return {
|
|
2069
|
+
key: item,
|
|
2070
|
+
keyIsName: true,
|
|
2071
|
+
value: item
|
|
2072
|
+
};
|
|
2073
|
+
const entry = splitAt(item, code.firstTopLevelIndex(item, (index) => code.isPunctuation(index, ":")));
|
|
2074
|
+
return {
|
|
2075
|
+
...entry,
|
|
2076
|
+
keyIsName: entry.key.first === entry.key.last && first?.kind === "identifier"
|
|
2077
|
+
};
|
|
2078
|
+
};
|
|
2079
|
+
const kotlinMapBuilderOpener = (code, range) => {
|
|
2080
|
+
const opener = code.openerOfLastToken(range, ")");
|
|
2081
|
+
const builder = code.token(range.first);
|
|
2082
|
+
return opener !== void 0 && builder?.kind === "identifier" && KOTLIN_MAP_BUILDERS.has(builder.text) ? opener : void 0;
|
|
2083
|
+
};
|
|
2084
|
+
const javaMapOfOpener = (code, range) => code.isIdentifier(range.first, "Map") && code.isPunctuation(range.first + 1, ".") && code.isIdentifier(range.first + 2, "of") && code.openerOfLastToken(range, ")") === range.first + 3 ? range.first + 3 : void 0;
|
|
2085
|
+
const alternatingEntries = (items) => items.flatMap((item, position) => position % 2 === 0 ? [{
|
|
2086
|
+
key: item,
|
|
2087
|
+
keyIsName: false,
|
|
2088
|
+
value: items[position + 1]
|
|
2089
|
+
}] : []);
|
|
2090
|
+
const csharpEntry = (code, item) => {
|
|
2091
|
+
const indexerCloser = code.isPunctuation(item.first, "[") ? code.closerOf(item.first) : void 0;
|
|
2092
|
+
if (indexerCloser !== void 0 && code.isPunctuation(indexerCloser + 1, "=")) return {
|
|
2093
|
+
key: {
|
|
2094
|
+
first: item.first + 1,
|
|
2095
|
+
last: indexerCloser - 1
|
|
2096
|
+
},
|
|
2097
|
+
keyIsName: false,
|
|
2098
|
+
value: {
|
|
2099
|
+
first: indexerCloser + 2,
|
|
2100
|
+
last: item.last
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
const [key, value] = (code.isPunctuation(item.first, "{") ? itemsOfGroup(code, item.first) : void 0) ?? [];
|
|
2104
|
+
return key === void 0 ? {
|
|
2105
|
+
key: item,
|
|
2106
|
+
keyIsName: false,
|
|
2107
|
+
value: void 0
|
|
2108
|
+
} : {
|
|
2109
|
+
key,
|
|
2110
|
+
keyIsName: false,
|
|
2111
|
+
value
|
|
2112
|
+
};
|
|
2113
|
+
};
|
|
2114
|
+
const mapLiteralEntries = {
|
|
2115
|
+
javascript: (code, range) => code.openerOfLastToken(range, "}") === range.first ? itemsOfGroup(code, range.first)?.map((item) => javascriptEntry(code, item)) : void 0,
|
|
2116
|
+
python: (code, range) => code.openerOfLastToken(range, "}") === range.first ? entriesSeparatedByColon(code, itemsOfGroup(code, range.first) ?? []) : void 0,
|
|
2117
|
+
dart: entriesInClosingBraces,
|
|
2118
|
+
swift: (code, range) => code.openerOfLastToken(range, "]") === range.first ? entriesSeparatedByColon(code, (itemsOfGroup(code, range.first) ?? []).filter((item) => !code.isPunctuation(item.first, ":"))) : void 0,
|
|
2119
|
+
go: entriesInClosingBraces,
|
|
2120
|
+
kotlin: (code, range) => itemsOfGroup(code, kotlinMapBuilderOpener(code, range))?.map((item) => splitAt(item, code.firstTopLevelIndex(item, (index) => code.isIdentifier(index, "to")))),
|
|
2121
|
+
java: (code, range) => {
|
|
2122
|
+
const items = itemsOfGroup(code, javaMapOfOpener(code, range));
|
|
2123
|
+
return items === void 0 ? void 0 : alternatingEntries(items);
|
|
2124
|
+
},
|
|
2125
|
+
csharp: (code, range) => itemsOfGroup(code, code.openerOfLastToken(range, "}"))?.map((item) => csharpEntry(code, item))
|
|
2126
|
+
};
|
|
2127
|
+
//#endregion
|
|
2128
|
+
//#region src/scan/sdk-calls.ts
|
|
2129
|
+
const LABEL_SEPARATORS = new Set([":", "="]);
|
|
2130
|
+
const CHARACTERS_EXTENDING_A_SEPARATOR = new Set([
|
|
2131
|
+
":",
|
|
2132
|
+
"=",
|
|
2133
|
+
">"
|
|
2134
|
+
]);
|
|
2135
|
+
const labelledArgument = (code, item) => {
|
|
2136
|
+
const labelToken = code.token(item.first);
|
|
2137
|
+
const separator = code.token(item.first + 1);
|
|
2138
|
+
return labelToken?.kind === "identifier" && separator?.kind === "punctuation" && LABEL_SEPARATORS.has(separator.text) && !CHARACTERS_EXTENDING_A_SEPARATOR.has(code.characterAfter(item.first + 1)) && item.first + 2 <= item.last ? {
|
|
2139
|
+
label: labelToken.text,
|
|
2140
|
+
range: {
|
|
2141
|
+
first: item.first + 2,
|
|
2142
|
+
last: item.last
|
|
2143
|
+
}
|
|
2144
|
+
} : {
|
|
2145
|
+
label: void 0,
|
|
2146
|
+
range: item
|
|
2147
|
+
};
|
|
2148
|
+
};
|
|
2149
|
+
const argumentInSlot = (callArguments, slot) => {
|
|
2150
|
+
const labelled = callArguments.find((argument) => slot.label !== void 0 && argument.label === slot.label);
|
|
2151
|
+
if (labelled !== void 0) return labelled;
|
|
2152
|
+
const positional = slot.position === void 0 ? void 0 : callArguments[slot.position];
|
|
2153
|
+
return positional?.label === void 0 ? positional : void 0;
|
|
2154
|
+
};
|
|
2155
|
+
const precedingTokenMatchesForm = (code, nameIndex, shape) => {
|
|
2156
|
+
switch (shape.form) {
|
|
2157
|
+
case "method": return code.isPunctuation(nameIndex - 1, ".");
|
|
2158
|
+
case "annotation": return code.isPunctuation(nameIndex - 1, "@");
|
|
2159
|
+
case "function": return true;
|
|
2160
|
+
}
|
|
2161
|
+
};
|
|
2162
|
+
const dynamicReferenceOf = ({ line, sdk, call, argument }) => ({
|
|
2163
|
+
line,
|
|
2164
|
+
sdk,
|
|
2165
|
+
call,
|
|
2166
|
+
argument
|
|
2167
|
+
});
|
|
2168
|
+
const RAW_VALUE_ACCESSOR = "rawValue";
|
|
2169
|
+
const rangeInsideBrackets = (code, range) => code.isPunctuation(range.first, "[") && code.closerOf(range.first) === range.last ? {
|
|
2170
|
+
first: range.first + 1,
|
|
2171
|
+
last: range.last - 1
|
|
2172
|
+
} : range;
|
|
2173
|
+
const dottedPathSegments = (code, range) => {
|
|
2174
|
+
const path = rangeInsideBrackets(code, range);
|
|
2175
|
+
const tokenCount = path.last - path.first + 1;
|
|
2176
|
+
if (tokenCount < 1 || tokenCount % 2 === 0) return;
|
|
2177
|
+
const segments = [];
|
|
2178
|
+
for (let index = path.first; index <= path.last; index += 2) {
|
|
2179
|
+
const segment = code.token(index);
|
|
2180
|
+
if (segment?.kind !== "identifier" || index < path.last && !code.isPunctuation(index + 1, ".")) return;
|
|
2181
|
+
segments.push(segment.text);
|
|
2182
|
+
}
|
|
2183
|
+
return segments;
|
|
2184
|
+
};
|
|
2185
|
+
const nameReadForKey = (code, range) => {
|
|
2186
|
+
const segments = dottedPathSegments(code, range);
|
|
2187
|
+
if (segments === void 0) return;
|
|
2188
|
+
const accessesRawValue = segments.length > 1 && segments[segments.length - 1] === RAW_VALUE_ACCESSOR;
|
|
2189
|
+
return segments[segments.length - (accessesRawValue ? 2 : 1)];
|
|
2190
|
+
};
|
|
2191
|
+
const TOKENS_SEARCHED_FOR_THE_END_OF_TYPE_ARGUMENTS = 64;
|
|
2192
|
+
const TOKENS_ENDING_A_SEARCH_FOR_TYPE_ARGUMENTS = new Set([
|
|
2193
|
+
"(",
|
|
2194
|
+
")",
|
|
2195
|
+
"{",
|
|
2196
|
+
"}",
|
|
2197
|
+
";"
|
|
2198
|
+
]);
|
|
2199
|
+
const indexAfterTypeArguments = (code, openingAngleBracket) => {
|
|
2200
|
+
let depth = 0;
|
|
2201
|
+
const lastSearched = openingAngleBracket + TOKENS_SEARCHED_FOR_THE_END_OF_TYPE_ARGUMENTS;
|
|
2202
|
+
for (let index = openingAngleBracket; index <= lastSearched; index += 1) {
|
|
2203
|
+
const token = code.token(index);
|
|
2204
|
+
if (token === void 0 || token.kind === "punctuation" && TOKENS_ENDING_A_SEARCH_FOR_TYPE_ARGUMENTS.has(token.text)) return;
|
|
2205
|
+
if (code.isPunctuation(index, "<")) depth += 1;
|
|
2206
|
+
else if (code.isPunctuation(index, ">")) {
|
|
2207
|
+
depth -= 1;
|
|
2208
|
+
if (depth === 0) return index + 1;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
};
|
|
2212
|
+
const openingParenthesisAfterName = (code, nameIndex) => {
|
|
2213
|
+
const afterName = code.opensTypeArguments(nameIndex + 1) ? indexAfterTypeArguments(code, nameIndex + 1) : nameIndex + 1;
|
|
2214
|
+
return afterName !== void 0 && code.isPunctuation(afterName, "(") ? afterName : void 0;
|
|
2215
|
+
};
|
|
2216
|
+
const mapKeyExpressions = (code, range, property) => {
|
|
2217
|
+
const entries = mapLiteralEntries[code.language](code, range);
|
|
2218
|
+
if (entries === void 0) return [{
|
|
2219
|
+
range,
|
|
2220
|
+
isName: false
|
|
2221
|
+
}];
|
|
2222
|
+
if (property === void 0) return entries.map((entry) => ({
|
|
2223
|
+
range: entry.key,
|
|
2224
|
+
isName: entry.keyIsName
|
|
2225
|
+
}));
|
|
2226
|
+
const propertyEntry = entries.find((entry) => literalTextOf(code, {
|
|
2227
|
+
range: entry.key,
|
|
2228
|
+
isName: entry.keyIsName
|
|
2229
|
+
}) === property);
|
|
2230
|
+
return propertyEntry?.value === void 0 ? [] : mapKeyExpressions(code, propertyEntry.value, void 0);
|
|
2231
|
+
};
|
|
2232
|
+
const keyExpressionsOf = (code, callArguments, keyArgument) => {
|
|
2233
|
+
const argument = argumentInSlot(callArguments, keyArgument.slot);
|
|
2234
|
+
if (argument === void 0) return [];
|
|
2235
|
+
switch (keyArgument.kind) {
|
|
2236
|
+
case "key": return [{
|
|
2237
|
+
range: argument.range,
|
|
2238
|
+
isName: false
|
|
2239
|
+
}];
|
|
2240
|
+
case "mapKeys": return mapKeyExpressions(code, argument.range, keyArgument.property);
|
|
2241
|
+
}
|
|
2242
|
+
};
|
|
2243
|
+
const callArgumentsOf = (code, nameIndex) => {
|
|
2244
|
+
const openingParenthesis = openingParenthesisAfterName(code, nameIndex);
|
|
2245
|
+
return (openingParenthesis === void 0 ? void 0 : code.commaSeparatedItems(openingParenthesis))?.map((item) => labelledArgument(code, item));
|
|
2246
|
+
};
|
|
2247
|
+
const hasArgumentCountOf = (shape, callArguments) => shape.argumentCount === void 0 || callArguments.length >= shape.argumentCount.minimum && callArguments.length <= (shape.argumentCount.maximum ?? Number.POSITIVE_INFINITY);
|
|
2248
|
+
const collectCallReferences = (code, nameIndex, sdk, shape, matches) => {
|
|
2249
|
+
const callArguments = precedingTokenMatchesForm(code, nameIndex, shape) ? callArgumentsOf(code, nameIndex) : void 0;
|
|
2250
|
+
if (callArguments === void 0 || !hasArgumentCountOf(shape, callArguments)) return;
|
|
2251
|
+
const inCodeDefaultArgument = shape.inCodeDefault === void 0 ? void 0 : argumentInSlot(callArguments, shape.inCodeDefault);
|
|
2252
|
+
const inCodeDefault = inCodeDefaultArgument === void 0 ? void 0 : inCodeDefaultValueOf(code, inCodeDefaultArgument.range);
|
|
2253
|
+
for (const expression of keyExpressionsOf(code, callArguments, shape.keyArgument)) {
|
|
2254
|
+
const line = code.lineOf(expression.range.first);
|
|
2255
|
+
const keyLiteral = keyLiteralOf(code, expression);
|
|
2256
|
+
const tokenIndex = expression.range.first;
|
|
2257
|
+
const call = {
|
|
2258
|
+
line,
|
|
2259
|
+
sdk,
|
|
2260
|
+
call: shape.name,
|
|
2261
|
+
inCodeDefault,
|
|
2262
|
+
getterType: shape.getterType,
|
|
2263
|
+
test: shape.test
|
|
2264
|
+
};
|
|
2265
|
+
switch (keyLiteral.kind) {
|
|
2266
|
+
case "key":
|
|
2267
|
+
matches.push({
|
|
2268
|
+
tokenIndex,
|
|
2269
|
+
kind: "direct",
|
|
2270
|
+
reference: {
|
|
2271
|
+
...call,
|
|
2272
|
+
key: keyLiteral.key,
|
|
2273
|
+
argument: void 0
|
|
2274
|
+
}
|
|
2275
|
+
});
|
|
2276
|
+
break;
|
|
2277
|
+
case "notLiteral": {
|
|
2278
|
+
const reference = {
|
|
2279
|
+
...call,
|
|
2280
|
+
argument: code.textOf(expression.range)
|
|
2281
|
+
};
|
|
2282
|
+
const name = nameReadForKey(code, expression.range);
|
|
2283
|
+
if (name === void 0) matches.push({
|
|
2284
|
+
tokenIndex,
|
|
2285
|
+
kind: "dynamic",
|
|
2286
|
+
reference: dynamicReferenceOf(reference)
|
|
2287
|
+
});
|
|
2288
|
+
else matches.push({
|
|
2289
|
+
tokenIndex,
|
|
2290
|
+
kind: "named",
|
|
2291
|
+
name,
|
|
2292
|
+
reference
|
|
2293
|
+
});
|
|
2294
|
+
break;
|
|
2295
|
+
}
|
|
2296
|
+
case "outsideKeyGrammar": break;
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
};
|
|
2300
|
+
const sdkCallMatches = (code, callShapes) => {
|
|
2301
|
+
const shapesByName = /* @__PURE__ */ new Map();
|
|
2302
|
+
for (const callShape of callShapes) {
|
|
2303
|
+
const shapesNamedAlike = shapesByName.get(callShape.shape.name);
|
|
2304
|
+
if (shapesNamedAlike === void 0) shapesByName.set(callShape.shape.name, [callShape]);
|
|
2305
|
+
else shapesNamedAlike.push(callShape);
|
|
2306
|
+
}
|
|
2307
|
+
const matches = [];
|
|
2308
|
+
code.tokens.forEach((token, index) => {
|
|
2309
|
+
if (token.kind !== "identifier") return;
|
|
2310
|
+
for (const { sdk, shape } of shapesByName.get(token.text) ?? []) collectCallReferences(code, index, sdk, shape, matches);
|
|
2311
|
+
});
|
|
2312
|
+
return matches;
|
|
2313
|
+
};
|
|
2314
|
+
//#endregion
|
|
2315
|
+
//#region src/scan/test-files.ts
|
|
2316
|
+
const TEST_DIRECTORY_NAMES = new Set([
|
|
2317
|
+
"test",
|
|
2318
|
+
"tests",
|
|
2319
|
+
"__tests__",
|
|
2320
|
+
"spec",
|
|
2321
|
+
"specs",
|
|
2322
|
+
"androidtest"
|
|
2323
|
+
]);
|
|
2324
|
+
const TEST_FILE_NAME_SUFFIXES = [
|
|
2325
|
+
".test",
|
|
2326
|
+
".spec",
|
|
2327
|
+
"_test",
|
|
2328
|
+
"_spec"
|
|
2329
|
+
];
|
|
2330
|
+
const TEST_CLASS_NAME_SUFFIXES = [
|
|
2331
|
+
"Test",
|
|
2332
|
+
"Tests",
|
|
2333
|
+
"Spec"
|
|
2334
|
+
];
|
|
2335
|
+
const TEST_FILE_NAME_PREFIX = "test_";
|
|
2336
|
+
const withoutExtension = (fileName) => {
|
|
2337
|
+
const dot = fileName.lastIndexOf(".");
|
|
2338
|
+
return dot <= 0 ? fileName : fileName.slice(0, dot);
|
|
2339
|
+
};
|
|
2340
|
+
const isTestFileName = (fileName) => {
|
|
2341
|
+
const name = withoutExtension(fileName);
|
|
2342
|
+
const lowercaseName = name.toLowerCase();
|
|
2343
|
+
return lowercaseName.startsWith(TEST_FILE_NAME_PREFIX) || TEST_FILE_NAME_SUFFIXES.some((suffix) => lowercaseName.endsWith(suffix)) || TEST_CLASS_NAME_SUFFIXES.some((suffix) => name.endsWith(suffix));
|
|
2344
|
+
};
|
|
2345
|
+
const isTestFile = (displayPath) => {
|
|
2346
|
+
const segments = displayPath.split("/");
|
|
2347
|
+
const fileName = segments[segments.length - 1] ?? "";
|
|
2348
|
+
return segments.slice(0, -1).some((directory) => TEST_DIRECTORY_NAMES.has(directory.toLowerCase())) || isTestFileName(fileName);
|
|
2349
|
+
};
|
|
2350
|
+
//#endregion
|
|
2351
|
+
//#region src/scan/code-references.ts
|
|
2352
|
+
const RE_EXPORT_STEPS_FOLLOWED = 2;
|
|
2353
|
+
const byTokenIndex = (first, second) => first.tokenIndex - second.tokenIndex;
|
|
2354
|
+
const matchesIn = (file, code, callShapes, projectKeys, constantNameOfToken) => {
|
|
2355
|
+
const callMatches = callShapes.length === 0 ? [] : sdkCallMatches(code, callShapes);
|
|
2356
|
+
const keyTokensOfCalls = new Set(callMatches.flatMap((match) => match.kind === "direct" ? [match.tokenIndex] : []));
|
|
2357
|
+
const isTest = isTestFile(file.displayPath);
|
|
2358
|
+
const textMatches = knownKeyTextsIn(code, projectKeys, keyTokensOfCalls).map((text) => ({
|
|
2359
|
+
tokenIndex: text.tokenIndex,
|
|
2360
|
+
kind: "indirect",
|
|
2361
|
+
reference: {
|
|
2362
|
+
key: text.key,
|
|
2363
|
+
line: code.lineOf(text.tokenIndex),
|
|
2364
|
+
test: isTest
|
|
2365
|
+
},
|
|
2366
|
+
constantName: constantNameOfToken.get(text.tokenIndex)
|
|
2367
|
+
}));
|
|
2368
|
+
return [...callMatches, ...textMatches].sort(byTokenIndex);
|
|
2369
|
+
};
|
|
2370
|
+
const matchOutcomes = (matches, constantIndex) => {
|
|
2371
|
+
const constantsReadByCalls = new Set(matches.flatMap((match) => match.kind === "named" && constantIndex.keyOf(match.name) !== void 0 ? [match.name] : []));
|
|
2372
|
+
return matches.map(({ file, ...match }) => {
|
|
2373
|
+
switch (match.kind) {
|
|
2374
|
+
case "direct": return {
|
|
2375
|
+
file,
|
|
2376
|
+
kind: "direct",
|
|
2377
|
+
reference: match.reference
|
|
2378
|
+
};
|
|
2379
|
+
case "dynamic": return {
|
|
2380
|
+
file,
|
|
2381
|
+
kind: "dynamic",
|
|
2382
|
+
reference: match.reference
|
|
2383
|
+
};
|
|
2384
|
+
case "named": {
|
|
2385
|
+
const key = constantIndex.keyOf(match.name);
|
|
2386
|
+
return key === void 0 ? {
|
|
2387
|
+
file,
|
|
2388
|
+
kind: "dynamic",
|
|
2389
|
+
reference: dynamicReferenceOf(match.reference)
|
|
2390
|
+
} : {
|
|
2391
|
+
file,
|
|
2392
|
+
kind: "direct",
|
|
2393
|
+
reference: {
|
|
2394
|
+
...match.reference,
|
|
2395
|
+
key
|
|
2396
|
+
}
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
case "indirect": return match.constantName !== void 0 && constantsReadByCalls.has(match.constantName) ? {
|
|
2400
|
+
file,
|
|
2401
|
+
kind: "definitionOfConstantRead"
|
|
2402
|
+
} : {
|
|
2403
|
+
file,
|
|
2404
|
+
kind: "indirect",
|
|
2405
|
+
reference: match.reference
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
});
|
|
2409
|
+
};
|
|
2410
|
+
const holdsImportMarkerOf = (contents, family) => {
|
|
2411
|
+
const { importMarkers, providerPackagesContainingMarkers = [] } = sdkFamilies[family];
|
|
2412
|
+
const contentsWithoutProviderPackages = providerPackagesContainingMarkers.reduce((text, providerPackage) => text.replaceAll(providerPackage, ""), contents);
|
|
2413
|
+
return importMarkers.some((marker) => contentsWithoutProviderPackages.includes(marker));
|
|
2414
|
+
};
|
|
2415
|
+
const familiesWithMarkerIn = (contents, families) => families.filter((family) => holdsImportMarkerOf(contents, family));
|
|
2416
|
+
const callsAnyOf = (code, families) => {
|
|
2417
|
+
const callNames = new Set(families.flatMap((family) => sdkFamilies[family].calls.map((shape) => shape.name)));
|
|
2418
|
+
return code.tokens.some((token) => token.kind === "identifier" && callNames.has(token.text));
|
|
2419
|
+
};
|
|
2420
|
+
const familiesOfExtension = (extension) => allSdkFamilies.filter((family) => sdkFamilies[family].extensions.includes(extension));
|
|
2421
|
+
const readReachedFile = async (path, localImports) => {
|
|
2422
|
+
const extension = extname(path);
|
|
2423
|
+
const contents = await readFileWithinSizeLimit(path);
|
|
2424
|
+
if (contents === void 0 || !isSourceExtension(extension)) return {
|
|
2425
|
+
kind: "unmarked",
|
|
2426
|
+
reExportedFiles: []
|
|
2427
|
+
};
|
|
2428
|
+
const families = familiesWithMarkerIn(contents, familiesOfExtension(extension));
|
|
2429
|
+
if (families.length > 0) return {
|
|
2430
|
+
kind: "marked",
|
|
2431
|
+
families
|
|
2432
|
+
};
|
|
2433
|
+
return {
|
|
2434
|
+
kind: "unmarked",
|
|
2435
|
+
reExportedFiles: await localImports.filesReExportedBy(new SourceCode(contents, languageOfExtension[extension]), path)
|
|
2436
|
+
};
|
|
2437
|
+
};
|
|
2438
|
+
const sdkFamiliesUsedIn = (highestDirectory) => {
|
|
2439
|
+
const projectAreas = new ProjectAreas(highestDirectory);
|
|
2440
|
+
const localImports = new LocalImports(highestDirectory);
|
|
2441
|
+
const reachedFileOfPath = /* @__PURE__ */ new Map();
|
|
2442
|
+
const familiesMarkedInPackageOrTargetOfFile = /* @__PURE__ */ new Map();
|
|
2443
|
+
const reachedFileAt = (path) => {
|
|
2444
|
+
const reachedFileReadBefore = reachedFileOfPath.get(path);
|
|
2445
|
+
if (reachedFileReadBefore !== void 0) return reachedFileReadBefore;
|
|
2446
|
+
const reachedFileRead = readReachedFile(path, localImports);
|
|
2447
|
+
reachedFileOfPath.set(path, reachedFileRead);
|
|
2448
|
+
return reachedFileRead;
|
|
2449
|
+
};
|
|
2450
|
+
const familiesReachedThrough = async (path, reExportStepsLeft) => {
|
|
2451
|
+
const reachedFile = await reachedFileAt(path);
|
|
2452
|
+
switch (reachedFile.kind) {
|
|
2453
|
+
case "marked": return reachedFile.families;
|
|
2454
|
+
case "unmarked": {
|
|
2455
|
+
const reExportedFiles = reExportStepsLeft === 0 ? [] : reachedFile.reExportedFiles;
|
|
2456
|
+
return (await Promise.all(reExportedFiles.map((reExportedFile) => familiesReachedThrough(reExportedFile, reExportStepsLeft - 1)))).flat();
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
};
|
|
2460
|
+
const familiesMarkedThroughImports = async (file, code) => {
|
|
2461
|
+
const importedPaths = await localImports.filesImportedBy(code, file.path);
|
|
2462
|
+
return (await Promise.all(importedPaths.map((importedPath) => familiesReachedThrough(importedPath, RE_EXPORT_STEPS_FOLLOWED)))).flat();
|
|
2463
|
+
};
|
|
2464
|
+
const familiesMarkedInSamePackageOrTarget = async (file, code) => {
|
|
2465
|
+
const familiesFoundBefore = familiesMarkedInPackageOrTargetOfFile.get(file.path);
|
|
2466
|
+
if (familiesFoundBefore !== void 0) return familiesFoundBefore;
|
|
2467
|
+
const filesOfPackageOrTarget = await localImports.filesOfSamePackageOrTarget(code, file.path);
|
|
2468
|
+
const families = Promise.all(filesOfPackageOrTarget.map((fileOfPackageOrTarget) => familiesReachedThrough(fileOfPackageOrTarget, RE_EXPORT_STEPS_FOLLOWED))).then((familiesOfFiles) => familiesOfFiles.flat());
|
|
2469
|
+
for (const fileOfPackageOrTarget of filesOfPackageOrTarget) familiesMarkedInPackageOrTargetOfFile.set(fileOfPackageOrTarget, families);
|
|
2470
|
+
return families;
|
|
2471
|
+
};
|
|
2472
|
+
const familiesMarkedByOtherFiles = async (file, code) => [...await familiesMarkedThroughImports(file, code), ...await familiesMarkedInSamePackageOrTarget(file, code)];
|
|
2473
|
+
return async (file, code) => {
|
|
2474
|
+
const familiesForExtension = familiesOfExtension(file.extension);
|
|
2475
|
+
const markedHere = familiesWithMarkerIn(file.contents, familiesForExtension);
|
|
2476
|
+
const markedByOtherFiles = callsAnyOf(code, familiesForExtension.filter((family) => !markedHere.includes(family))) ? await familiesMarkedByOtherFiles(file, code) : [];
|
|
2477
|
+
const usedFamilies = [];
|
|
2478
|
+
for (const family of familiesForExtension) {
|
|
2479
|
+
const requiredArea = sdkFamilies[family].projectArea;
|
|
2480
|
+
if ((markedHere.includes(family) || markedByOtherFiles.includes(family)) && (requiredArea === void 0 || await projectAreas.contain(requiredArea, file.path))) usedFamilies.push(family);
|
|
2481
|
+
}
|
|
2482
|
+
return usedFamilies;
|
|
2483
|
+
};
|
|
2484
|
+
};
|
|
2485
|
+
const scanCodeReferences = async (scope, projectKeys) => {
|
|
2486
|
+
const scan = {
|
|
2487
|
+
filesScanned: 0,
|
|
2488
|
+
references: [],
|
|
2489
|
+
dynamicReferences: []
|
|
2490
|
+
};
|
|
2491
|
+
const familiesUsedIn = sdkFamiliesUsedIn(scope.workingDirectory);
|
|
2492
|
+
const constantIndex = new ConstantIndex();
|
|
2493
|
+
const matches = [];
|
|
2494
|
+
for await (const file of scannedFiles(scope)) {
|
|
2495
|
+
scan.filesScanned += 1;
|
|
2496
|
+
const code = new SourceCode(file.contents, languageOfExtension[file.extension]);
|
|
2497
|
+
const callShapes = (await familiesUsedIn(file, code)).flatMap((sdk) => sdkFamilies[sdk].calls.map((shape) => ({
|
|
2498
|
+
sdk,
|
|
2499
|
+
shape
|
|
2500
|
+
})));
|
|
2501
|
+
const constantAssignments = constantAssignmentsIn(code);
|
|
2502
|
+
for (const assignment of constantAssignments) constantIndex.add(assignment);
|
|
2503
|
+
const constantNameOfToken = new Map(constantAssignments.map((assignment) => [assignment.tokenIndex, assignment.name]));
|
|
2504
|
+
matches.push(...matchesIn(file, code, callShapes, projectKeys, constantNameOfToken).map((match) => ({
|
|
2505
|
+
...match,
|
|
2506
|
+
file: file.displayPath
|
|
2507
|
+
})));
|
|
2508
|
+
}
|
|
2509
|
+
for (const outcome of matchOutcomes(matches, constantIndex)) switch (outcome.kind) {
|
|
2510
|
+
case "direct":
|
|
2511
|
+
scan.references.push({
|
|
2512
|
+
...outcome.reference,
|
|
2513
|
+
kind: "direct",
|
|
2514
|
+
file: outcome.file
|
|
2515
|
+
});
|
|
2516
|
+
break;
|
|
2517
|
+
case "indirect":
|
|
2518
|
+
scan.references.push({
|
|
2519
|
+
...outcome.reference,
|
|
2520
|
+
kind: "indirect",
|
|
2521
|
+
file: outcome.file
|
|
2522
|
+
});
|
|
2523
|
+
break;
|
|
2524
|
+
case "dynamic":
|
|
2525
|
+
scan.dynamicReferences.push({
|
|
2526
|
+
...outcome.reference,
|
|
2527
|
+
file: outcome.file
|
|
2528
|
+
});
|
|
2529
|
+
break;
|
|
2530
|
+
case "definitionOfConstantRead": break;
|
|
2531
|
+
}
|
|
2532
|
+
return scan;
|
|
2533
|
+
};
|
|
2534
|
+
//#endregion
|
|
2535
|
+
//#region src/project-scan.ts
|
|
2536
|
+
const scanProject = async (context, path) => {
|
|
2537
|
+
const { workingDirectory } = context.invocation;
|
|
2538
|
+
const settings = await readProjectSettings(workingDirectory);
|
|
2539
|
+
const projectSlug = requireLinkedProjectSlug(context, settings);
|
|
2540
|
+
const root = await scanRootAt(workingDirectory, path);
|
|
2541
|
+
const client = new AdminApiClient({
|
|
2542
|
+
baseUrl: context.baseUrl,
|
|
2543
|
+
token: await configuredApiToken(context)
|
|
2544
|
+
});
|
|
2545
|
+
const project = await findProjectBySlug(client, projectSlug);
|
|
2546
|
+
const configs = await client.listConfigs(project.id);
|
|
2547
|
+
const projectKeys = new Set(configs.flatMap((config) => [config.key, ...config.deprecatedKeys.map((deprecatedKey) => deprecatedKey.key)]));
|
|
2548
|
+
const codeReferenceScan = await scanCodeReferences({
|
|
2549
|
+
root,
|
|
2550
|
+
workingDirectory,
|
|
2551
|
+
include: settings?.include ?? "everyFile",
|
|
2552
|
+
exclude: settings?.exclude ?? []
|
|
2553
|
+
}, projectKeys);
|
|
2554
|
+
return {
|
|
2555
|
+
project,
|
|
2556
|
+
configs,
|
|
2557
|
+
ignoredKeys: new Set(settings?.ignoreKeys ?? []),
|
|
2558
|
+
scannedLocation: root.description,
|
|
2559
|
+
codeReferenceScan
|
|
2560
|
+
};
|
|
2561
|
+
};
|
|
2562
|
+
//#endregion
|
|
2563
|
+
//#region src/commands/scan.ts
|
|
2564
|
+
const referenceJson = (reference) => {
|
|
2565
|
+
switch (reference.kind) {
|
|
2566
|
+
case "direct": return {
|
|
2567
|
+
key: reference.key,
|
|
2568
|
+
file: reference.file,
|
|
2569
|
+
line: reference.line,
|
|
2570
|
+
kind: reference.kind,
|
|
2571
|
+
sdk: reference.sdk,
|
|
2572
|
+
call: reference.call,
|
|
2573
|
+
...reference.argument === void 0 ? {} : { argument: reference.argument },
|
|
2574
|
+
...reference.inCodeDefault === void 0 ? {} : { default: reference.inCodeDefault },
|
|
2575
|
+
...reference.getterType === void 0 ? {} : { getterType: reference.getterType },
|
|
2576
|
+
test: reference.test
|
|
2577
|
+
};
|
|
2578
|
+
case "indirect": return {
|
|
2579
|
+
key: reference.key,
|
|
2580
|
+
file: reference.file,
|
|
2581
|
+
line: reference.line,
|
|
2582
|
+
kind: reference.kind,
|
|
2583
|
+
test: reference.test
|
|
2584
|
+
};
|
|
2585
|
+
}
|
|
2586
|
+
};
|
|
2587
|
+
const scanJson = (projectSlug, scan) => ({
|
|
2588
|
+
project: projectSlug,
|
|
2589
|
+
filesScanned: scan.filesScanned,
|
|
2590
|
+
references: scan.references.map(referenceJson),
|
|
2591
|
+
dynamicReferences: scan.dynamicReferences.map((reference) => ({
|
|
2592
|
+
file: reference.file,
|
|
2593
|
+
line: reference.line,
|
|
2594
|
+
sdk: reference.sdk,
|
|
2595
|
+
call: reference.call,
|
|
2596
|
+
argument: reference.argument
|
|
2597
|
+
}))
|
|
2598
|
+
});
|
|
2599
|
+
const DYNAMIC_REFERENCES_HEADING = "Dynamic references (calls whose key is not a string literal)";
|
|
2600
|
+
const withCollapsedWhitespace = (text) => {
|
|
2601
|
+
let collapsed = "";
|
|
2602
|
+
let previousWasWhitespace = false;
|
|
2603
|
+
for (const character of text) {
|
|
2604
|
+
const isWhitespace = character.trim() === "";
|
|
2605
|
+
if (!isWhitespace) collapsed += character;
|
|
2606
|
+
else if (!previousWasWhitespace) collapsed += " ";
|
|
2607
|
+
previousWasWhitespace = isWhitespace;
|
|
2608
|
+
}
|
|
2609
|
+
return collapsed;
|
|
2610
|
+
};
|
|
2611
|
+
const describeReference = (reference) => {
|
|
2612
|
+
switch (reference.kind) {
|
|
2613
|
+
case "direct": return `${reference.argument === void 0 ? reference.call : `${reference.call}(${reference.argument})`} (${reference.test ? `${reference.sdk}, test` : reference.sdk})`;
|
|
2614
|
+
case "indirect": return reference.test ? "(indirect, test)" : "(indirect)";
|
|
2615
|
+
}
|
|
2616
|
+
};
|
|
2617
|
+
const nothingToScanText = (scannedLocation) => `No source files to scan in ${scannedLocation}.\n`;
|
|
2618
|
+
const scanText = (scan, scannedLocation) => {
|
|
2619
|
+
if (scan.filesScanned === 0) return nothingToScanText(scannedLocation);
|
|
2620
|
+
const referencesByKey = /* @__PURE__ */ new Map();
|
|
2621
|
+
for (const reference of scan.references) {
|
|
2622
|
+
const referencesToKey = referencesByKey.get(reference.key);
|
|
2623
|
+
if (referencesToKey === void 0) referencesByKey.set(reference.key, [reference]);
|
|
2624
|
+
else referencesToKey.push(reference);
|
|
2625
|
+
}
|
|
2626
|
+
const keySections = [...referencesByKey.keys()].sort().map((key) => [key, ...(referencesByKey.get(key) ?? []).map((reference) => ` ${reference.file}:${reference.line} ${describeReference(reference)}`)].join("\n"));
|
|
2627
|
+
const dynamicSection = scan.dynamicReferences.length === 0 ? [] : [[DYNAMIC_REFERENCES_HEADING, ...scan.dynamicReferences.map((reference) => ` ${reference.file}:${reference.line} ${reference.call}(${withCollapsedWhitespace(reference.argument)}) (${reference.sdk})`)].join("\n")];
|
|
2628
|
+
const directCount = scan.references.filter((reference) => reference.kind === "direct").length;
|
|
2629
|
+
const counts = `${directCount} direct, ${scan.references.length - directCount} indirect and ${scan.dynamicReferences.length} dynamic references in ${scan.filesScanned} scanned ${plural(scan.filesScanned, "file", "files")}.`;
|
|
2630
|
+
return `${[
|
|
2631
|
+
...keySections,
|
|
2632
|
+
...dynamicSection,
|
|
2633
|
+
counts
|
|
2634
|
+
].join("\n\n")}\n`;
|
|
2635
|
+
};
|
|
2636
|
+
const scan = async (context, [path]) => {
|
|
2637
|
+
const { project, scannedLocation, codeReferenceScan } = await scanProject(context, path);
|
|
2638
|
+
return {
|
|
2639
|
+
json: scanJson(project.slug, codeReferenceScan),
|
|
2640
|
+
text: scanText(codeReferenceScan, scannedLocation),
|
|
2641
|
+
exitCode: ExitCode.success
|
|
2642
|
+
};
|
|
2643
|
+
};
|
|
2644
|
+
//#endregion
|
|
2645
|
+
//#region src/validate/findings.ts
|
|
2646
|
+
const servedValueTypesOfConfigTypes = new Map([
|
|
2647
|
+
["boolean", "boolean"],
|
|
2648
|
+
["string", "string"],
|
|
2649
|
+
["url", "string"],
|
|
2650
|
+
["integer", "integer"],
|
|
2651
|
+
["float", "float"],
|
|
2652
|
+
["timespan", "float"],
|
|
2653
|
+
["json", "json"]
|
|
2654
|
+
]);
|
|
2655
|
+
const servedValueTypesReadableAs = {
|
|
2656
|
+
boolean: ["boolean"],
|
|
2657
|
+
string: [
|
|
2658
|
+
"string",
|
|
2659
|
+
"string-enum",
|
|
2660
|
+
"number-enum"
|
|
2661
|
+
],
|
|
2662
|
+
integer: [
|
|
2663
|
+
"integer",
|
|
2664
|
+
"float",
|
|
2665
|
+
"number-enum"
|
|
2666
|
+
],
|
|
2667
|
+
float: ["float", "number-enum"],
|
|
2668
|
+
number: [
|
|
2669
|
+
"integer",
|
|
2670
|
+
"float",
|
|
2671
|
+
"number-enum"
|
|
2672
|
+
],
|
|
2673
|
+
json: ["json"]
|
|
2674
|
+
};
|
|
2675
|
+
const servedValueTypesOfEnumValueTypes = new Map([["string", "string-enum"], ["number", "number-enum"]]);
|
|
2676
|
+
const isServedTo = {
|
|
2677
|
+
client: (config) => config.client,
|
|
2678
|
+
server: (config) => config.server
|
|
2679
|
+
};
|
|
2680
|
+
const keyMatchesOfConfigs = (configs) => new Map(configs.flatMap((config) => [[config.key, {
|
|
2681
|
+
kind: "currentKey",
|
|
2682
|
+
config
|
|
2683
|
+
}], ...config.deprecatedKeys.map((deprecatedKey) => [deprecatedKey.key, {
|
|
2684
|
+
kind: "deprecatedKey",
|
|
2685
|
+
config
|
|
2686
|
+
}])]));
|
|
2687
|
+
const typesReadBy = (reference) => [...reference.getterType === void 0 ? [] : [reference.getterType], ...reference.inCodeDefault === void 0 ? [] : [reference.inCodeDefault.type]];
|
|
2688
|
+
const servedValueTypeOf = (config) => isEnumConfig(config) ? servedValueTypesOfEnumValueTypes.get(config.typeOptions.valueType) : servedValueTypesOfConfigTypes.get(config.type);
|
|
2689
|
+
const isAllowedValue = (inCodeDefault, allowedValues) => {
|
|
2690
|
+
switch (inCodeDefault.type) {
|
|
2691
|
+
case "string": return allowedValues.includes(inCodeDefault.value);
|
|
2692
|
+
case "integer":
|
|
2693
|
+
case "float": return allowedValues.some((allowedValue) => Number(allowedValue) === inCodeDefault.value);
|
|
2694
|
+
case "boolean":
|
|
2695
|
+
case "json": return false;
|
|
2696
|
+
}
|
|
2697
|
+
};
|
|
2698
|
+
const valueFindings = (reference, config) => {
|
|
2699
|
+
const servedValueType = servedValueTypeOf(config);
|
|
2700
|
+
if (servedValueType === void 0) return [];
|
|
2701
|
+
const typeReadMismatch = typesReadBy(reference).find((typeRead) => !servedValueTypesReadableAs[typeRead].includes(servedValueType));
|
|
2702
|
+
if (typeReadMismatch !== void 0) return [{
|
|
2703
|
+
kind: "type-mismatch",
|
|
2704
|
+
reference,
|
|
2705
|
+
typeRead: typeReadMismatch,
|
|
2706
|
+
configType: config.type,
|
|
2707
|
+
servedAs: servedValueType
|
|
2708
|
+
}];
|
|
2709
|
+
const { inCodeDefault } = reference;
|
|
2710
|
+
if (!isEnumConfig(config) || inCodeDefault === void 0) return [];
|
|
2711
|
+
const allowedValues = config.typeOptions.values;
|
|
2712
|
+
return isAllowedValue(inCodeDefault, allowedValues) ? [] : [{
|
|
2713
|
+
kind: "enum-value-not-allowed",
|
|
2714
|
+
reference,
|
|
2715
|
+
inCodeDefault,
|
|
2716
|
+
allowedValues
|
|
2717
|
+
}];
|
|
2718
|
+
};
|
|
2719
|
+
const availabilityFindings = (reference, config) => {
|
|
2720
|
+
const { sdkType } = sdkFamilies[reference.sdk];
|
|
2721
|
+
if (reference.test || isServedTo[sdkType](config)) return [];
|
|
2722
|
+
return [{
|
|
2723
|
+
kind: "availability-mismatch",
|
|
2724
|
+
reference,
|
|
2725
|
+
sdkType,
|
|
2726
|
+
configAvailableTo: sdkTypes.filter((servedSdkType) => isServedTo[servedSdkType](config))
|
|
2727
|
+
}];
|
|
2728
|
+
};
|
|
2729
|
+
const configFindings = (reference, config) => [...valueFindings(reference, config), ...availabilityFindings(reference, config)];
|
|
2730
|
+
const findingsOfReference = (reference, keyMatch) => {
|
|
2731
|
+
switch (keyMatch.kind) {
|
|
2732
|
+
case "currentKey": return configFindings(reference, keyMatch.config);
|
|
2733
|
+
case "deprecatedKey": return [{
|
|
2734
|
+
kind: "deprecated-key",
|
|
2735
|
+
reference,
|
|
2736
|
+
currentKey: keyMatch.config.key
|
|
2737
|
+
}, ...configFindings(reference, keyMatch.config)];
|
|
2738
|
+
case "unknownKey": return [{
|
|
2739
|
+
kind: "unknown-key",
|
|
2740
|
+
reference
|
|
2741
|
+
}];
|
|
2742
|
+
}
|
|
2743
|
+
};
|
|
2744
|
+
const checkCodeReferences = (references, configs, ignoredKeys) => {
|
|
2745
|
+
const keyMatches = keyMatchesOfConfigs(configs);
|
|
2746
|
+
const checkedReferences = references.filter((reference) => reference.kind === "direct" && !ignoredKeys.has(reference.key));
|
|
2747
|
+
return {
|
|
2748
|
+
referencesChecked: checkedReferences.length,
|
|
2749
|
+
findings: checkedReferences.flatMap((reference) => findingsOfReference(reference, keyMatches.get(reference.key) ?? { kind: "unknownKey" }))
|
|
2750
|
+
};
|
|
2751
|
+
};
|
|
2752
|
+
//#endregion
|
|
2753
|
+
//#region src/commands/validate.ts
|
|
2754
|
+
const SERVED_VALUE_TYPE_NAMES = {
|
|
2755
|
+
boolean: "a boolean",
|
|
2756
|
+
string: "a string",
|
|
2757
|
+
integer: "an integer",
|
|
2758
|
+
float: "a float",
|
|
2759
|
+
json: "JSON",
|
|
2760
|
+
"string-enum": "strings",
|
|
2761
|
+
"number-enum": "numbers"
|
|
2762
|
+
};
|
|
2763
|
+
const CONFIG_TYPES_SERVED_AS_THEMSELVES = new Set([
|
|
2764
|
+
"boolean",
|
|
2765
|
+
"string",
|
|
2766
|
+
"integer",
|
|
2767
|
+
"float",
|
|
2768
|
+
"json"
|
|
2769
|
+
]);
|
|
2770
|
+
const describeAvailability = (configAvailableTo) => {
|
|
2771
|
+
const [onlySdkType] = configAvailableTo;
|
|
2772
|
+
return onlySdkType === void 0 ? "served to no SDK" : `${onlySdkType}-only`;
|
|
2773
|
+
};
|
|
2774
|
+
const describeInCodeDefault = (inCodeDefault) => inCodeDefault.type === "string" ? JSON.stringify(inCodeDefault.value) : String(inCodeDefault.value);
|
|
2775
|
+
const describeConfigType = (configType, servedAs) => CONFIG_TYPES_SERVED_AS_THEMSELVES.has(configType) ? configType : `${configType}, served as ${SERVED_VALUE_TYPE_NAMES[servedAs]}`;
|
|
2776
|
+
const FINDING_PRESENTATIONS = {
|
|
2777
|
+
"unknown-key": {
|
|
2778
|
+
heading: "Unknown keys",
|
|
2779
|
+
facts: () => ({}),
|
|
2780
|
+
description: (_finding, projectSlug) => `no config in ${projectSlug} has this key.`
|
|
2781
|
+
},
|
|
2782
|
+
"deprecated-key": {
|
|
2783
|
+
heading: "Deprecated keys",
|
|
2784
|
+
facts: (finding) => ({ currentKey: finding.currentKey }),
|
|
2785
|
+
description: (finding) => `the config was renamed to ${finding.currentKey}.`
|
|
2786
|
+
},
|
|
2787
|
+
"type-mismatch": {
|
|
2788
|
+
heading: "Type mismatches",
|
|
2789
|
+
facts: (finding) => ({
|
|
2790
|
+
configType: finding.configType,
|
|
2791
|
+
servedAs: finding.servedAs,
|
|
2792
|
+
...finding.reference.inCodeDefault === void 0 ? {} : { default: finding.reference.inCodeDefault },
|
|
2793
|
+
...finding.reference.getterType === void 0 ? {} : { getterType: finding.reference.getterType }
|
|
2794
|
+
}),
|
|
2795
|
+
description: (finding) => `read as ${finding.typeRead}, but the config's type is ${describeConfigType(finding.configType, finding.servedAs)}.`
|
|
2796
|
+
},
|
|
2797
|
+
"availability-mismatch": {
|
|
2798
|
+
heading: "Availability mismatches",
|
|
2799
|
+
facts: (finding) => ({
|
|
2800
|
+
sdk: finding.reference.sdk,
|
|
2801
|
+
sdkType: finding.sdkType,
|
|
2802
|
+
configAvailableTo: finding.configAvailableTo
|
|
2803
|
+
}),
|
|
2804
|
+
description: (finding) => `read from a ${finding.sdkType} SDK (${finding.reference.sdk}), but the config is ${describeAvailability(finding.configAvailableTo)}.`
|
|
2805
|
+
},
|
|
2806
|
+
"enum-value-not-allowed": {
|
|
2807
|
+
heading: "Enum values not allowed",
|
|
2808
|
+
facts: (finding) => ({
|
|
2809
|
+
default: finding.inCodeDefault,
|
|
2810
|
+
allowedValues: finding.allowedValues
|
|
2811
|
+
}),
|
|
2812
|
+
description: (finding) => `${describeInCodeDefault(finding.inCodeDefault)} is not one of the allowed values: ${finding.allowedValues.join(", ")}.`
|
|
2813
|
+
}
|
|
2814
|
+
};
|
|
2815
|
+
const presentationOf = (finding) => FINDING_PRESENTATIONS[finding.kind];
|
|
2816
|
+
const findingJson = (finding) => ({
|
|
2817
|
+
kind: finding.kind,
|
|
2818
|
+
key: finding.reference.key,
|
|
2819
|
+
file: finding.reference.file,
|
|
2820
|
+
line: finding.reference.line,
|
|
2821
|
+
...presentationOf(finding).facts(finding)
|
|
2822
|
+
});
|
|
2823
|
+
const findingLine = (finding, projectSlug) => ` ${finding.reference.file}:${finding.reference.line} ${finding.reference.key}: ` + presentationOf(finding).description(finding, projectSlug);
|
|
2824
|
+
const countsText = (scan, check) => {
|
|
2825
|
+
const findingCount = check.findings.length;
|
|
2826
|
+
const problems = findingCount === 0 ? "no problems" : `${findingCount} ${plural(findingCount, "problem", "problems")}`;
|
|
2827
|
+
const checked = `Checked ${check.referencesChecked} direct ${plural(check.referencesChecked, "reference", "references")} in ${scan.filesScanned} scanned ${plural(scan.filesScanned, "file", "files")} and found ${problems}.`;
|
|
2828
|
+
const dynamicCount = scan.dynamicReferences.length;
|
|
2829
|
+
return dynamicCount === 0 ? checked : `${checked} ${dynamicCount} dynamic ${plural(dynamicCount, "reference was", "references were")} not checked.`;
|
|
2830
|
+
};
|
|
2831
|
+
const validateText = (projectSlug, scan, scannedLocation, check) => {
|
|
2832
|
+
if (scan.filesScanned === 0) return nothingToScanText(scannedLocation);
|
|
2833
|
+
return `${[...Object.keys(FINDING_PRESENTATIONS).flatMap((kind) => {
|
|
2834
|
+
const findingsOfKind = check.findings.filter((finding) => finding.kind === kind);
|
|
2835
|
+
return findingsOfKind.length === 0 ? [] : [[FINDING_PRESENTATIONS[kind].heading, ...findingsOfKind.map((finding) => findingLine(finding, projectSlug))].join("\n")];
|
|
2836
|
+
}), countsText(scan, check)].join("\n\n")}\n`;
|
|
2837
|
+
};
|
|
2838
|
+
const validate = async (context, [path]) => {
|
|
2839
|
+
const { project, configs, ignoredKeys, scannedLocation, codeReferenceScan } = await scanProject(context, path);
|
|
2840
|
+
const check = checkCodeReferences(codeReferenceScan.references, configs, ignoredKeys);
|
|
2841
|
+
return {
|
|
2842
|
+
json: {
|
|
2843
|
+
...scanJson(project.slug, codeReferenceScan),
|
|
2844
|
+
findings: check.findings.map(findingJson)
|
|
2845
|
+
},
|
|
2846
|
+
text: validateText(project.slug, codeReferenceScan, scannedLocation, check),
|
|
2847
|
+
exitCode: check.findings.length === 0 ? ExitCode.success : ExitCode.problemsFound
|
|
2848
|
+
};
|
|
2849
|
+
};
|
|
2850
|
+
//#endregion
|
|
2851
|
+
//#region src/run.ts
|
|
2852
|
+
const DEFAULT_BASE_URL = "https://api.configdirector.com";
|
|
2853
|
+
const withoutTrailingSlashes = (url) => {
|
|
2854
|
+
let trimmedUrl = url;
|
|
2855
|
+
while (trimmedUrl.endsWith("/")) trimmedUrl = trimmedUrl.slice(0, -1);
|
|
2856
|
+
return trimmedUrl;
|
|
2857
|
+
};
|
|
2858
|
+
const runCommand = async (invocation, command, actionArguments) => {
|
|
2859
|
+
const parsedCommand = actionArguments.at(-1);
|
|
2860
|
+
const options = parsedCommand.optsWithGlobals();
|
|
2861
|
+
const output = await command({
|
|
2862
|
+
invocation,
|
|
2863
|
+
baseUrl: withoutTrailingSlashes(options.baseUrl ?? nonEmptyVariable(invocation, "CONFIGDIRECTOR_BASE_URL") ?? DEFAULT_BASE_URL),
|
|
2864
|
+
tokenOption: options.token,
|
|
2865
|
+
projectSlug: options.project ?? nonEmptyVariable(invocation, "CONFIGDIRECTOR_PROJECT")
|
|
2866
|
+
}, parsedCommand.processedArgs);
|
|
2867
|
+
invocation.stdout.write(options.json ? `${JSON.stringify(output.json, null, 2)}\n` : output.text);
|
|
2868
|
+
return output.exitCode;
|
|
2869
|
+
};
|
|
2870
|
+
const reportFailure = (invocation, error) => {
|
|
2871
|
+
if (error instanceof CommanderError) return error.exitCode === 0 ? ExitCode.success : ExitCode.couldNotRun;
|
|
2872
|
+
if (error instanceof CliError) {
|
|
2873
|
+
invocation.stderr.write(`Error: ${error.message}\n`);
|
|
2874
|
+
return ExitCode.couldNotRun;
|
|
2875
|
+
}
|
|
2876
|
+
invocation.stderr.write(`${error instanceof Error ? error.stack : String(error)}\n`);
|
|
2877
|
+
return ExitCode.couldNotRun;
|
|
2878
|
+
};
|
|
2879
|
+
const run = async (invocation) => {
|
|
2880
|
+
let exitCode = ExitCode.success;
|
|
2881
|
+
const commandAction = (command) => async (...actionArguments) => {
|
|
2882
|
+
exitCode = await runCommand(invocation, command, actionArguments);
|
|
2883
|
+
};
|
|
2884
|
+
const program = new Command("configdirector").description("Work with ConfigDirector configs from the terminal and CI.").addHelpText("afterAll", "\nBeta: commands and output may change before 1.0.").configureHelp({ showGlobalOptions: true }).version(version).option("--token <token>", "API token, used instead of CONFIGDIRECTOR_TOKEN and the token saved by login").option("--base-url <url>", `Admin API host, used instead of CONFIGDIRECTOR_BASE_URL (default ${DEFAULT_BASE_URL})`).option("--project <slug>", "project slug, used instead of CONFIGDIRECTOR_PROJECT").option("--json", "print JSON on stdout").exitOverride().configureOutput({
|
|
2885
|
+
writeOut: (text) => invocation.stdout.write(text),
|
|
2886
|
+
writeErr: (text) => invocation.stderr.write(text)
|
|
2887
|
+
});
|
|
2888
|
+
program.command("login").description("save an API token for later runs, after checking it can list projects").action(commandAction(login));
|
|
2889
|
+
program.command("init").description("write configdirector.json for a project in the current directory").action(commandAction(init));
|
|
2890
|
+
program.command("scan").argument("[path]", "directory or file to scan (default: the current directory)").description("list where the code reads config keys through a ConfigDirector SDK").action(commandAction(scan));
|
|
2891
|
+
program.command("validate").argument("[path]", "directory or file to check (default: the current directory)").description("check every config key the code reads against the project's configs, exiting 1 when one is wrong").action(commandAction(validate));
|
|
2892
|
+
try {
|
|
2893
|
+
await program.parseAsync(invocation.commandLineArguments, { from: "user" });
|
|
2894
|
+
return exitCode;
|
|
2895
|
+
} catch (error) {
|
|
2896
|
+
return reportFailure(invocation, error);
|
|
2897
|
+
}
|
|
2898
|
+
};
|
|
2899
|
+
//#endregion
|
|
2900
|
+
//#region src/main.ts
|
|
2901
|
+
process.exitCode = await run({
|
|
2902
|
+
commandLineArguments: process.argv.slice(2),
|
|
2903
|
+
environmentVariables: process.env,
|
|
2904
|
+
workingDirectory: process.cwd(),
|
|
2905
|
+
homeDirectory: homedir(),
|
|
2906
|
+
platform: process.platform,
|
|
2907
|
+
stdin: process.stdin,
|
|
2908
|
+
stdout: process.stdout,
|
|
2909
|
+
stderr: process.stderr
|
|
2910
|
+
});
|
|
2911
|
+
//#endregion
|
|
2912
|
+
export {};
|