@alint-js/core 0.1.3 → 0.1.5
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/LICENSE +21 -0
- package/README.md +11 -7
- package/dist/agent.d.mts +1 -1
- package/dist/{define-8BzWDcPs.d.mts → define-CJ2QJPZ8.d.mts} +1 -1
- package/dist/dsl.d.mts +2 -2
- package/dist/extract-BVbjWXMC.mjs +676 -0
- package/dist/index.d.mts +109 -7
- package/dist/index.mjs +867 -384
- package/dist/languages-js.d.mts +1 -1
- package/dist/languages-js.mjs +1 -1
- package/dist/structured-output.d.mts +1 -1
- package/dist/types-DM4idR4w.d.mts +179 -0
- package/dist/{types-DKylUrAn.d.mts → types-deBCDA4_.d.mts} +49 -6
- package/package.json +2 -1
- package/dist/extract-CIWssxNK.mjs +0 -358
- package/dist/types-DSvXUVZ7.d.mts +0 -86
package/dist/index.mjs
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { o as withAgentRetry, t as combineAbortSignals } from "./agent-B6GG68NX.mjs";
|
|
2
2
|
import { n as definePlugin, r as defineRule, t as defineConfig } from "./define-Da342Fcx.mjs";
|
|
3
|
-
import { a as sliceRange, i as
|
|
3
|
+
import { a as createSourceFile, c as sliceRange, d as stableHash, i as SourceChangedError, l as withLanguage, n as targetIdentity, o as createSourceRuntime, r as withStableIdentities, s as sliceLines, t as extractJsSourceTargets, u as createStableHasher } from "./extract-BVbjWXMC.mjs";
|
|
4
4
|
import { minimatch } from "minimatch";
|
|
5
|
-
import { isAbsolute, relative, resolve } from "pathe";
|
|
5
|
+
import { basename, extname, isAbsolute, relative, resolve } from "pathe";
|
|
6
6
|
import { access, mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
7
|
-
import { basename, dirname, extname, isAbsolute as isAbsolute$1, join, relative as relative$1, resolve as resolve$1, sep } from "node:path";
|
|
7
|
+
import { basename as basename$1, dirname, extname as extname$1, isAbsolute as isAbsolute$1, join, relative as relative$1, resolve as resolve$1, sep } from "node:path";
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
8
9
|
import process, { cwd } from "node:process";
|
|
10
|
+
import { array, boolean, check, custom, finite, getDotPath, literal, number, object, optional, parse, pipe, record, safeParse, string, union, unknown } from "valibot";
|
|
9
11
|
import { Buffer } from "node:buffer";
|
|
10
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
11
12
|
import { statSync } from "node:fs";
|
|
12
|
-
import { array, boolean, check, custom, finite, getDotPath, literal, number, object, optional, parse, pipe, record, safeParse, string, union, unknown } from "valibot";
|
|
13
13
|
import { errorMessageFrom } from "@moeru/std/error";
|
|
14
14
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
15
|
+
import { streamText } from "@xsai/stream-text";
|
|
15
16
|
//#region src/config/config-array.ts
|
|
16
17
|
const effectiveBasePathSymbol = Symbol("effectiveBasePath");
|
|
17
18
|
const inheritedMatcherScopesSymbol = Symbol("inheritedMatcherScopes");
|
|
@@ -242,16 +243,9 @@ const javascriptLanguage = {
|
|
|
242
243
|
".jsx",
|
|
243
244
|
".mjs"
|
|
244
245
|
],
|
|
245
|
-
extract: (file) => extractJsSourceTargets(withLanguage
|
|
246
|
+
extract: (file) => extractJsSourceTargets(withLanguage(file, "javascript")),
|
|
246
247
|
name: "javascript"
|
|
247
248
|
};
|
|
248
|
-
function withLanguage$1(file, language) {
|
|
249
|
-
if (file.language === language) return file;
|
|
250
|
-
return {
|
|
251
|
-
...file,
|
|
252
|
-
language
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
249
|
//#endregion
|
|
256
250
|
//#region src/core/languages/registry.ts
|
|
257
251
|
function createLanguageRegistry$1() {
|
|
@@ -278,11 +272,11 @@ const textLanguage = {
|
|
|
278
272
|
file,
|
|
279
273
|
identity: "file",
|
|
280
274
|
kind: "file",
|
|
281
|
-
language: "
|
|
275
|
+
language: "plaintext",
|
|
282
276
|
origin: { physicalPath: file.path },
|
|
283
277
|
text: file.text
|
|
284
278
|
}],
|
|
285
|
-
name: "
|
|
279
|
+
name: "plaintext"
|
|
286
280
|
};
|
|
287
281
|
//#endregion
|
|
288
282
|
//#region src/core/languages/ts/index.ts
|
|
@@ -296,20 +290,13 @@ const typescriptLanguage = {
|
|
|
296
290
|
extract: (file) => extractJsSourceTargets(withLanguage(file, "typescript")),
|
|
297
291
|
name: "typescript"
|
|
298
292
|
};
|
|
299
|
-
function withLanguage(file, language) {
|
|
300
|
-
if (file.language === language) return file;
|
|
301
|
-
return {
|
|
302
|
-
...file,
|
|
303
|
-
language
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
293
|
//#endregion
|
|
307
294
|
//#region src/core/languages/resolve.ts
|
|
308
295
|
function resolveLanguage(file, registry, options) {
|
|
309
296
|
return resolveLanguageForPath(file.path, registry, options);
|
|
310
297
|
}
|
|
311
298
|
function resolveLanguageForPath(filePath, registry, options) {
|
|
312
|
-
const languageName = options.language ?? options.processedLanguage ?? registry.byExtension.get(extname(filePath)) ?? "
|
|
299
|
+
const languageName = options.language ?? options.processedLanguage ?? registry.byExtension.get(extname$1(filePath)) ?? "plaintext";
|
|
313
300
|
const language = registry.languages.get(languageName);
|
|
314
301
|
if (!language) throw new Error(`Unknown language "${languageName}".`);
|
|
315
302
|
return language;
|
|
@@ -324,41 +311,298 @@ function createBuiltInLanguageRegistry() {
|
|
|
324
311
|
return registry;
|
|
325
312
|
}
|
|
326
313
|
//#endregion
|
|
327
|
-
//#region
|
|
328
|
-
|
|
314
|
+
//#region src/dsl/registry.ts
|
|
315
|
+
function buildRuleRegistry(config) {
|
|
316
|
+
const rules = /* @__PURE__ */ new Map();
|
|
317
|
+
const localIds = /* @__PURE__ */ new Map();
|
|
318
|
+
const enabledRules = [];
|
|
319
|
+
for (const [alias, plugin] of Object.entries(config.plugins)) for (const [localId, rule] of Object.entries(plugin.rules ?? {})) {
|
|
320
|
+
const id = `${alias}/${localId}`;
|
|
321
|
+
if (rules.has(id)) throw new Error(`Duplicate rule id "${id}".`);
|
|
322
|
+
rules.set(id, rule);
|
|
323
|
+
localIds.set(id, localId);
|
|
324
|
+
}
|
|
325
|
+
for (const [id, entry] of Object.entries(config.rules)) {
|
|
326
|
+
const rule = rules.get(id);
|
|
327
|
+
if (!rule) throw new Error(`Unknown rule "${id}".`);
|
|
328
|
+
const normalizedEntry = normalizeRuleConfigEntry(entry);
|
|
329
|
+
const severity = normalizedEntry.severity;
|
|
330
|
+
if (severity === "off") continue;
|
|
331
|
+
enabledRules.push({
|
|
332
|
+
id,
|
|
333
|
+
localId: localIds.get(id) ?? id,
|
|
334
|
+
options: parseRuleOptions(id, rule.options, normalizedEntry.options),
|
|
335
|
+
rule,
|
|
336
|
+
severity
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
enabledRules,
|
|
341
|
+
rules
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function formatRuleOptionIssues(index, issues) {
|
|
345
|
+
return issues.map((issue) => {
|
|
346
|
+
const path = getDotPath(issue);
|
|
347
|
+
return path === null ? `"${index}": ${issue.message}` : `"${index}.${path}": ${issue.message}`;
|
|
348
|
+
}).join("; ");
|
|
349
|
+
}
|
|
350
|
+
function isRuleConfigTuple(entry) {
|
|
351
|
+
return Array.isArray(entry);
|
|
352
|
+
}
|
|
353
|
+
function normalizeRuleConfigEntry(entry) {
|
|
354
|
+
if (isRuleConfigTuple(entry)) {
|
|
355
|
+
const [severity = "warn", ...options] = entry;
|
|
356
|
+
return {
|
|
357
|
+
options,
|
|
358
|
+
severity
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
options: [],
|
|
363
|
+
severity: entry ?? "warn"
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function parseRuleOption(id, index, schema, value) {
|
|
367
|
+
const result = safeParse(schema, value);
|
|
368
|
+
if (result.success) return result.output;
|
|
369
|
+
if (value === void 0) {
|
|
370
|
+
const emptyObjectResult = safeParse(schema, {});
|
|
371
|
+
if (emptyObjectResult.success) return emptyObjectResult.output;
|
|
372
|
+
}
|
|
373
|
+
throw new TypeError(`Invalid options for rule "${id}": ${formatRuleOptionIssues(index, result.issues)}`);
|
|
374
|
+
}
|
|
375
|
+
function parseRuleOptions(id, schema, options) {
|
|
376
|
+
if (!schema) {
|
|
377
|
+
if (options.length > 0) throw new TypeError(`Rule "${id}" does not accept options.`);
|
|
378
|
+
return [];
|
|
379
|
+
}
|
|
380
|
+
if (options.length > schema.length) throw new TypeError(`Invalid options for rule "${id}": Unexpected option at index ${schema.length}.`);
|
|
381
|
+
return schema.map((optionSchema, index) => parseRuleOption(id, index, optionSchema, options[index]));
|
|
382
|
+
}
|
|
329
383
|
//#endregion
|
|
330
|
-
//#region src/core/
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
384
|
+
//#region src/core/languages/diagnostics.ts
|
|
385
|
+
/** A rule named a language and nothing registered it. This is a builtin rule. Not a real rule. */
|
|
386
|
+
const MISSING_LANGUAGE_RULE = "alint/missing-language";
|
|
387
|
+
/** A linted file's extension belongs to no registered language. This is a builtin rule. Not a real rule either. */
|
|
388
|
+
const UNREGISTERED_LANGUAGE_RULE = "alint/unregistered-language";
|
|
389
|
+
/**
|
|
390
|
+
* One diagnostic per missing language, always an error: a rule scoped to a language nothing provides
|
|
391
|
+
* matches no file, so leaving it at a warning would let a run pass having silently checked nothing.
|
|
392
|
+
*/
|
|
393
|
+
function missingLanguageDiagnostics(missing) {
|
|
394
|
+
return [...missing.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([languageId, entry]) => {
|
|
395
|
+
const ruleIds = [...entry.ruleIds].sort();
|
|
396
|
+
const subject = ruleIds.length === 1 ? `Rule "${ruleIds[0]}" needs` : `Rules ${ruleIds.map((id) => `"${id}"`).join(", ")} need`;
|
|
397
|
+
return {
|
|
398
|
+
filePath: entry.filePath,
|
|
399
|
+
message: `${subject} the "${languageId}" language, which no plugin registered. Add a plugin that provides "${languageId}", or turn the rule off.`,
|
|
400
|
+
ruleId: MISSING_LANGUAGE_RULE,
|
|
401
|
+
severity: "error"
|
|
402
|
+
};
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
/** Records one rule that named a language the config never registered, grouped by language id. */
|
|
406
|
+
function recordMissingLanguage(into, languageId, ruleId, path) {
|
|
407
|
+
const existing = into.get(languageId);
|
|
408
|
+
if (existing === void 0) {
|
|
409
|
+
into.set(languageId, {
|
|
410
|
+
filePath: path,
|
|
411
|
+
ruleIds: /* @__PURE__ */ new Set([ruleId])
|
|
412
|
+
});
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
existing.ruleIds.add(ruleId);
|
|
416
|
+
if (path < existing.filePath) existing.filePath = path;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Records one linted file no language claimed, grouped by extension so a missing pack is one
|
|
420
|
+
* diagnostic, not one per file. `'off'` records nothing.
|
|
421
|
+
*/
|
|
422
|
+
function recordUnregistered(into, path, severity) {
|
|
423
|
+
if (severity === "off") return;
|
|
424
|
+
const key = extname(path) || basename(path);
|
|
425
|
+
const existing = into.get(key);
|
|
426
|
+
into.set(key, {
|
|
427
|
+
count: (existing?.count ?? 0) + 1,
|
|
428
|
+
filePath: existing !== void 0 && existing.filePath < path ? existing.filePath : path,
|
|
429
|
+
severity: existing?.severity === "error" ? "error" : severity
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
/** One diagnostic per extension, sorted, for the run to prepend to its results. */
|
|
433
|
+
function unregisteredLanguageDiagnostics(unregistered) {
|
|
434
|
+
return [...unregistered.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([extension, entry]) => ({
|
|
435
|
+
filePath: entry.filePath,
|
|
436
|
+
message: `No language is registered for "${extension}": ${entry.count} ${entry.count === 1 ? "file was" : "files were"} handled as plain text. Register one through a plugin's "languages", or pin \`language: 'plaintext'\` to accept plain text.`,
|
|
437
|
+
ruleId: UNREGISTERED_LANGUAGE_RULE,
|
|
438
|
+
severity: entry.severity
|
|
439
|
+
}));
|
|
440
|
+
}
|
|
441
|
+
function unregisteredLanguageSeverity(linterOptions) {
|
|
442
|
+
const value = linterOptions.reportUnregisteredLanguages;
|
|
443
|
+
return value === "error" || value === "off" ? value : "warn";
|
|
444
|
+
}
|
|
445
|
+
//#endregion
|
|
446
|
+
//#region src/core/languages/rule-languages.ts
|
|
447
|
+
/**
|
|
448
|
+
* The language a file falls back to when no registered language claims its extension. Not a real
|
|
449
|
+
* language, which is why `'any'` excludes it.
|
|
450
|
+
*/
|
|
451
|
+
const PLAIN_TEXT = "plaintext";
|
|
452
|
+
/**
|
|
453
|
+
* Whether a rule reads this target.
|
|
454
|
+
*
|
|
455
|
+
* A rule with language off still reads file targets, because reading raw text, or asking a model about a
|
|
456
|
+
* whole file, needs no language. However, it never reads the function and class targets extraction produces.
|
|
457
|
+
* `'file'` is the one kind every language yields, plain text included, so it is the dividing line.
|
|
458
|
+
*
|
|
459
|
+
* `'any'` means any real language, so a file that fell back to plain text because nothing claimed
|
|
460
|
+
* its extension is withheld: the rule asked for structure and there is none to give. The run says so
|
|
461
|
+
* once per extension through `alint/unregistered-language` rather than letting each rule report an
|
|
462
|
+
* empty file.
|
|
463
|
+
*/
|
|
464
|
+
function isTargetLanguageAccepted(languages, targetKind, targetLanguage) {
|
|
465
|
+
if (languages.kind === "off") return targetKind === "file";
|
|
466
|
+
if (languages.kind === "any") return targetLanguage !== PLAIN_TEXT;
|
|
467
|
+
return languages.ids.includes(targetLanguage);
|
|
468
|
+
}
|
|
469
|
+
function resolveRuleLanguages(languages) {
|
|
470
|
+
if (languages === void 0) return { kind: "off" };
|
|
471
|
+
if (languages === "any") return { kind: "any" };
|
|
472
|
+
if ("ids" in languages) return {
|
|
473
|
+
ids: languages.ids,
|
|
474
|
+
kind: "list",
|
|
475
|
+
skipMissing: languages.skipMissing ?? false
|
|
476
|
+
};
|
|
477
|
+
return {
|
|
478
|
+
ids: languages,
|
|
479
|
+
kind: "list",
|
|
480
|
+
skipMissing: false
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
//#endregion
|
|
484
|
+
//#region src/core/preparation.ts
|
|
485
|
+
/**
|
|
486
|
+
* The `ctx.src.extract` a run hands to rules, for parsing files it was never asked to lint, which
|
|
487
|
+
* an index builder sweeping the workspace needs.
|
|
488
|
+
*
|
|
489
|
+
* It resolves each file's OWN config rather than a caller's, because the language a file resolves to
|
|
490
|
+
* is a config decision and two config groups may register different plugins. It extracts on demand
|
|
491
|
+
* and holds nothing: source planning releases rich extractor values after compact jobs are admitted,
|
|
492
|
+
* and a run-wide parse memo here would put that retention back. A caller keeps only what it derives.
|
|
493
|
+
*
|
|
494
|
+
* `getSrc` defers reading the runtime because the runtime is built around this closure. It is assigned
|
|
495
|
+
* before any rule runs, so by the first call it is present.
|
|
496
|
+
*/
|
|
497
|
+
function createSourceExtractor(cwd, config, getSrc) {
|
|
498
|
+
return async (filePath, options = {}) => {
|
|
499
|
+
const path = resolve(cwd, filePath);
|
|
500
|
+
const resolvedConfig = resolveConfigForFile(path, config, { cwd });
|
|
501
|
+
if (resolvedConfig.ignored) return [];
|
|
502
|
+
const effectiveConfig = resolvedConfig.config;
|
|
503
|
+
const language = resolveLanguageForPath(path, createLanguageRegistry(effectiveConfig), { language: options.language ?? effectiveConfig.language });
|
|
504
|
+
const src = getSrc();
|
|
505
|
+
const file = await src.readFile(path);
|
|
506
|
+
return language.extract(file, {
|
|
507
|
+
cwd,
|
|
508
|
+
languageOptions: effectiveConfig.languageOptions,
|
|
509
|
+
src
|
|
510
|
+
});
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function prepareRun(options = {}) {
|
|
514
|
+
const cwd$2 = options.cwd ?? cwd();
|
|
515
|
+
const config = options.config ?? [];
|
|
516
|
+
const files = [];
|
|
517
|
+
const directories = [];
|
|
518
|
+
const missingLanguages = /* @__PURE__ */ new Map();
|
|
519
|
+
const unregisteredLanguages = /* @__PURE__ */ new Map();
|
|
520
|
+
for (const filePath of options.files ?? []) {
|
|
521
|
+
const path = resolve(cwd$2, filePath);
|
|
522
|
+
const resolvedConfig = resolveConfigForFile(path, config, { cwd: cwd$2 });
|
|
523
|
+
if (resolvedConfig.ignored) continue;
|
|
524
|
+
const effectiveConfig = resolvedConfig.config;
|
|
525
|
+
const languageRegistry = createLanguageRegistry(effectiveConfig);
|
|
526
|
+
const language = resolveLanguageForPath(path, languageRegistry, { language: effectiveConfig.language });
|
|
527
|
+
const rules = prepareRules(effectiveConfig);
|
|
528
|
+
recordMissingLanguages(missingLanguages, rules, languageRegistry, path);
|
|
529
|
+
if (effectiveConfig.language === void 0 && language.name === "plaintext" && rules.some(({ enabledRule }) => !isTargetLanguageAccepted(resolveRuleLanguages(enabledRule.rule.languages), "file", "plaintext"))) recordUnregistered(unregisteredLanguages, path, unregisteredLanguageSeverity(effectiveConfig.linterOptions));
|
|
530
|
+
files.push({
|
|
531
|
+
agent: effectiveConfig.agent,
|
|
532
|
+
configHash: stableHash({
|
|
533
|
+
language: effectiveConfig.language,
|
|
534
|
+
languageOptions: effectiveConfig.languageOptions,
|
|
535
|
+
processor: effectiveConfig.processor,
|
|
536
|
+
resolvedLanguage: language.name,
|
|
537
|
+
settings: effectiveConfig.settings
|
|
538
|
+
}),
|
|
539
|
+
fileIndex: files.length,
|
|
540
|
+
language,
|
|
541
|
+
languageOptions: effectiveConfig.languageOptions,
|
|
542
|
+
path,
|
|
543
|
+
rules,
|
|
544
|
+
settings: effectiveConfig.settings
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
for (const directoryPath of options.directories ?? []) {
|
|
548
|
+
const path = resolve(cwd$2, directoryPath);
|
|
549
|
+
const resolvedConfig = resolveConfigForDirectory(path, config, { cwd: cwd$2 });
|
|
550
|
+
if (resolvedConfig.ignored) continue;
|
|
551
|
+
const effectiveConfig = resolvedConfig.config;
|
|
552
|
+
directories.push({
|
|
553
|
+
agent: effectiveConfig.agent,
|
|
554
|
+
configHash: stableHash({ settings: effectiveConfig.settings }),
|
|
555
|
+
directoryIndex: directories.length,
|
|
556
|
+
rules: prepareRules(effectiveConfig),
|
|
557
|
+
settings: effectiveConfig.settings,
|
|
558
|
+
target: {
|
|
559
|
+
kind: "directory",
|
|
560
|
+
path
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return {
|
|
565
|
+
directories,
|
|
566
|
+
files,
|
|
567
|
+
missingLanguages,
|
|
568
|
+
project: options.projectTargets === false ? void 0 : prepareProject(cwd$2, config),
|
|
569
|
+
unregisteredLanguages
|
|
341
570
|
};
|
|
342
|
-
return api;
|
|
343
571
|
}
|
|
344
|
-
function
|
|
345
|
-
|
|
572
|
+
function createLanguageRegistry(config) {
|
|
573
|
+
const registry = createBuiltInLanguageRegistry();
|
|
574
|
+
for (const plugin of Object.values(config.plugins)) for (const language of Object.values(plugin.languages ?? {})) registerLanguage(registry, language);
|
|
575
|
+
return registry;
|
|
576
|
+
}
|
|
577
|
+
function prepareProject(root, config) {
|
|
578
|
+
const resolvedConfig = resolveConfigForProject(root, config, { cwd: root });
|
|
579
|
+
if (resolvedConfig.ignored) return void 0;
|
|
580
|
+
const effectiveConfig = resolvedConfig.config;
|
|
581
|
+
return {
|
|
582
|
+
agent: effectiveConfig.agent,
|
|
583
|
+
configHash: stableHash({ settings: effectiveConfig.settings }),
|
|
584
|
+
root,
|
|
585
|
+
rules: prepareRules(effectiveConfig),
|
|
586
|
+
settings: effectiveConfig.settings
|
|
587
|
+
};
|
|
346
588
|
}
|
|
347
|
-
function
|
|
348
|
-
return
|
|
589
|
+
function prepareRules(config) {
|
|
590
|
+
return buildRuleRegistry(config).enabledRules.map((enabledRule, ruleIndex) => ({
|
|
591
|
+
enabledRule,
|
|
592
|
+
ruleIndex
|
|
593
|
+
}));
|
|
349
594
|
}
|
|
350
|
-
function
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const property = record[key];
|
|
356
|
-
return property === void 0 ? [] : [`${JSON.stringify(key)}:${stableStringify(property)}`];
|
|
357
|
-
}).join(",")}}`;
|
|
595
|
+
function recordMissingLanguages(into, rules, registry, path) {
|
|
596
|
+
for (const { enabledRule } of rules) {
|
|
597
|
+
const languages = resolveRuleLanguages(enabledRule.rule.languages);
|
|
598
|
+
if (languages.kind !== "list" || languages.skipMissing) continue;
|
|
599
|
+
for (const languageId of languages.ids) if (!registry.languages.has(languageId)) recordMissingLanguage(into, languageId, enabledRule.id, path);
|
|
358
600
|
}
|
|
359
|
-
return JSON.stringify(value);
|
|
360
601
|
}
|
|
361
602
|
//#endregion
|
|
603
|
+
//#region package.json
|
|
604
|
+
var version = "0.1.5";
|
|
605
|
+
//#endregion
|
|
362
606
|
//#region src/core/cache/types.ts
|
|
363
607
|
const CACHE_MAGIC = "ALINT_CACHE";
|
|
364
608
|
//#endregion
|
|
@@ -704,7 +948,7 @@ function parseCacheBody(text) {
|
|
|
704
948
|
async function persistCacheBody(location, header, body, writeFile) {
|
|
705
949
|
const validatedBody = parse(CacheFileBodySchema, body);
|
|
706
950
|
await mkdir(dirname(location), { recursive: true });
|
|
707
|
-
const tempPath = join(dirname(location), `.${basename(location)}.${process.pid}.${randomUUID()}.tmp`);
|
|
951
|
+
const tempPath = join(dirname(location), `.${basename$1(location)}.${process.pid}.${randomUUID()}.tmp`);
|
|
708
952
|
try {
|
|
709
953
|
await writeFile(tempPath, `${header}\n${JSON.stringify(validatedBody)}\n`);
|
|
710
954
|
await rename(tempPath, location);
|
|
@@ -836,7 +1080,7 @@ async function executeRuleJob(job, options) {
|
|
|
836
1080
|
let handlerCause;
|
|
837
1081
|
let handlerFailed = false;
|
|
838
1082
|
try {
|
|
839
|
-
await job.execution.runtime.executionState.run(state, job
|
|
1083
|
+
await job.execution.runtime.executionState.run(state, () => runHandler(job));
|
|
840
1084
|
} catch (cause) {
|
|
841
1085
|
if (state.reporterFailed) throw state.reporterCause;
|
|
842
1086
|
handlerCause = cause;
|
|
@@ -858,7 +1102,7 @@ async function executeRuleJob(job, options) {
|
|
|
858
1102
|
});
|
|
859
1103
|
if (handlerFailed) return finish(job, bucket, {
|
|
860
1104
|
cache: "miss",
|
|
861
|
-
failure: failure(handlerCause, job, "handler"),
|
|
1105
|
+
failure: failure(handlerCause, job, handlerCause instanceof SourceChangedError ? "source-changed" : "handler"),
|
|
862
1106
|
state: "failed"
|
|
863
1107
|
});
|
|
864
1108
|
if (slot && fingerprint) try {
|
|
@@ -910,16 +1154,7 @@ function createFingerprint(job, modelHash) {
|
|
|
910
1154
|
};
|
|
911
1155
|
}
|
|
912
1156
|
function createTargetHash(job) {
|
|
913
|
-
|
|
914
|
-
return target.cacheTargetHash ?? stableHash({
|
|
915
|
-
language: target.language,
|
|
916
|
-
loc: target.loc,
|
|
917
|
-
metadata: target.metadata,
|
|
918
|
-
name: target.name,
|
|
919
|
-
origin: target.origin,
|
|
920
|
-
range: target.range,
|
|
921
|
-
text: target.text
|
|
922
|
-
});
|
|
1157
|
+
return job.target.cacheTargetHash;
|
|
923
1158
|
}
|
|
924
1159
|
function failure(cause, job, kind) {
|
|
925
1160
|
return {
|
|
@@ -979,6 +1214,44 @@ function replayCachedEntry(entry, bucket, job, progress, runProgress) {
|
|
|
979
1214
|
});
|
|
980
1215
|
}
|
|
981
1216
|
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Dispatches one compact job descriptor to its registered rule handler.
|
|
1219
|
+
*
|
|
1220
|
+
* Triggering workflow:
|
|
1221
|
+
*
|
|
1222
|
+
* {@link executeRuleJob}
|
|
1223
|
+
* -> {@link runHandler}
|
|
1224
|
+
* -> `RuleHandlers.onTarget*`
|
|
1225
|
+
*
|
|
1226
|
+
* Upstream:
|
|
1227
|
+
* - {@link executeRuleJob}
|
|
1228
|
+
*
|
|
1229
|
+
* Downstream:
|
|
1230
|
+
* - `RuleRuntime.handlers`
|
|
1231
|
+
*/
|
|
1232
|
+
function runHandler(job) {
|
|
1233
|
+
const { handlers } = job.execution.runtime;
|
|
1234
|
+
const target = job.target.descriptor;
|
|
1235
|
+
switch (job.execution.handler) {
|
|
1236
|
+
case "class":
|
|
1237
|
+
if (target.kind !== "class") throw new TypeError(`Expected class target, received "${target.kind}".`);
|
|
1238
|
+
return handlers.onTargetClass?.(target);
|
|
1239
|
+
case "directory":
|
|
1240
|
+
if (target.kind !== "directory") throw new TypeError(`Expected directory target, received "${target.kind}".`);
|
|
1241
|
+
return handlers.onTargetDirectory?.(target);
|
|
1242
|
+
case "file":
|
|
1243
|
+
if (target.kind !== "file") throw new TypeError(`Expected file target, received "${target.kind}".`);
|
|
1244
|
+
return handlers.onTargetFile?.(target);
|
|
1245
|
+
case "function":
|
|
1246
|
+
if (target.kind !== "function") throw new TypeError(`Expected function target, received "${target.kind}".`);
|
|
1247
|
+
return handlers.onTargetFunction?.(target);
|
|
1248
|
+
case "language-declaration-error": throw new Error(`Rule "${job.execution.runtime.enabledRule.id}" handles function or class targets but declares no "languages", so it receives none. Declare \`languages: 'any'\`, or list the language ids it handles.`);
|
|
1249
|
+
case "project":
|
|
1250
|
+
if (target.kind !== "project") throw new TypeError(`Expected project target, received "${target.kind}".`);
|
|
1251
|
+
return handlers.onTargetProject?.(target);
|
|
1252
|
+
case "with": return handlers.onTargetWith?.(target);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
982
1255
|
function scopeRank(scope) {
|
|
983
1256
|
if (scope === "source") return 0;
|
|
984
1257
|
if (scope === "directory") return 1;
|
|
@@ -989,33 +1262,44 @@ function scopeRank(scope) {
|
|
|
989
1262
|
function createRunProgress(filesTotal) {
|
|
990
1263
|
assertNonNegativeInteger(filesTotal, "File total");
|
|
991
1264
|
const execution = createCounts();
|
|
992
|
-
let
|
|
1265
|
+
let filesPlanned = 0;
|
|
1266
|
+
let planningComplete = false;
|
|
993
1267
|
const snapshot = () => {
|
|
994
1268
|
const jobsCompleted = terminalCount(execution);
|
|
995
1269
|
return {
|
|
996
1270
|
execution: { ...execution },
|
|
1271
|
+
filesPlanned,
|
|
997
1272
|
filesTotal,
|
|
998
|
-
final,
|
|
999
1273
|
jobsCompleted,
|
|
1000
1274
|
jobsStarted: jobsCompleted + execution.running,
|
|
1001
|
-
jobsTotal: execution.planned
|
|
1275
|
+
jobsTotal: execution.planned,
|
|
1276
|
+
planningComplete
|
|
1002
1277
|
};
|
|
1003
1278
|
};
|
|
1004
1279
|
return {
|
|
1280
|
+
completeFilePlanning: () => {
|
|
1281
|
+
if (planningComplete) throw new Error("Cannot complete file planning after planning is complete.");
|
|
1282
|
+
if (filesPlanned >= filesTotal) throw new Error("Cannot complete planning for more files than the file total.");
|
|
1283
|
+
filesPlanned += 1;
|
|
1284
|
+
return snapshot();
|
|
1285
|
+
},
|
|
1286
|
+
completePlanning: () => {
|
|
1287
|
+
planningComplete = true;
|
|
1288
|
+
return snapshot();
|
|
1289
|
+
},
|
|
1005
1290
|
finalize: () => {
|
|
1006
1291
|
if (execution.queued !== 0 || execution.running !== 0) throw new Error("Cannot finalize progress while jobs are queued or running.");
|
|
1007
|
-
|
|
1292
|
+
if (!planningComplete) throw new Error("Cannot finalize progress before planning is complete.");
|
|
1008
1293
|
return snapshot();
|
|
1009
1294
|
},
|
|
1010
1295
|
finish: (from, to) => {
|
|
1011
|
-
assertMutable(final);
|
|
1012
1296
|
if (execution[from] <= 0) throw new Error(`Cannot finish a job without a ${from} job.`);
|
|
1013
1297
|
execution[from] -= 1;
|
|
1014
1298
|
execution[to] += 1;
|
|
1015
1299
|
return snapshot();
|
|
1016
1300
|
},
|
|
1017
1301
|
queue: (count = 1) => {
|
|
1018
|
-
|
|
1302
|
+
if (planningComplete) throw new Error("Cannot queue work after planning is complete.");
|
|
1019
1303
|
assertNonNegativeInteger(count, "Queued job count");
|
|
1020
1304
|
assertSafeSum(execution.planned, count, "Planned job count");
|
|
1021
1305
|
assertSafeSum(execution.queued, count, "Queued job count");
|
|
@@ -1025,7 +1309,6 @@ function createRunProgress(filesTotal) {
|
|
|
1025
1309
|
},
|
|
1026
1310
|
snapshot,
|
|
1027
1311
|
start: () => {
|
|
1028
|
-
assertMutable(final);
|
|
1029
1312
|
if (execution.queued <= 0) throw new Error("Cannot start a job without a queued job.");
|
|
1030
1313
|
execution.queued -= 1;
|
|
1031
1314
|
execution.running += 1;
|
|
@@ -1033,9 +1316,6 @@ function createRunProgress(filesTotal) {
|
|
|
1033
1316
|
}
|
|
1034
1317
|
};
|
|
1035
1318
|
}
|
|
1036
|
-
function assertMutable(final) {
|
|
1037
|
-
if (final) throw new Error("Cannot update finalized progress.");
|
|
1038
|
-
}
|
|
1039
1319
|
function assertNonNegativeInteger(value, name) {
|
|
1040
1320
|
if (!Number.isSafeInteger(value) || value < 0) throw new TypeError(`${name} must be a non-negative integer.`);
|
|
1041
1321
|
}
|
|
@@ -1334,6 +1614,9 @@ var RuleScheduler = class {
|
|
|
1334
1614
|
this.settleClose();
|
|
1335
1615
|
return this.closePromise;
|
|
1336
1616
|
}
|
|
1617
|
+
completeFilePlanning() {
|
|
1618
|
+
return this.options.progress.completeFilePlanning();
|
|
1619
|
+
}
|
|
1337
1620
|
schedule(jobs) {
|
|
1338
1621
|
if (!this.accepting || jobs.length === 0) return {
|
|
1339
1622
|
jobsAdded: 0,
|
|
@@ -1568,152 +1851,6 @@ function createLane() {
|
|
|
1568
1851
|
};
|
|
1569
1852
|
}
|
|
1570
1853
|
//#endregion
|
|
1571
|
-
//#region src/dsl/registry.ts
|
|
1572
|
-
function buildRuleRegistry(config) {
|
|
1573
|
-
const rules = /* @__PURE__ */ new Map();
|
|
1574
|
-
const localIds = /* @__PURE__ */ new Map();
|
|
1575
|
-
const enabledRules = [];
|
|
1576
|
-
for (const [alias, plugin] of Object.entries(config.plugins)) for (const [localId, rule] of Object.entries(plugin.rules ?? {})) {
|
|
1577
|
-
const id = `${alias}/${localId}`;
|
|
1578
|
-
if (rules.has(id)) throw new Error(`Duplicate rule id "${id}".`);
|
|
1579
|
-
rules.set(id, rule);
|
|
1580
|
-
localIds.set(id, localId);
|
|
1581
|
-
}
|
|
1582
|
-
for (const [id, entry] of Object.entries(config.rules)) {
|
|
1583
|
-
const rule = rules.get(id);
|
|
1584
|
-
if (!rule) throw new Error(`Unknown rule "${id}".`);
|
|
1585
|
-
const normalizedEntry = normalizeRuleConfigEntry(entry);
|
|
1586
|
-
const severity = normalizedEntry.severity;
|
|
1587
|
-
if (severity === "off") continue;
|
|
1588
|
-
enabledRules.push({
|
|
1589
|
-
id,
|
|
1590
|
-
localId: localIds.get(id) ?? id,
|
|
1591
|
-
options: parseRuleOptions(id, rule.options, normalizedEntry.options),
|
|
1592
|
-
rule,
|
|
1593
|
-
severity
|
|
1594
|
-
});
|
|
1595
|
-
}
|
|
1596
|
-
return {
|
|
1597
|
-
enabledRules,
|
|
1598
|
-
rules
|
|
1599
|
-
};
|
|
1600
|
-
}
|
|
1601
|
-
function formatRuleOptionIssues(index, issues) {
|
|
1602
|
-
return issues.map((issue) => {
|
|
1603
|
-
const path = getDotPath(issue);
|
|
1604
|
-
return path === null ? `"${index}": ${issue.message}` : `"${index}.${path}": ${issue.message}`;
|
|
1605
|
-
}).join("; ");
|
|
1606
|
-
}
|
|
1607
|
-
function isRuleConfigTuple(entry) {
|
|
1608
|
-
return Array.isArray(entry);
|
|
1609
|
-
}
|
|
1610
|
-
function normalizeRuleConfigEntry(entry) {
|
|
1611
|
-
if (isRuleConfigTuple(entry)) {
|
|
1612
|
-
const [severity = "warn", ...options] = entry;
|
|
1613
|
-
return {
|
|
1614
|
-
options,
|
|
1615
|
-
severity
|
|
1616
|
-
};
|
|
1617
|
-
}
|
|
1618
|
-
return {
|
|
1619
|
-
options: [],
|
|
1620
|
-
severity: entry ?? "warn"
|
|
1621
|
-
};
|
|
1622
|
-
}
|
|
1623
|
-
function parseRuleOption(id, index, schema, value) {
|
|
1624
|
-
const result = safeParse(schema, value);
|
|
1625
|
-
if (result.success) return result.output;
|
|
1626
|
-
if (value === void 0) {
|
|
1627
|
-
const emptyObjectResult = safeParse(schema, {});
|
|
1628
|
-
if (emptyObjectResult.success) return emptyObjectResult.output;
|
|
1629
|
-
}
|
|
1630
|
-
throw new TypeError(`Invalid options for rule "${id}": ${formatRuleOptionIssues(index, result.issues)}`);
|
|
1631
|
-
}
|
|
1632
|
-
function parseRuleOptions(id, schema, options) {
|
|
1633
|
-
if (!schema) {
|
|
1634
|
-
if (options.length > 0) throw new TypeError(`Rule "${id}" does not accept options.`);
|
|
1635
|
-
return [];
|
|
1636
|
-
}
|
|
1637
|
-
if (options.length > schema.length) throw new TypeError(`Invalid options for rule "${id}": Unexpected option at index ${schema.length}.`);
|
|
1638
|
-
return schema.map((optionSchema, index) => parseRuleOption(id, index, optionSchema, options[index]));
|
|
1639
|
-
}
|
|
1640
|
-
//#endregion
|
|
1641
|
-
//#region src/core/preparation.ts
|
|
1642
|
-
function prepareRun(options = {}) {
|
|
1643
|
-
const cwd$2 = options.cwd ?? cwd();
|
|
1644
|
-
const config = options.config ?? [];
|
|
1645
|
-
const files = [];
|
|
1646
|
-
const directories = [];
|
|
1647
|
-
for (const filePath of options.files ?? []) {
|
|
1648
|
-
const path = resolve(cwd$2, filePath);
|
|
1649
|
-
const resolvedConfig = resolveConfigForFile(path, config, { cwd: cwd$2 });
|
|
1650
|
-
if (resolvedConfig.ignored) continue;
|
|
1651
|
-
const effectiveConfig = resolvedConfig.config;
|
|
1652
|
-
const language = resolveLanguageForPath(path, createLanguageRegistry(effectiveConfig), { language: effectiveConfig.language });
|
|
1653
|
-
files.push({
|
|
1654
|
-
agent: effectiveConfig.agent,
|
|
1655
|
-
configHash: stableHash({
|
|
1656
|
-
language: effectiveConfig.language,
|
|
1657
|
-
languageOptions: effectiveConfig.languageOptions,
|
|
1658
|
-
processor: effectiveConfig.processor,
|
|
1659
|
-
resolvedLanguage: language.name,
|
|
1660
|
-
settings: effectiveConfig.settings
|
|
1661
|
-
}),
|
|
1662
|
-
fileIndex: files.length,
|
|
1663
|
-
language,
|
|
1664
|
-
languageOptions: effectiveConfig.languageOptions,
|
|
1665
|
-
path,
|
|
1666
|
-
rules: prepareRules(effectiveConfig),
|
|
1667
|
-
settings: effectiveConfig.settings
|
|
1668
|
-
});
|
|
1669
|
-
}
|
|
1670
|
-
for (const directoryPath of options.directories ?? []) {
|
|
1671
|
-
const path = resolve(cwd$2, directoryPath);
|
|
1672
|
-
const resolvedConfig = resolveConfigForDirectory(path, config, { cwd: cwd$2 });
|
|
1673
|
-
if (resolvedConfig.ignored) continue;
|
|
1674
|
-
const effectiveConfig = resolvedConfig.config;
|
|
1675
|
-
directories.push({
|
|
1676
|
-
agent: effectiveConfig.agent,
|
|
1677
|
-
configHash: stableHash({ settings: effectiveConfig.settings }),
|
|
1678
|
-
directoryIndex: directories.length,
|
|
1679
|
-
rules: prepareRules(effectiveConfig),
|
|
1680
|
-
settings: effectiveConfig.settings,
|
|
1681
|
-
target: {
|
|
1682
|
-
kind: "directory",
|
|
1683
|
-
path
|
|
1684
|
-
}
|
|
1685
|
-
});
|
|
1686
|
-
}
|
|
1687
|
-
return {
|
|
1688
|
-
directories,
|
|
1689
|
-
files,
|
|
1690
|
-
project: options.projectTargets === false ? void 0 : prepareProject(cwd$2, config)
|
|
1691
|
-
};
|
|
1692
|
-
}
|
|
1693
|
-
function createLanguageRegistry(config) {
|
|
1694
|
-
const registry = createBuiltInLanguageRegistry();
|
|
1695
|
-
for (const plugin of Object.values(config.plugins)) for (const language of Object.values(plugin.languages ?? {})) registerLanguage(registry, language);
|
|
1696
|
-
return registry;
|
|
1697
|
-
}
|
|
1698
|
-
function prepareProject(root, config) {
|
|
1699
|
-
const resolvedConfig = resolveConfigForProject(root, config, { cwd: root });
|
|
1700
|
-
if (resolvedConfig.ignored) return void 0;
|
|
1701
|
-
const effectiveConfig = resolvedConfig.config;
|
|
1702
|
-
return {
|
|
1703
|
-
agent: effectiveConfig.agent,
|
|
1704
|
-
configHash: stableHash({ settings: effectiveConfig.settings }),
|
|
1705
|
-
root,
|
|
1706
|
-
rules: prepareRules(effectiveConfig),
|
|
1707
|
-
settings: effectiveConfig.settings
|
|
1708
|
-
};
|
|
1709
|
-
}
|
|
1710
|
-
function prepareRules(config) {
|
|
1711
|
-
return buildRuleRegistry(config).enabledRules.map((enabledRule, ruleIndex) => ({
|
|
1712
|
-
enabledRule,
|
|
1713
|
-
ruleIndex
|
|
1714
|
-
}));
|
|
1715
|
-
}
|
|
1716
|
-
//#endregion
|
|
1717
1854
|
//#region src/core/project/index.ts
|
|
1718
1855
|
var ProjectIndexBuilder = class {
|
|
1719
1856
|
files = [];
|
|
@@ -1767,7 +1904,7 @@ var ProjectIndexBuilder = class {
|
|
|
1767
1904
|
}
|
|
1768
1905
|
};
|
|
1769
1906
|
function createProjectJobs(options) {
|
|
1770
|
-
const executions = options.runtimes.map((runtime) => projectExecution(runtime
|
|
1907
|
+
const executions = options.runtimes.map((runtime) => projectExecution(runtime)).filter((execution) => execution !== void 0);
|
|
1771
1908
|
if (executions.length === 0) return { jobs: [] };
|
|
1772
1909
|
const owner = options.cacheStore.beginOwner({
|
|
1773
1910
|
kind: "project",
|
|
@@ -1777,10 +1914,9 @@ function createProjectJobs(options) {
|
|
|
1777
1914
|
cacheOwner: owner,
|
|
1778
1915
|
cacheTargetHash: options.project.hash,
|
|
1779
1916
|
configHash: options.configHash,
|
|
1917
|
+
descriptor: options.project.target,
|
|
1780
1918
|
identity: "project",
|
|
1781
|
-
kind: "project"
|
|
1782
|
-
language: "project",
|
|
1783
|
-
text: options.project.hash
|
|
1919
|
+
kind: "project"
|
|
1784
1920
|
};
|
|
1785
1921
|
return {
|
|
1786
1922
|
jobs: executions.map((execution) => {
|
|
@@ -1845,125 +1981,108 @@ function copyTargetSnapshot(target) {
|
|
|
1845
1981
|
semanticHash: target.semanticHash
|
|
1846
1982
|
};
|
|
1847
1983
|
}
|
|
1848
|
-
function projectExecution(runtime
|
|
1984
|
+
function projectExecution(runtime) {
|
|
1849
1985
|
if (runtime.handlers.onTargetWith) return {
|
|
1850
|
-
|
|
1986
|
+
handler: "with",
|
|
1851
1987
|
runtime
|
|
1852
1988
|
};
|
|
1853
1989
|
if (runtime.handlers.onTargetProject) return {
|
|
1854
|
-
|
|
1990
|
+
handler: "project",
|
|
1855
1991
|
runtime
|
|
1856
1992
|
};
|
|
1857
1993
|
}
|
|
1858
|
-
|
|
1859
|
-
|
|
1994
|
+
//#endregion
|
|
1995
|
+
//#region src/core/source/planner.ts
|
|
1996
|
+
/**
|
|
1997
|
+
* Converts one rich extractor result into detached descriptors and admits its compact jobs.
|
|
1998
|
+
*
|
|
1999
|
+
* The returned outcome promise captures only cache ownership and hashes. This function does not
|
|
2000
|
+
* await rule execution, so its `SourceFile`, parser output, and rich targets become unreachable
|
|
2001
|
+
* when planning returns.
|
|
2002
|
+
*/
|
|
2003
|
+
async function planSource(input, options) {
|
|
2004
|
+
if (options.signal?.aborted) return { outcomes: Promise.resolve([]) };
|
|
2005
|
+
let file;
|
|
1860
2006
|
try {
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
2007
|
+
file = await options.src.readFile(input.path);
|
|
2008
|
+
} catch (error) {
|
|
2009
|
+
reportFilePlanningComplete(input, 0, options);
|
|
2010
|
+
return {
|
|
2011
|
+
failure: fileFailure(input, "read", failureMessage(error, "Failed to read source file.")),
|
|
2012
|
+
outcomes: Promise.resolve([])
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
if (options.signal?.aborted) return { outcomes: Promise.resolve([]) };
|
|
2016
|
+
let targets;
|
|
2017
|
+
try {
|
|
2018
|
+
targets = await input.language.extract(file, {
|
|
2019
|
+
cwd: options.cwd,
|
|
2020
|
+
languageOptions: input.languageOptions,
|
|
2021
|
+
src: options.src
|
|
2022
|
+
});
|
|
2023
|
+
} catch (error) {
|
|
2024
|
+
reportFilePlanningComplete(input, 0, options);
|
|
2025
|
+
return {
|
|
2026
|
+
failure: fileFailure(input, "extract", failureMessage(error, "Failed to extract source targets.")),
|
|
2027
|
+
outcomes: Promise.resolve([])
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
const { contentHash } = file;
|
|
2031
|
+
let cacheOwner;
|
|
2032
|
+
let jobs;
|
|
2033
|
+
let project;
|
|
2034
|
+
try {
|
|
2035
|
+
project = options.projectSnapshots === false ? void 0 : createProjectSnapshot(input, file.language, file.path, contentHash, targets);
|
|
2036
|
+
cacheOwner = options.cacheStore.beginOwner({
|
|
1888
2037
|
kind: "file",
|
|
1889
2038
|
path: file.path
|
|
1890
2039
|
});
|
|
1891
|
-
const
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
inputPath: input.path,
|
|
1907
|
-
jobsAdded: batch.jobsAdded,
|
|
1908
|
-
progress: options.scheduler.snapshot()
|
|
1909
|
-
});
|
|
1910
|
-
} catch (error) {
|
|
1911
|
-
options.scheduler.cancelWithError(error);
|
|
1912
|
-
throw error;
|
|
1913
|
-
}
|
|
1914
|
-
const outcomes = await batch.outcomes;
|
|
1915
|
-
cacheOwner.commit(options.signal?.aborted ? {
|
|
2040
|
+
const baseFile = {
|
|
2041
|
+
contentHash,
|
|
2042
|
+
language: file.language,
|
|
2043
|
+
path: file.path
|
|
2044
|
+
};
|
|
2045
|
+
jobs = createSourceJobs(input, createExecutionTargets(input, targets, baseFile, options.ruleRuntimes, options.cwd, cacheOwner));
|
|
2046
|
+
} catch (error) {
|
|
2047
|
+
reportFilePlanningComplete(input, 0, options);
|
|
2048
|
+
return {
|
|
2049
|
+
failure: fileFailure(input, "extract", failureMessage(error, "Failed to plan source targets.")),
|
|
2050
|
+
outcomes: Promise.resolve([])
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
if (options.signal?.aborted) {
|
|
2054
|
+
cacheOwner.commit({
|
|
1916
2055
|
contentHash,
|
|
1917
2056
|
mode: "merge"
|
|
1918
|
-
}
|
|
2057
|
+
});
|
|
1919
2058
|
return {
|
|
1920
|
-
outcomes,
|
|
2059
|
+
outcomes: Promise.resolve([]),
|
|
1921
2060
|
project
|
|
1922
2061
|
};
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
const index = cursor;
|
|
1937
|
-
cursor += 1;
|
|
1938
|
-
const input = inputs[index];
|
|
1939
|
-
if (!input) return;
|
|
1940
|
-
try {
|
|
1941
|
-
results[index] = await executeSourceSession(input, {
|
|
1942
|
-
...options,
|
|
1943
|
-
ruleRuntimes: options.createRuleRuntimes(input)
|
|
1944
|
-
});
|
|
1945
|
-
} catch (error) {
|
|
1946
|
-
stopping = true;
|
|
1947
|
-
if (!failed) {
|
|
1948
|
-
failed = true;
|
|
1949
|
-
firstError = error;
|
|
1950
|
-
}
|
|
1951
|
-
}
|
|
1952
|
-
}
|
|
1953
|
-
});
|
|
1954
|
-
await Promise.all(workers);
|
|
1955
|
-
if (failed) throw firstError;
|
|
1956
|
-
return results.filter((result) => result !== void 0);
|
|
1957
|
-
}
|
|
1958
|
-
function resolveSourceWindow(ruleConcurrency) {
|
|
1959
|
-
return Math.max(1, Math.min(ruleConcurrency, 4));
|
|
2062
|
+
}
|
|
2063
|
+
const batch = options.scheduler.schedule(jobs);
|
|
2064
|
+
reportFilePlanningComplete(input, batch.jobsAdded, options);
|
|
2065
|
+
return {
|
|
2066
|
+
outcomes: batch.outcomes.then((settled) => {
|
|
2067
|
+
cacheOwner.commit(options.signal?.aborted ? {
|
|
2068
|
+
contentHash,
|
|
2069
|
+
mode: "merge"
|
|
2070
|
+
} : { contentHash });
|
|
2071
|
+
return settled;
|
|
2072
|
+
}),
|
|
2073
|
+
project
|
|
2074
|
+
};
|
|
1960
2075
|
}
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
2076
|
+
/**
|
|
2077
|
+
* Starts every input plan independently and preserves input-ordered results.
|
|
2078
|
+
*/
|
|
2079
|
+
async function planSources(inputs, options) {
|
|
2080
|
+
return Promise.all(inputs.map((input) => planSource(input, {
|
|
2081
|
+
...options,
|
|
2082
|
+
ruleRuntimes: options.createRuleRuntimes(input)
|
|
2083
|
+
})));
|
|
1965
2084
|
}
|
|
1966
|
-
function createExecutionTargets(input, targets, runtimes, cwd, cacheOwner) {
|
|
2085
|
+
function createExecutionTargets(input, targets, file, runtimes, cwd, cacheOwner) {
|
|
1967
2086
|
const resolveIdentity = createTargetIdentityResolver(targets.map((target) => ({
|
|
1968
2087
|
filePath: target.kind === "file" ? normalizeCachePath(cwd, input.path) : void 0,
|
|
1969
2088
|
identity: target.identity,
|
|
@@ -1971,32 +2090,50 @@ function createExecutionTargets(input, targets, runtimes, cwd, cacheOwner) {
|
|
|
1971
2090
|
name: target.name,
|
|
1972
2091
|
range: target.range
|
|
1973
2092
|
})));
|
|
1974
|
-
return targets.
|
|
2093
|
+
return targets.flatMap((target, targetIndex) => {
|
|
2094
|
+
const executions = runtimes.map((runtime) => sourceExecution(runtime, target.kind, target.language)).filter((execution) => execution !== void 0);
|
|
2095
|
+
if (executions.length === 0) return [];
|
|
2096
|
+
const identity = resolveIdentity({
|
|
2097
|
+
filePath: target.kind === "file" ? normalizeCachePath(cwd, input.path) : void 0,
|
|
2098
|
+
identity: target.identity,
|
|
2099
|
+
kind: target.kind,
|
|
2100
|
+
name: target.name,
|
|
2101
|
+
range: target.range
|
|
2102
|
+
}, targetIndex);
|
|
2103
|
+
const plannedTarget = {
|
|
2104
|
+
file,
|
|
2105
|
+
identity: target.identity,
|
|
2106
|
+
kind: target.kind,
|
|
2107
|
+
language: target.language,
|
|
2108
|
+
loc: target.loc && {
|
|
2109
|
+
end: { ...target.loc.end },
|
|
2110
|
+
start: { ...target.loc.start }
|
|
2111
|
+
},
|
|
2112
|
+
metadata: snapshotMetadata(target.metadata),
|
|
2113
|
+
name: target.name,
|
|
2114
|
+
origin: target.origin && {
|
|
2115
|
+
...target.origin,
|
|
2116
|
+
range: target.origin.range && { ...target.origin.range }
|
|
2117
|
+
},
|
|
2118
|
+
range: target.range && { ...target.range }
|
|
2119
|
+
};
|
|
1975
2120
|
return {
|
|
1976
|
-
executions
|
|
2121
|
+
executions,
|
|
1977
2122
|
target: {
|
|
1978
2123
|
activeFilePath: input.path,
|
|
1979
2124
|
cacheOwner,
|
|
2125
|
+
cacheTargetHash: targetSemanticHash(target),
|
|
1980
2126
|
configHash: input.configHash,
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
identity: target.identity,
|
|
1984
|
-
kind: target.kind,
|
|
1985
|
-
name: target.name,
|
|
1986
|
-
range: target.range
|
|
1987
|
-
}, targetIndex),
|
|
2127
|
+
descriptor: plannedTarget,
|
|
2128
|
+
identity,
|
|
1988
2129
|
kind: target.kind,
|
|
1989
|
-
language: target.language,
|
|
1990
2130
|
loc: target.loc,
|
|
1991
|
-
metadata: target.metadata,
|
|
1992
2131
|
name: target.name,
|
|
1993
|
-
|
|
1994
|
-
range: target.range,
|
|
1995
|
-
text: target.text
|
|
2132
|
+
range: target.range
|
|
1996
2133
|
},
|
|
1997
2134
|
targetIndex
|
|
1998
2135
|
};
|
|
1999
|
-
})
|
|
2136
|
+
});
|
|
2000
2137
|
}
|
|
2001
2138
|
function createProjectSnapshot(input, language, path, contentHash, targets) {
|
|
2002
2139
|
return {
|
|
@@ -2016,15 +2153,7 @@ function createProjectSnapshot(input, language, path, contentHash, targets) {
|
|
|
2016
2153
|
...target.name === void 0 ? {} : { name: target.name },
|
|
2017
2154
|
...target.range === void 0 ? {} : { range: { ...target.range } }
|
|
2018
2155
|
},
|
|
2019
|
-
semanticHash:
|
|
2020
|
-
language: target.language,
|
|
2021
|
-
loc: target.loc,
|
|
2022
|
-
metadata: target.metadata,
|
|
2023
|
-
name: target.name,
|
|
2024
|
-
origin: target.origin,
|
|
2025
|
-
range: target.range,
|
|
2026
|
-
text: target.text
|
|
2027
|
-
})
|
|
2156
|
+
semanticHash: targetSemanticHash(target)
|
|
2028
2157
|
}))
|
|
2029
2158
|
};
|
|
2030
2159
|
}
|
|
@@ -2077,36 +2206,81 @@ function fileFailure(input, kind, message) {
|
|
|
2077
2206
|
message
|
|
2078
2207
|
};
|
|
2079
2208
|
}
|
|
2080
|
-
function
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2209
|
+
function reportFilePlanningComplete(input, jobsAdded, options) {
|
|
2210
|
+
try {
|
|
2211
|
+
const progress = options.scheduler.completeFilePlanning();
|
|
2212
|
+
options.progress?.onFileReady?.({
|
|
2213
|
+
fileIndex: input.fileIndex,
|
|
2214
|
+
inputPath: input.path,
|
|
2215
|
+
jobsAdded,
|
|
2216
|
+
progress
|
|
2217
|
+
});
|
|
2218
|
+
} catch (error) {
|
|
2219
|
+
options.scheduler.cancelWithError(error);
|
|
2220
|
+
throw error;
|
|
2221
|
+
}
|
|
2085
2222
|
}
|
|
2086
|
-
function
|
|
2223
|
+
function snapshotMetadata(metadata) {
|
|
2224
|
+
if (metadata === void 0) return void 0;
|
|
2225
|
+
return Object.fromEntries(Object.entries(metadata).map(([key, value]) => [key, snapshotMetadataValue(value, /* @__PURE__ */ new WeakSet())]));
|
|
2226
|
+
}
|
|
2227
|
+
function snapshotMetadataValue(value, ancestors) {
|
|
2228
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
2229
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
2230
|
+
if (typeof value !== "object") throw new TypeError("Source target metadata must contain only finite JSON data.");
|
|
2231
|
+
if (ancestors.has(value)) throw new TypeError("Source target metadata must not contain cycles.");
|
|
2232
|
+
ancestors.add(value);
|
|
2233
|
+
try {
|
|
2234
|
+
if (Array.isArray(value)) return value.map((item) => snapshotMetadataValue(item, ancestors));
|
|
2235
|
+
const prototype = Object.getPrototypeOf(value);
|
|
2236
|
+
if (prototype !== Object.prototype && prototype !== null) throw new TypeError("Source target metadata must contain only plain objects and arrays.");
|
|
2237
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, snapshotMetadataValue(item, ancestors)]));
|
|
2238
|
+
} finally {
|
|
2239
|
+
ancestors.delete(value);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
function sourceExecution(runtime, kind, language) {
|
|
2243
|
+
const languages = resolveRuleLanguages(runtime.enabledRule.rule.languages);
|
|
2244
|
+
if (languages.kind === "off" && kind === "file" && (runtime.handlers.onTargetClass ?? runtime.handlers.onTargetFunction)) return {
|
|
2245
|
+
handler: "language-declaration-error",
|
|
2246
|
+
runtime
|
|
2247
|
+
};
|
|
2248
|
+
if (!isTargetLanguageAccepted(languages, kind, language)) return void 0;
|
|
2087
2249
|
if (runtime.handlers.onTargetWith) return {
|
|
2088
|
-
|
|
2250
|
+
handler: "with",
|
|
2089
2251
|
runtime
|
|
2090
2252
|
};
|
|
2091
|
-
if (
|
|
2092
|
-
|
|
2253
|
+
if (kind === "class" && runtime.handlers.onTargetClass) return {
|
|
2254
|
+
handler: "class",
|
|
2093
2255
|
runtime
|
|
2094
2256
|
};
|
|
2095
|
-
if (
|
|
2096
|
-
|
|
2257
|
+
if (kind === "file" && runtime.handlers.onTargetFile) return {
|
|
2258
|
+
handler: "file",
|
|
2097
2259
|
runtime
|
|
2098
2260
|
};
|
|
2099
|
-
if (
|
|
2100
|
-
|
|
2261
|
+
if (kind === "function" && runtime.handlers.onTargetFunction) return {
|
|
2262
|
+
handler: "function",
|
|
2101
2263
|
runtime
|
|
2102
2264
|
};
|
|
2103
2265
|
}
|
|
2266
|
+
/** Keeps per-rule cache fingerprints and compact project snapshots on one semantic target identity. */
|
|
2267
|
+
function targetSemanticHash(target) {
|
|
2268
|
+
return stableHash({
|
|
2269
|
+
language: target.language,
|
|
2270
|
+
loc: target.loc,
|
|
2271
|
+
metadata: target.metadata,
|
|
2272
|
+
name: target.name,
|
|
2273
|
+
origin: target.origin,
|
|
2274
|
+
range: target.range,
|
|
2275
|
+
text: target.text
|
|
2276
|
+
});
|
|
2277
|
+
}
|
|
2104
2278
|
//#endregion
|
|
2105
2279
|
//#region src/core/targets/directory.ts
|
|
2106
2280
|
function createDirectoryJobs(input, runtimes) {
|
|
2107
2281
|
const target = input.target;
|
|
2108
2282
|
return runtimes.flatMap((runtime) => {
|
|
2109
|
-
const execution = directoryExecution(runtime
|
|
2283
|
+
const execution = directoryExecution(runtime);
|
|
2110
2284
|
if (!execution) return [];
|
|
2111
2285
|
const ruleId = runtime.enabledRule.id;
|
|
2112
2286
|
return [{
|
|
@@ -2134,22 +2308,22 @@ function createDirectoryJobs(input, runtimes) {
|
|
|
2134
2308
|
},
|
|
2135
2309
|
target: {
|
|
2136
2310
|
activeFilePath: target.path,
|
|
2311
|
+
cacheTargetHash: stableHash(target),
|
|
2137
2312
|
configHash: input.configHash,
|
|
2313
|
+
descriptor: target,
|
|
2138
2314
|
identity: target.path,
|
|
2139
|
-
kind: "directory"
|
|
2140
|
-
language: "directory",
|
|
2141
|
-
text: target.path
|
|
2315
|
+
kind: "directory"
|
|
2142
2316
|
}
|
|
2143
2317
|
}];
|
|
2144
2318
|
});
|
|
2145
2319
|
}
|
|
2146
|
-
function directoryExecution(runtime
|
|
2320
|
+
function directoryExecution(runtime) {
|
|
2147
2321
|
if (runtime.handlers.onTargetWith) return {
|
|
2148
|
-
|
|
2322
|
+
handler: "with",
|
|
2149
2323
|
runtime
|
|
2150
2324
|
};
|
|
2151
2325
|
if (runtime.handlers.onTargetDirectory) return {
|
|
2152
|
-
|
|
2326
|
+
handler: "directory",
|
|
2153
2327
|
runtime
|
|
2154
2328
|
};
|
|
2155
2329
|
}
|
|
@@ -2199,11 +2373,12 @@ async function runAlint(options = {}) {
|
|
|
2199
2373
|
startedAt: runStartedAt
|
|
2200
2374
|
});
|
|
2201
2375
|
const cwd$1 = options.cwd ?? cwd();
|
|
2376
|
+
const config = options.config ?? [];
|
|
2202
2377
|
const setupConfig = options.setupConfig ?? {
|
|
2203
2378
|
providers: [],
|
|
2204
2379
|
version: 1
|
|
2205
2380
|
};
|
|
2206
|
-
const src = createSourceRuntime();
|
|
2381
|
+
const src = createSourceRuntime({ extract: createSourceExtractor(cwd$1, config, () => src) });
|
|
2207
2382
|
const normalizedCacheConfig = normalizeRunnerCacheConfig(options.runner?.cache, cwd$1);
|
|
2208
2383
|
const cacheStore = await createCacheStore({
|
|
2209
2384
|
cwd: cwd$1,
|
|
@@ -2250,7 +2425,7 @@ async function runAlint(options = {}) {
|
|
|
2250
2425
|
try {
|
|
2251
2426
|
const projectRuntimes = preparation.project ? createRuntimes(preparation.project) : [];
|
|
2252
2427
|
const projectBuilder = preparation.project && projectRuntimes.some((runtime) => runtime.handlers.onTargetWith || runtime.handlers.onTargetProject) ? new ProjectIndexBuilder(preparation.project.root) : void 0;
|
|
2253
|
-
const sourceResults = await
|
|
2428
|
+
const sourceResults = await planSources(preparation.files, {
|
|
2254
2429
|
cacheStore,
|
|
2255
2430
|
createRuleRuntimes: createRuntimes,
|
|
2256
2431
|
cwd: cwd$1,
|
|
@@ -2258,20 +2433,18 @@ async function runAlint(options = {}) {
|
|
|
2258
2433
|
projectSnapshots: projectBuilder !== void 0,
|
|
2259
2434
|
scheduler,
|
|
2260
2435
|
signal: options.signal,
|
|
2261
|
-
sourceWindow: resolveSourceWindow(concurrency),
|
|
2262
2436
|
src
|
|
2263
2437
|
});
|
|
2264
|
-
for (const result of sourceResults)
|
|
2265
|
-
|
|
2266
|
-
if (result.failure) fileFailures.push(result.failure);
|
|
2267
|
-
}
|
|
2438
|
+
for (const result of sourceResults) if (result.failure) fileFailures.push(result.failure);
|
|
2439
|
+
const pendingOutcomes = sourceResults.map((result) => result.outcomes);
|
|
2268
2440
|
if (projectBuilder) {
|
|
2269
2441
|
for (const result of [...sourceResults].sort((left, right) => (left.project?.fileIndex ?? Number.MAX_SAFE_INTEGER) - (right.project?.fileIndex ?? Number.MAX_SAFE_INTEGER))) if (result.project) projectBuilder.add(result.project);
|
|
2270
2442
|
}
|
|
2271
2443
|
const directoryBatches = preparation.directories.map((input) => {
|
|
2272
2444
|
return scheduler.schedule(createDirectoryJobs(input, createRuntimes(input)));
|
|
2273
2445
|
});
|
|
2274
|
-
|
|
2446
|
+
pendingOutcomes.push(...directoryBatches.map((batch) => batch.outcomes));
|
|
2447
|
+
let clearEmptyProjectOwner = false;
|
|
2275
2448
|
if (fileFailures.length === 0 && preparation.project && !options.signal?.aborted) {
|
|
2276
2449
|
if (projectBuilder) {
|
|
2277
2450
|
const project = createProjectJobs({
|
|
@@ -2282,19 +2455,26 @@ async function runAlint(options = {}) {
|
|
|
2282
2455
|
});
|
|
2283
2456
|
const projectOwner = project.owner;
|
|
2284
2457
|
const batch = scheduler.schedule(project.jobs);
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
}).commit({ mode: "replace" });
|
|
2458
|
+
pendingOutcomes.push(batch.outcomes.then((settled) => {
|
|
2459
|
+
projectOwner?.commit({ mode: options.signal?.aborted ? "merge" : "replace" });
|
|
2460
|
+
return settled;
|
|
2461
|
+
}));
|
|
2462
|
+
} else if (!options.cacheOnly) clearEmptyProjectOwner = true;
|
|
2291
2463
|
}
|
|
2464
|
+
const planningProgress = runProgress.completePlanning();
|
|
2465
|
+
options.progress?.onPlanningEnd?.({ progress: planningProgress });
|
|
2466
|
+
outcomes.push(...(await Promise.all(pendingOutcomes)).flat());
|
|
2467
|
+
if (clearEmptyProjectOwner) cacheStore.beginOwner({
|
|
2468
|
+
kind: "project",
|
|
2469
|
+
path: preparation.project.root
|
|
2470
|
+
}).commit({ mode: "replace" });
|
|
2292
2471
|
} catch (error) {
|
|
2293
2472
|
scheduler.cancelWithError(error);
|
|
2294
2473
|
infrastructureError = error;
|
|
2295
2474
|
infrastructureFailed = true;
|
|
2296
2475
|
admissionFailed = true;
|
|
2297
2476
|
} finally {
|
|
2477
|
+
runProgress.completePlanning();
|
|
2298
2478
|
try {
|
|
2299
2479
|
await scheduler.close();
|
|
2300
2480
|
} catch (error) {
|
|
@@ -2311,7 +2491,11 @@ async function runAlint(options = {}) {
|
|
|
2311
2491
|
const ruleFailures = outcomes.flatMap((outcome) => outcome.state === "failed" ? [outcome.failure] : []);
|
|
2312
2492
|
const failures = [...fileFailures.sort((left, right) => left.file.index - right.file.index), ...ruleFailures];
|
|
2313
2493
|
const result = {
|
|
2314
|
-
diagnostics:
|
|
2494
|
+
diagnostics: [
|
|
2495
|
+
...missingLanguageDiagnostics(preparation.missingLanguages),
|
|
2496
|
+
...unregisteredLanguageDiagnostics(preparation.unregisteredLanguages),
|
|
2497
|
+
...outcomes.flatMap((outcome) => outcome.diagnostics)
|
|
2498
|
+
],
|
|
2315
2499
|
execution: executionCounts(outcomes),
|
|
2316
2500
|
usage: runUsage(outcomes)
|
|
2317
2501
|
};
|
|
@@ -2385,4 +2569,303 @@ function usageTotals(records) {
|
|
|
2385
2569
|
};
|
|
2386
2570
|
}
|
|
2387
2571
|
//#endregion
|
|
2388
|
-
|
|
2572
|
+
//#region src/models/benchmark-scheduler.ts
|
|
2573
|
+
/** Runs FIFO job queues independently so one provider's cap cannot throttle or overload another provider. */
|
|
2574
|
+
async function scheduleProviderJobs(jobs, concurrencyByProvider, execute) {
|
|
2575
|
+
const queues = /* @__PURE__ */ new Map();
|
|
2576
|
+
for (const [index, job] of jobs.entries()) {
|
|
2577
|
+
const queue = queues.get(job.providerId) ?? [];
|
|
2578
|
+
queue.push({
|
|
2579
|
+
index,
|
|
2580
|
+
job
|
|
2581
|
+
});
|
|
2582
|
+
queues.set(job.providerId, queue);
|
|
2583
|
+
}
|
|
2584
|
+
const limits = /* @__PURE__ */ new Map();
|
|
2585
|
+
for (const providerId of queues.keys()) {
|
|
2586
|
+
const limit = concurrencyByProvider[providerId] ?? 1;
|
|
2587
|
+
if (!Number.isInteger(limit) || limit < 1) throw new RangeError(`Benchmark concurrency for provider "${providerId}" must be a positive integer.`);
|
|
2588
|
+
limits.set(providerId, limit);
|
|
2589
|
+
}
|
|
2590
|
+
const results = Array.from({ length: jobs.length });
|
|
2591
|
+
const workers = [];
|
|
2592
|
+
for (const [providerId, queue] of queues) {
|
|
2593
|
+
const workerCount = Math.min(limits.get(providerId), queue.length);
|
|
2594
|
+
let nextJobIndex = 0;
|
|
2595
|
+
for (let workerIndex = 0; workerIndex < workerCount; workerIndex += 1) workers.push((async () => {
|
|
2596
|
+
while (nextJobIndex < queue.length) {
|
|
2597
|
+
const scheduled = queue[nextJobIndex];
|
|
2598
|
+
nextJobIndex += 1;
|
|
2599
|
+
if (scheduled !== void 0) results[scheduled.index] = { value: await execute(scheduled.job, scheduled.index) };
|
|
2600
|
+
}
|
|
2601
|
+
})());
|
|
2602
|
+
}
|
|
2603
|
+
await Promise.all(workers);
|
|
2604
|
+
return results.map((result, index) => {
|
|
2605
|
+
if (result === void 0) throw new Error(`Benchmark scheduler did not complete job ${index}.`);
|
|
2606
|
+
return result.value;
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
//#endregion
|
|
2610
|
+
//#region src/models/benchmark.ts
|
|
2611
|
+
const defaultSampleCount = 3;
|
|
2612
|
+
const defaultTimeoutMs = 6e4;
|
|
2613
|
+
const maxOutputTokens = 128;
|
|
2614
|
+
/**
|
|
2615
|
+
* Benchmarks model jobs concurrently within independent provider limits.
|
|
2616
|
+
*
|
|
2617
|
+
* Each model's repeat warm-up, measured repeats, and non-repeats remain serial so concurrent samples cannot
|
|
2618
|
+
* distort that model's cache behavior. Non-repeat prompts put their nonce before the shared corpus.
|
|
2619
|
+
*/
|
|
2620
|
+
async function benchmarkModels(config, options = {}) {
|
|
2621
|
+
const request = options.request ?? measureModel;
|
|
2622
|
+
const sampleCount = options.sampleCount ?? defaultSampleCount;
|
|
2623
|
+
const seed = options.seed ?? Math.floor(Date.now() / 1e3);
|
|
2624
|
+
const timeoutMs = options.timeoutMs ?? config.runner?.timeoutMs ?? defaultTimeoutMs;
|
|
2625
|
+
const jobs = config.providers.flatMap((provider) => provider.models.map((model) => ({
|
|
2626
|
+
identity: `${provider.id}/${model.id}`,
|
|
2627
|
+
model,
|
|
2628
|
+
providerId: provider.id
|
|
2629
|
+
})));
|
|
2630
|
+
const results = Array.from({ length: jobs.length });
|
|
2631
|
+
const active = /* @__PURE__ */ new Map();
|
|
2632
|
+
const modelsTotal = jobs.length;
|
|
2633
|
+
if (!Number.isInteger(sampleCount) || sampleCount < 1) throw new RangeError("Benchmark sample count must be a positive integer.");
|
|
2634
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new RangeError("Benchmark timeout must be greater than zero.");
|
|
2635
|
+
return scheduleProviderJobs(jobs, options.providerConcurrency ?? {}, async (job, modelIndex) => {
|
|
2636
|
+
const baseResult = {
|
|
2637
|
+
modelId: job.model.id,
|
|
2638
|
+
providerId: job.providerId,
|
|
2639
|
+
success: {
|
|
2640
|
+
attempted: 0,
|
|
2641
|
+
completed: 0
|
|
2642
|
+
}
|
|
2643
|
+
};
|
|
2644
|
+
let model;
|
|
2645
|
+
try {
|
|
2646
|
+
model = resolveModel(config, { request: job.identity });
|
|
2647
|
+
} catch (error) {
|
|
2648
|
+
const result = {
|
|
2649
|
+
...baseResult,
|
|
2650
|
+
error: errorMessageFrom(error) ?? "Could not resolve model."
|
|
2651
|
+
};
|
|
2652
|
+
results[modelIndex] = result;
|
|
2653
|
+
reportProgress(options, modelsTotal, results, active);
|
|
2654
|
+
return result;
|
|
2655
|
+
}
|
|
2656
|
+
const result = await benchmarkModel(model, modelIndex, seed, sampleCount, timeoutMs, request, (current) => {
|
|
2657
|
+
active.set(modelIndex, current);
|
|
2658
|
+
reportProgress(options, modelsTotal, results, active);
|
|
2659
|
+
});
|
|
2660
|
+
active.delete(modelIndex);
|
|
2661
|
+
results[modelIndex] = result;
|
|
2662
|
+
reportProgress(options, modelsTotal, results, active);
|
|
2663
|
+
return result;
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2666
|
+
async function attempt(result, request, timeoutMs, onStarted) {
|
|
2667
|
+
result.success.attempted += 1;
|
|
2668
|
+
onStarted();
|
|
2669
|
+
const measurement = await request(AbortSignal.timeout(timeoutMs));
|
|
2670
|
+
result.success.completed += 1;
|
|
2671
|
+
return measurement;
|
|
2672
|
+
}
|
|
2673
|
+
async function benchmarkModel(model, modelIndex, seed, sampleCount, timeoutMs, request, onProgress) {
|
|
2674
|
+
const result = {
|
|
2675
|
+
modelId: model.id,
|
|
2676
|
+
providerId: model.provider.id,
|
|
2677
|
+
success: {
|
|
2678
|
+
attempted: 0,
|
|
2679
|
+
completed: 0
|
|
2680
|
+
}
|
|
2681
|
+
};
|
|
2682
|
+
const repeatPrompt = benchmarkPrompt(seed, model, 0, true);
|
|
2683
|
+
const repeatMeasurements = [];
|
|
2684
|
+
const nonRepeatMeasurements = [];
|
|
2685
|
+
try {
|
|
2686
|
+
await attempt(result, (signal) => request({
|
|
2687
|
+
model,
|
|
2688
|
+
prompt: repeatPrompt,
|
|
2689
|
+
signal
|
|
2690
|
+
}), timeoutMs, () => onProgress(currentProgress(model, modelIndex, result, "repeat", 0, sampleCount, true)));
|
|
2691
|
+
for (let index = 0; index < sampleCount; index += 1) repeatMeasurements.push(await attempt(result, (signal) => request({
|
|
2692
|
+
model,
|
|
2693
|
+
prompt: repeatPrompt,
|
|
2694
|
+
signal
|
|
2695
|
+
}), timeoutMs, () => onProgress(currentProgress(model, modelIndex, result, "repeat", index + 1, sampleCount, false))));
|
|
2696
|
+
for (let index = 0; index < sampleCount; index += 1) {
|
|
2697
|
+
const prompt = benchmarkPrompt(seed, model, index + 1, false);
|
|
2698
|
+
nonRepeatMeasurements.push(await attempt(result, (signal) => request({
|
|
2699
|
+
model,
|
|
2700
|
+
prompt,
|
|
2701
|
+
signal
|
|
2702
|
+
}), timeoutMs, () => onProgress(currentProgress(model, modelIndex, result, "non-repeat", index + 1, sampleCount, false))));
|
|
2703
|
+
}
|
|
2704
|
+
} catch (error) {
|
|
2705
|
+
return {
|
|
2706
|
+
...result,
|
|
2707
|
+
error: errorMessageFrom(error) ?? "Benchmark request failed."
|
|
2708
|
+
};
|
|
2709
|
+
}
|
|
2710
|
+
return {
|
|
2711
|
+
...result,
|
|
2712
|
+
nonRepeatMs: median(nonRepeatMeasurements.map((measurement) => measurement.durationMs)),
|
|
2713
|
+
repeatMs: median(repeatMeasurements.map((measurement) => measurement.durationMs)),
|
|
2714
|
+
throughput: medianOptional(nonRepeatMeasurements.map(throughputOf))
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
function benchmarkPrompt(seed, model, sample, repeat) {
|
|
2718
|
+
const nonce = repeat ? `${seed}:${model.provider.id}/${model.id}:repeat` : `${seed}:${model.provider.id}/${model.id}:non-repeat:${sample}`;
|
|
2719
|
+
const corpus = sampleCorpus(seed + sample);
|
|
2720
|
+
return [
|
|
2721
|
+
`[alint benchmark nonce: ${nonce}]`,
|
|
2722
|
+
"Read the following source review corpus. Return exactly eight concise bullet points summarizing its behavior, risks, and maintainability. Do not quote the nonce.",
|
|
2723
|
+
"",
|
|
2724
|
+
corpus
|
|
2725
|
+
].join("\n");
|
|
2726
|
+
}
|
|
2727
|
+
function currentProgress(model, modelIndex, result, phase, sample, samples, warmup) {
|
|
2728
|
+
return {
|
|
2729
|
+
modelId: model.id,
|
|
2730
|
+
modelIndex,
|
|
2731
|
+
phase,
|
|
2732
|
+
providerId: model.provider.id,
|
|
2733
|
+
sample,
|
|
2734
|
+
samples,
|
|
2735
|
+
success: { ...result.success },
|
|
2736
|
+
warmup
|
|
2737
|
+
};
|
|
2738
|
+
}
|
|
2739
|
+
async function measureModel({ model, prompt, signal }) {
|
|
2740
|
+
const messages = [{
|
|
2741
|
+
content: prompt,
|
|
2742
|
+
role: "user"
|
|
2743
|
+
}];
|
|
2744
|
+
const startedAt = performance.now();
|
|
2745
|
+
let firstOutputAt;
|
|
2746
|
+
const result = streamText({
|
|
2747
|
+
...model.params,
|
|
2748
|
+
abortSignal: signal,
|
|
2749
|
+
baseURL: model.provider.endpoint,
|
|
2750
|
+
headers: model.provider.headers,
|
|
2751
|
+
maxTokens: maxOutputTokens,
|
|
2752
|
+
messages,
|
|
2753
|
+
model: model.id,
|
|
2754
|
+
streamOptions: { includeUsage: true },
|
|
2755
|
+
temperature: 0
|
|
2756
|
+
});
|
|
2757
|
+
const completion = Promise.allSettled([
|
|
2758
|
+
result.messages,
|
|
2759
|
+
result.steps,
|
|
2760
|
+
result.totalUsage,
|
|
2761
|
+
result.usage
|
|
2762
|
+
]);
|
|
2763
|
+
let settled;
|
|
2764
|
+
try {
|
|
2765
|
+
for await (const event of result.eventStream) if (firstOutputAt === void 0 && (event.type === "reasoning.delta" || event.type === "text.delta")) firstOutputAt = performance.now();
|
|
2766
|
+
} finally {
|
|
2767
|
+
settled = await completion;
|
|
2768
|
+
}
|
|
2769
|
+
const endedAt = performance.now();
|
|
2770
|
+
const usageResult = settled[3];
|
|
2771
|
+
if (usageResult.status === "rejected") throw usageResult.reason;
|
|
2772
|
+
return {
|
|
2773
|
+
durationMs: endedAt - startedAt,
|
|
2774
|
+
firstOutputMs: firstOutputAt === void 0 ? void 0 : firstOutputAt - startedAt,
|
|
2775
|
+
outputTokens: usageResult.value?.outputTokens
|
|
2776
|
+
};
|
|
2777
|
+
}
|
|
2778
|
+
function median(values) {
|
|
2779
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
2780
|
+
const middle = Math.floor(sorted.length / 2);
|
|
2781
|
+
return sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
|
|
2782
|
+
}
|
|
2783
|
+
function medianOptional(values) {
|
|
2784
|
+
const present = values.filter((value) => value !== void 0);
|
|
2785
|
+
return present.length === 0 ? void 0 : median(present);
|
|
2786
|
+
}
|
|
2787
|
+
function reportProgress(options, modelsTotal, results, active) {
|
|
2788
|
+
options.onProgress?.({
|
|
2789
|
+
active: [...active.values()].sort((left, right) => left.modelIndex - right.modelIndex).map((current) => ({
|
|
2790
|
+
...current,
|
|
2791
|
+
success: { ...current.success }
|
|
2792
|
+
})),
|
|
2793
|
+
modelsTotal,
|
|
2794
|
+
results: results.map((result) => result === void 0 ? void 0 : {
|
|
2795
|
+
...result,
|
|
2796
|
+
success: { ...result.success }
|
|
2797
|
+
})
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2800
|
+
function throughputOf(measurement) {
|
|
2801
|
+
if (measurement.firstOutputMs === void 0 || measurement.outputTokens === void 0 || measurement.durationMs <= measurement.firstOutputMs) return;
|
|
2802
|
+
return measurement.outputTokens / ((measurement.durationMs - measurement.firstOutputMs) / 1e3);
|
|
2803
|
+
}
|
|
2804
|
+
const corpusLines = [
|
|
2805
|
+
"type QueueEntry = { id: string, attempts: number, payload: string }",
|
|
2806
|
+
"type QueueState = { active: Map<string, QueueEntry>, pending: QueueEntry[] }",
|
|
2807
|
+
"",
|
|
2808
|
+
"export function createQueue(limit: number) {",
|
|
2809
|
+
" const state: QueueState = { active: new Map(), pending: [] }",
|
|
2810
|
+
" let closed = false",
|
|
2811
|
+
"",
|
|
2812
|
+
" function enqueue(entry: QueueEntry): boolean {",
|
|
2813
|
+
" if (closed || state.active.has(entry.id)) return false",
|
|
2814
|
+
" state.pending.push({ ...entry })",
|
|
2815
|
+
" drain()",
|
|
2816
|
+
" return true",
|
|
2817
|
+
" }",
|
|
2818
|
+
"",
|
|
2819
|
+
" function drain(): void {",
|
|
2820
|
+
" while (!closed && state.active.size < limit && state.pending.length > 0) {",
|
|
2821
|
+
" const next = state.pending.shift()",
|
|
2822
|
+
" if (!next) break",
|
|
2823
|
+
" state.active.set(next.id, next)",
|
|
2824
|
+
" void execute(next).finally(() => {",
|
|
2825
|
+
" state.active.delete(next.id)",
|
|
2826
|
+
" drain()",
|
|
2827
|
+
" })",
|
|
2828
|
+
" }",
|
|
2829
|
+
" }",
|
|
2830
|
+
"",
|
|
2831
|
+
" async function execute(entry: QueueEntry): Promise<void> {",
|
|
2832
|
+
" const response = await fetch(\"/jobs/\" + entry.id, {",
|
|
2833
|
+
" body: JSON.stringify(entry.payload),",
|
|
2834
|
+
" headers: { \"content-type\": \"application/json\" },",
|
|
2835
|
+
" method: \"POST\",",
|
|
2836
|
+
" })",
|
|
2837
|
+
" if (!response.ok && entry.attempts < 3) {",
|
|
2838
|
+
" state.pending.push({ ...entry, attempts: entry.attempts + 1 })",
|
|
2839
|
+
" }",
|
|
2840
|
+
" }",
|
|
2841
|
+
"",
|
|
2842
|
+
" function close(): void {",
|
|
2843
|
+
" closed = true",
|
|
2844
|
+
" state.pending.length = 0",
|
|
2845
|
+
" }",
|
|
2846
|
+
"",
|
|
2847
|
+
" return { close, enqueue }",
|
|
2848
|
+
"}",
|
|
2849
|
+
"",
|
|
2850
|
+
"The queue accepts independent jobs and limits concurrent requests.",
|
|
2851
|
+
"Its retry path changes ordering because failed jobs return at the tail.",
|
|
2852
|
+
"Closing drops pending work but does not cancel requests already in flight.",
|
|
2853
|
+
"Callers receive admission status but cannot observe completion or failure.",
|
|
2854
|
+
"The mutable map prevents duplicate active identifiers, not duplicate pending identifiers.",
|
|
2855
|
+
"A synchronous drain call establishes ownership before asynchronous execution starts.",
|
|
2856
|
+
"Network failures reject execute and are not retried by the response-status branch.",
|
|
2857
|
+
"The relative URL assumes a runtime with an application-specific fetch base.",
|
|
2858
|
+
"Payload serialization can throw before a request is created.",
|
|
2859
|
+
"The limit must be positive or pending work can remain stuck forever."
|
|
2860
|
+
];
|
|
2861
|
+
function sampleCorpus(seed) {
|
|
2862
|
+
const offset = Math.abs(seed) % corpusLines.length;
|
|
2863
|
+
const rotated = [...corpusLines.slice(offset), ...corpusLines.slice(0, offset)].join("\n");
|
|
2864
|
+
return [
|
|
2865
|
+
1,
|
|
2866
|
+
2,
|
|
2867
|
+
3
|
|
2868
|
+
].map((section) => `--- corpus section ${section} ---\n${rotated}`).join("\n\n");
|
|
2869
|
+
}
|
|
2870
|
+
//#endregion
|
|
2871
|
+
export { AlintAbortError, AlintRunCancelledError, AlintRunError, benchmarkModels, buildRuleRegistry, createBuiltInLanguageRegistry, createSourceExtractor, createSourceFile, createSourceRuntime, defineConfig, definePlugin, defineRule, hasDiscoveryFilePatterns, matchesDiscoveryFile, normalizeConfig, registerLanguage, resolveConfigForDirectory, resolveConfigForFile, resolveConfigForProject, resolveLanguage, resolveModel, runAlint, sliceLines, sliceRange, targetIdentity, withLanguage, withStableIdentities };
|