@alint-js/core 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +30 -21
- package/dist/index.mjs +1028 -1005
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -4,16 +4,17 @@ import { a as createSourceFile, c as sliceRange, d as stableHash, i as SourceCha
|
|
|
4
4
|
import { minimatch } from "minimatch";
|
|
5
5
|
import { basename, extname, isAbsolute, relative, resolve } from "pathe";
|
|
6
6
|
import { array, boolean, check, custom, finite, getDotPath, integer, literal, looseObject, maxValue, minValue, number, object, optional, parse, picklist, pipe, record, safeParse, string, union, unknown } from "valibot";
|
|
7
|
-
import { randomUUID } from "node:crypto";
|
|
8
|
-
import { access, appendFile, mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
9
|
-
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";
|
|
10
7
|
import process, { cwd } from "node:process";
|
|
11
8
|
import { Buffer } from "node:buffer";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
12
10
|
import { statSync } from "node:fs";
|
|
11
|
+
import { access, appendFile, mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
12
|
+
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";
|
|
13
13
|
import lockfile from "proper-lockfile";
|
|
14
14
|
import { isNodeErrorCode } from "@alint-js/utils/node";
|
|
15
15
|
import { errorMessageFrom } from "@moeru/std/error";
|
|
16
16
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
17
|
+
import { setImmediate } from "node:timers/promises";
|
|
17
18
|
import { streamText } from "@xsai/stream-text";
|
|
18
19
|
//#region src/config/config-array.ts
|
|
19
20
|
const effectiveBasePathSymbol = Symbol("effectiveBasePath");
|
|
@@ -269,848 +270,480 @@ function mergeStopGateConfig(resolved, item) {
|
|
|
269
270
|
};
|
|
270
271
|
}
|
|
271
272
|
//#endregion
|
|
272
|
-
//#region
|
|
273
|
-
|
|
274
|
-
extensions: [
|
|
275
|
-
".cjs",
|
|
276
|
-
".js",
|
|
277
|
-
".jsx",
|
|
278
|
-
".mjs"
|
|
279
|
-
],
|
|
280
|
-
extract: (file) => extractJsSourceTargets(withLanguage(file, "javascript")),
|
|
281
|
-
name: "javascript"
|
|
282
|
-
};
|
|
273
|
+
//#region package.json
|
|
274
|
+
var version = "0.7.1";
|
|
283
275
|
//#endregion
|
|
284
|
-
//#region src/core/
|
|
285
|
-
|
|
276
|
+
//#region src/core/cache/types.ts
|
|
277
|
+
const CACHE_MAGIC = "ALINT_CACHE";
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region src/core/cache/store.ts
|
|
280
|
+
const DEFAULT_CACHE_FILE_NAME = ".alintcache";
|
|
281
|
+
const LOCK_STALE_MS = 2e4;
|
|
282
|
+
const LOCK_UPDATE_MS = 3e3;
|
|
283
|
+
const METADATA_READ_LIMIT = 4096;
|
|
284
|
+
const FiniteNumberSchema = pipe(number(), finite());
|
|
285
|
+
const PositionSchema = object({
|
|
286
|
+
column: FiniteNumberSchema,
|
|
287
|
+
line: FiniteNumberSchema
|
|
288
|
+
});
|
|
289
|
+
const SourceLocationSchema = object({
|
|
290
|
+
end: PositionSchema,
|
|
291
|
+
start: PositionSchema
|
|
292
|
+
});
|
|
293
|
+
const DiagnosticLocationSchema = object({
|
|
294
|
+
end: optional(PositionSchema),
|
|
295
|
+
start: PositionSchema
|
|
296
|
+
});
|
|
297
|
+
const SourceRangeSchema = object({
|
|
298
|
+
end: FiniteNumberSchema,
|
|
299
|
+
start: FiniteNumberSchema
|
|
300
|
+
});
|
|
301
|
+
const DiagnosticSchema = object({
|
|
302
|
+
cached: optional(boolean()),
|
|
303
|
+
evidence: optional(custom(isJsonValue)),
|
|
304
|
+
filePath: string(),
|
|
305
|
+
loc: optional(DiagnosticLocationSchema),
|
|
306
|
+
message: string(),
|
|
307
|
+
model: optional(object({
|
|
308
|
+
providerId: string(),
|
|
309
|
+
requested: optional(string()),
|
|
310
|
+
resolvedId: string()
|
|
311
|
+
})),
|
|
312
|
+
ruleId: string(),
|
|
313
|
+
severity: union([literal("error"), literal("warn")])
|
|
314
|
+
});
|
|
315
|
+
const UsageSchema = object({
|
|
316
|
+
filePath: optional(string()),
|
|
317
|
+
inputTokens: optional(FiniteNumberSchema),
|
|
318
|
+
metadata: optional(custom(isJsonValue)),
|
|
319
|
+
modelId: string(),
|
|
320
|
+
outputTokens: optional(FiniteNumberSchema),
|
|
321
|
+
providerId: string(),
|
|
322
|
+
ruleId: string(),
|
|
323
|
+
totalTokens: optional(FiniteNumberSchema)
|
|
324
|
+
});
|
|
325
|
+
const CacheEntrySchema = object({
|
|
326
|
+
diagnostics: array(DiagnosticSchema),
|
|
327
|
+
fingerprint: object({
|
|
328
|
+
configHash: string(),
|
|
329
|
+
modelHash: string(),
|
|
330
|
+
ruleHash: string(),
|
|
331
|
+
targetHash: string()
|
|
332
|
+
}),
|
|
333
|
+
target: object({
|
|
334
|
+
hash: string(),
|
|
335
|
+
identity: string(),
|
|
336
|
+
kind: string(),
|
|
337
|
+
loc: optional(SourceLocationSchema),
|
|
338
|
+
name: optional(string()),
|
|
339
|
+
range: optional(SourceRangeSchema)
|
|
340
|
+
}),
|
|
341
|
+
usage: array(UsageSchema)
|
|
342
|
+
});
|
|
343
|
+
const CachedOwnerSchema = object({
|
|
344
|
+
contentHash: optional(string()),
|
|
345
|
+
kind: union([literal("file"), literal("project")]),
|
|
346
|
+
path: string(),
|
|
347
|
+
slots: array(string())
|
|
348
|
+
});
|
|
349
|
+
function objectRecord(value) {
|
|
350
|
+
return pipe(unknown(), check((input) => typeof input === "object" && input !== null && !Array.isArray(input)), record(string(), value));
|
|
351
|
+
}
|
|
352
|
+
const CacheFileBodySchema = pipe(object({
|
|
353
|
+
createdAt: string(),
|
|
354
|
+
entries: objectRecord(CacheEntrySchema),
|
|
355
|
+
owners: objectRecord(CachedOwnerSchema),
|
|
356
|
+
updatedAt: string()
|
|
357
|
+
}), check((body) => {
|
|
358
|
+
const entryKeys = Object.keys(body.entries);
|
|
359
|
+
const referencedSlots = Object.values(body.owners).flatMap((owner) => owner.slots);
|
|
360
|
+
const uniqueSlots = new Set(referencedSlots);
|
|
361
|
+
return uniqueSlots.size === referencedSlots.length && uniqueSlots.size === entryKeys.length && entryKeys.every((key) => uniqueSlots.has(key));
|
|
362
|
+
}));
|
|
363
|
+
const CacheMetadataSchema = object({
|
|
364
|
+
alintVersion: string(),
|
|
365
|
+
createdAt: string(),
|
|
366
|
+
magic: literal(CACHE_MAGIC),
|
|
367
|
+
schemaVersion: literal(2),
|
|
368
|
+
type: literal("metadata")
|
|
369
|
+
});
|
|
370
|
+
const PutEventSchema = object({
|
|
371
|
+
at: string(),
|
|
372
|
+
entry: CacheEntrySchema,
|
|
373
|
+
owner: CachedOwnerSchema,
|
|
374
|
+
ownerKey: string(),
|
|
375
|
+
slotKey: string(),
|
|
376
|
+
type: literal("put")
|
|
377
|
+
});
|
|
378
|
+
const ReplaceOwnerEventSchema = object({
|
|
379
|
+
at: string(),
|
|
380
|
+
entries: array(object({
|
|
381
|
+
entry: CacheEntrySchema,
|
|
382
|
+
slotKey: string()
|
|
383
|
+
})),
|
|
384
|
+
mode: union([literal("merge"), literal("replace")]),
|
|
385
|
+
owner: CachedOwnerSchema,
|
|
386
|
+
ownerKey: string(),
|
|
387
|
+
type: literal("replace-owner")
|
|
388
|
+
});
|
|
389
|
+
const RemoveOwnerEventSchema = object({
|
|
390
|
+
at: string(),
|
|
391
|
+
ownerKey: string(),
|
|
392
|
+
type: literal("remove-owner")
|
|
393
|
+
});
|
|
394
|
+
const CacheEventSchema = union([
|
|
395
|
+
PutEventSchema,
|
|
396
|
+
ReplaceOwnerEventSchema,
|
|
397
|
+
RemoveOwnerEventSchema
|
|
398
|
+
]);
|
|
399
|
+
async function createCacheStore(options) {
|
|
400
|
+
const location = resolveCacheLocation(options.cwd, options.location);
|
|
401
|
+
if (!options.enabled) return createNoopCacheStore(location);
|
|
402
|
+
const alintVersion = options.alintVersion ?? version;
|
|
403
|
+
const initial = await loadCacheLog(location, alintVersion);
|
|
404
|
+
if (options.readOnly) return createReadOnlyCacheStore(location, initial.body, options.cwd);
|
|
405
|
+
const dependencies = {
|
|
406
|
+
append: options.appendFile ?? appendFile,
|
|
407
|
+
lock: options.lock ?? { acquire: lockfile.lock },
|
|
408
|
+
write: options.writeFile ?? writeFile
|
|
409
|
+
};
|
|
410
|
+
const trimmed = await trimOnce(location, alintVersion, initial, dependencies);
|
|
411
|
+
const writer = createEventWriter(location, trimmed.metadata, dependencies);
|
|
412
|
+
const fileExists = options.fileExists ?? defaultFileExists;
|
|
286
413
|
return {
|
|
287
|
-
|
|
288
|
-
|
|
414
|
+
beginOwner: (owner, metadata) => beginOwner(trimmed.body, owner, options.cwd, writer, metadata),
|
|
415
|
+
flush: () => writer.flush(false),
|
|
416
|
+
location,
|
|
417
|
+
reconcile: async () => {
|
|
418
|
+
const removedOwners = await collectMissingFileOwners(trimmed.body, options.cwd, fileExists);
|
|
419
|
+
for (const ownerKey of removedOwners) writer.enqueue({
|
|
420
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
421
|
+
ownerKey,
|
|
422
|
+
type: "remove-owner"
|
|
423
|
+
});
|
|
424
|
+
await writer.flush(true);
|
|
425
|
+
}
|
|
289
426
|
};
|
|
290
427
|
}
|
|
291
|
-
function
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
for (const
|
|
295
|
-
const
|
|
296
|
-
|
|
428
|
+
function createTargetIdentityResolver(targets) {
|
|
429
|
+
const baseCounts = /* @__PURE__ */ new Map();
|
|
430
|
+
const duplicateCandidateCounts = /* @__PURE__ */ new Map();
|
|
431
|
+
for (const target of targets) {
|
|
432
|
+
const base = createBaseTargetIdentity(target);
|
|
433
|
+
baseCounts.set(base, (baseCounts.get(base) ?? 0) + 1);
|
|
297
434
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const textLanguage = {
|
|
304
|
-
extensions: [],
|
|
305
|
-
extract: (file) => [{
|
|
306
|
-
file,
|
|
307
|
-
identity: "file",
|
|
308
|
-
kind: "file",
|
|
309
|
-
language: "plaintext",
|
|
310
|
-
origin: { physicalPath: file.path },
|
|
311
|
-
text: file.text
|
|
312
|
-
}],
|
|
313
|
-
name: "plaintext"
|
|
314
|
-
};
|
|
315
|
-
//#endregion
|
|
316
|
-
//#region src/core/languages/ts/index.ts
|
|
317
|
-
const typescriptLanguage = {
|
|
318
|
-
extensions: [
|
|
319
|
-
".cts",
|
|
320
|
-
".mts",
|
|
321
|
-
".ts",
|
|
322
|
-
".tsx"
|
|
323
|
-
],
|
|
324
|
-
extract: (file) => extractJsSourceTargets(withLanguage(file, "typescript")),
|
|
325
|
-
name: "typescript"
|
|
326
|
-
};
|
|
327
|
-
//#endregion
|
|
328
|
-
//#region src/core/languages/resolve.ts
|
|
329
|
-
function resolveLanguage(file, registry, options) {
|
|
330
|
-
return resolveLanguageForPath(file.path, registry, options);
|
|
331
|
-
}
|
|
332
|
-
function resolveLanguageForPath(filePath, registry, options) {
|
|
333
|
-
const languageName = options.language ?? options.processedLanguage ?? registry.byExtension.get(extname$1(filePath)) ?? "plaintext";
|
|
334
|
-
const language = registry.languages.get(languageName);
|
|
335
|
-
if (!language) {
|
|
336
|
-
const registered = [...registry.languages.keys()].sort().join(", ");
|
|
337
|
-
throw new Error(`Unknown language "${languageName}". Languages come from plugins, and these are registered: ${registered}.`);
|
|
435
|
+
for (const target of targets) {
|
|
436
|
+
const base = createBaseTargetIdentity(target);
|
|
437
|
+
if ((baseCounts.get(base) ?? 0) <= 1) continue;
|
|
438
|
+
const candidate = target.range ? `${base}:${target.range.start}:${target.range.end}` : base;
|
|
439
|
+
duplicateCandidateCounts.set(candidate, (duplicateCandidateCounts.get(candidate) ?? 0) + 1);
|
|
338
440
|
}
|
|
339
|
-
return
|
|
441
|
+
return (target, targetIndex) => {
|
|
442
|
+
const base = createBaseTargetIdentity(target);
|
|
443
|
+
if ((baseCounts.get(base) ?? 0) <= 1) return base;
|
|
444
|
+
const candidate = target.range ? `${base}:${target.range.start}:${target.range.end}` : base;
|
|
445
|
+
if ((duplicateCandidateCounts.get(candidate) ?? 0) <= 1) return candidate;
|
|
446
|
+
return `${candidate}:${targetIndex}`;
|
|
447
|
+
};
|
|
340
448
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const registry = createLanguageRegistry$1();
|
|
345
|
-
registerLanguage(registry, textLanguage);
|
|
346
|
-
registerLanguage(registry, javascriptLanguage);
|
|
347
|
-
registerLanguage(registry, typescriptLanguage);
|
|
348
|
-
return registry;
|
|
449
|
+
function normalizeCachePath(cwd, filePath) {
|
|
450
|
+
const resolvedPath = isAbsolute$1(filePath) ? resolve$1(filePath) : resolve$1(cwd, filePath);
|
|
451
|
+
return relative$1(cwd, resolvedPath).split(sep).join("/") || ".";
|
|
349
452
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
rules.set(id, rule);
|
|
360
|
-
localIds.set(id, localId);
|
|
361
|
-
}
|
|
362
|
-
for (const [id, entry] of Object.entries(config.rules)) {
|
|
363
|
-
const rule = rules.get(id);
|
|
364
|
-
if (!rule) throw new Error(`Unknown rule "${id}".`);
|
|
365
|
-
const normalizedEntry = normalizeRuleConfigEntry(entry);
|
|
366
|
-
const severity = normalizedEntry.severity;
|
|
367
|
-
if (severity === "off") continue;
|
|
368
|
-
enabledRules.push({
|
|
369
|
-
id,
|
|
370
|
-
localId: localIds.get(id) ?? id,
|
|
371
|
-
options: parseRuleOptions(id, rule.options, normalizedEntry.options),
|
|
372
|
-
rule,
|
|
373
|
-
severity
|
|
374
|
-
});
|
|
375
|
-
}
|
|
453
|
+
function normalizeRunnerCacheConfig(cache, cwd) {
|
|
454
|
+
if (cache === false) return {
|
|
455
|
+
enabled: false,
|
|
456
|
+
location: resolveCacheLocation(cwd)
|
|
457
|
+
};
|
|
458
|
+
if (cache === true || cache === void 0) return {
|
|
459
|
+
enabled: true,
|
|
460
|
+
location: resolveCacheLocation(cwd)
|
|
461
|
+
};
|
|
376
462
|
return {
|
|
377
|
-
|
|
378
|
-
|
|
463
|
+
enabled: cache.enabled ?? true,
|
|
464
|
+
location: resolveCacheLocation(cwd, cache.location)
|
|
379
465
|
};
|
|
380
466
|
}
|
|
381
|
-
function
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
467
|
+
function resolveCacheLocation(cwd, location) {
|
|
468
|
+
if (!location) return join(cwd, DEFAULT_CACHE_FILE_NAME);
|
|
469
|
+
const resolved = isAbsolute$1(location) ? resolve$1(location) : resolve$1(cwd, location);
|
|
470
|
+
if (location.endsWith("/") || location.endsWith("\\")) return join(resolved, DEFAULT_CACHE_FILE_NAME);
|
|
471
|
+
try {
|
|
472
|
+
if (statSync(resolved).isDirectory()) return join(resolved, DEFAULT_CACHE_FILE_NAME);
|
|
473
|
+
} catch {}
|
|
474
|
+
return resolved;
|
|
386
475
|
}
|
|
387
|
-
function
|
|
388
|
-
|
|
476
|
+
async function appendBatch(location, payload, append) {
|
|
477
|
+
const handle = await open(location, "a+");
|
|
478
|
+
try {
|
|
479
|
+
const { size } = await handle.stat();
|
|
480
|
+
let separator = "";
|
|
481
|
+
if (size > 0) {
|
|
482
|
+
const finalByte = Buffer.alloc(1);
|
|
483
|
+
await handle.read(finalByte, 0, 1, size - 1);
|
|
484
|
+
separator = finalByte[0] === 10 ? "" : "\n";
|
|
485
|
+
}
|
|
486
|
+
await append(location, `${separator}${payload}`, "utf8");
|
|
487
|
+
} finally {
|
|
488
|
+
await handle.close();
|
|
489
|
+
}
|
|
389
490
|
}
|
|
390
|
-
function
|
|
391
|
-
if (
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
491
|
+
function applyEvent(body, event) {
|
|
492
|
+
if (event.type === "remove-owner") {
|
|
493
|
+
removeOwner(body, event.ownerKey);
|
|
494
|
+
body.updatedAt = event.at;
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (event.type === "put") {
|
|
498
|
+
const current = body.owners[event.ownerKey];
|
|
499
|
+
const slots = new Set(current?.slots ?? []);
|
|
500
|
+
slots.add(event.slotKey);
|
|
501
|
+
body.entries[event.slotKey] = event.entry;
|
|
502
|
+
body.owners[event.ownerKey] = {
|
|
503
|
+
...event.owner,
|
|
504
|
+
slots: [...slots].sort()
|
|
396
505
|
};
|
|
506
|
+
body.updatedAt = event.at;
|
|
507
|
+
return;
|
|
397
508
|
}
|
|
509
|
+
const entries = event.mode === "merge" ? ownerEntries(body, body.owners[event.ownerKey]) : /* @__PURE__ */ new Map();
|
|
510
|
+
for (const item of event.entries) entries.set(item.slotKey, item.entry);
|
|
511
|
+
for (const slot of body.owners[event.ownerKey]?.slots ?? []) delete body.entries[slot];
|
|
512
|
+
for (const [slotKey, entry] of entries) body.entries[slotKey] = entry;
|
|
513
|
+
body.owners[event.ownerKey] = {
|
|
514
|
+
...event.owner,
|
|
515
|
+
slots: [...entries.keys()].sort()
|
|
516
|
+
};
|
|
517
|
+
body.updatedAt = event.at;
|
|
518
|
+
}
|
|
519
|
+
function beginOwner(body, owner, cwd, writer, ownerMetadata = {}) {
|
|
520
|
+
const normalizedOwner = {
|
|
521
|
+
kind: owner.kind,
|
|
522
|
+
path: normalizeCachePath(cwd, owner.path)
|
|
523
|
+
};
|
|
524
|
+
const key = ownerKey(normalizedOwner, cwd);
|
|
525
|
+
const previousOwner = body.owners[key];
|
|
526
|
+
const nextEntries = /* @__PURE__ */ new Map();
|
|
527
|
+
const checkpointEntries = /* @__PURE__ */ new Map();
|
|
528
|
+
const commit = (metadata = {}) => {
|
|
529
|
+
const contentHash = metadata.contentHash ?? ownerMetadata.contentHash;
|
|
530
|
+
const committedEntries = metadata.mode === "merge" ? ownerEntries(body, body.owners[key]) : /* @__PURE__ */ new Map();
|
|
531
|
+
for (const [entryKey, cacheEntry] of nextEntries) committedEntries.set(entryKey, cacheEntry);
|
|
532
|
+
const replacedSlots = /* @__PURE__ */ new Set([...previousOwner?.slots ?? [], ...body.owners[key]?.slots ?? []]);
|
|
533
|
+
for (const replacedSlot of replacedSlots) delete body.entries[replacedSlot];
|
|
534
|
+
for (const [entryKey, cacheEntry] of committedEntries) body.entries[entryKey] = cacheEntry;
|
|
535
|
+
const cachedOwner = {
|
|
536
|
+
contentHash,
|
|
537
|
+
kind: normalizedOwner.kind,
|
|
538
|
+
path: normalizedOwner.path,
|
|
539
|
+
slots: [...committedEntries.keys()].sort()
|
|
540
|
+
};
|
|
541
|
+
body.owners[key] = cachedOwner;
|
|
542
|
+
body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
543
|
+
writer.enqueue({
|
|
544
|
+
at: body.updatedAt,
|
|
545
|
+
entries: [...committedEntries].map(([slotKey, entry]) => ({
|
|
546
|
+
entry,
|
|
547
|
+
slotKey
|
|
548
|
+
})),
|
|
549
|
+
mode: metadata.mode ?? "replace",
|
|
550
|
+
owner: cachedOwner,
|
|
551
|
+
ownerKey: key,
|
|
552
|
+
type: "replace-owner"
|
|
553
|
+
});
|
|
554
|
+
checkpointEntries.clear();
|
|
555
|
+
};
|
|
398
556
|
return {
|
|
399
|
-
|
|
400
|
-
|
|
557
|
+
checkpoint: async () => {
|
|
558
|
+
const cachedOwner = {
|
|
559
|
+
contentHash: ownerMetadata.contentHash,
|
|
560
|
+
kind: normalizedOwner.kind,
|
|
561
|
+
path: normalizedOwner.path,
|
|
562
|
+
slots: []
|
|
563
|
+
};
|
|
564
|
+
for (const [entryKey, cacheEntry] of checkpointEntries) {
|
|
565
|
+
const event = {
|
|
566
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
567
|
+
entry: cacheEntry,
|
|
568
|
+
owner: cachedOwner,
|
|
569
|
+
ownerKey: key,
|
|
570
|
+
slotKey: entryKey,
|
|
571
|
+
type: "put"
|
|
572
|
+
};
|
|
573
|
+
applyEvent(body, event);
|
|
574
|
+
writer.enqueue(event);
|
|
575
|
+
}
|
|
576
|
+
checkpointEntries.clear();
|
|
577
|
+
await writer.flush(false);
|
|
578
|
+
},
|
|
579
|
+
commit,
|
|
580
|
+
discard: (cacheSlot) => {
|
|
581
|
+
const entryKey = slotKey(normalizedOwner, cacheSlot, cwd);
|
|
582
|
+
nextEntries.delete(entryKey);
|
|
583
|
+
checkpointEntries.delete(entryKey);
|
|
584
|
+
},
|
|
585
|
+
lookup: (cacheSlot, fingerprint) => {
|
|
586
|
+
const entryKey = slotKey(normalizedOwner, cacheSlot, cwd);
|
|
587
|
+
const cached = body.entries[entryKey];
|
|
588
|
+
if (!cached || !fingerprintsEqual(cached.fingerprint, fingerprint)) return void 0;
|
|
589
|
+
nextEntries.set(entryKey, cached);
|
|
590
|
+
return cached;
|
|
591
|
+
},
|
|
592
|
+
put: (cacheSlot, cacheEntry) => {
|
|
593
|
+
const entryKey = slotKey(normalizedOwner, cacheSlot, cwd);
|
|
594
|
+
nextEntries.set(entryKey, cacheEntry);
|
|
595
|
+
checkpointEntries.set(entryKey, cacheEntry);
|
|
596
|
+
}
|
|
401
597
|
};
|
|
402
598
|
}
|
|
403
|
-
function
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
599
|
+
async function collectMissingFileOwners(body, cwd, fileExists) {
|
|
600
|
+
const removed = [];
|
|
601
|
+
for (const [key, owner] of Object.entries(body.owners)) {
|
|
602
|
+
if (owner.kind !== "file" || await fileExists(resolve$1(cwd, owner.path))) continue;
|
|
603
|
+
removeOwner(body, key);
|
|
604
|
+
removed.push(key);
|
|
409
605
|
}
|
|
410
|
-
|
|
606
|
+
return removed;
|
|
411
607
|
}
|
|
412
|
-
function
|
|
413
|
-
if (
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
return schema.map((optionSchema, index) => parseRuleOption(id, index, optionSchema, options[index]));
|
|
608
|
+
function createBaseTargetIdentity(target) {
|
|
609
|
+
if (target.identity && (target.kind !== "file" || target.identity !== "file")) return target.filePath ? `${target.kind}:${target.filePath}:${target.identity}` : `${target.kind}:${target.identity}`;
|
|
610
|
+
if (target.kind === "file") return target.filePath ? `file:${target.filePath}` : "file";
|
|
611
|
+
if (target.name) return `${target.kind}:${target.name}`;
|
|
612
|
+
if (target.range) return `${target.kind}:${target.range.start}:${target.range.end}`;
|
|
613
|
+
return target.kind;
|
|
419
614
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
* One diagnostic per missing language, always an error: a rule scoped to a language nothing provides
|
|
428
|
-
* matches no file, so leaving it at a warning would let a run pass having silently checked nothing.
|
|
429
|
-
*/
|
|
430
|
-
function missingLanguageDiagnostics(missing) {
|
|
431
|
-
return [...missing.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([languageId, entry]) => {
|
|
432
|
-
const ruleIds = [...entry.ruleIds].sort();
|
|
433
|
-
const subject = ruleIds.length === 1 ? `Rule "${ruleIds[0]}" needs` : `Rules ${ruleIds.map((id) => `"${id}"`).join(", ")} need`;
|
|
434
|
-
return {
|
|
435
|
-
filePath: entry.filePath,
|
|
436
|
-
message: `${subject} the "${languageId}" language, which no plugin registered. Add a plugin that provides "${languageId}", or turn the rule off.`,
|
|
437
|
-
ruleId: MISSING_LANGUAGE_RULE,
|
|
438
|
-
severity: "error"
|
|
439
|
-
};
|
|
440
|
-
});
|
|
615
|
+
function createEmptyCacheBody(createdAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
616
|
+
return {
|
|
617
|
+
createdAt,
|
|
618
|
+
entries: {},
|
|
619
|
+
owners: {},
|
|
620
|
+
updatedAt: createdAt
|
|
621
|
+
};
|
|
441
622
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
623
|
+
function createEventWriter(location, metadata, dependencies) {
|
|
624
|
+
const backlog = [];
|
|
625
|
+
let validationError;
|
|
626
|
+
let previousWrite = Promise.resolve();
|
|
627
|
+
const persistBacklog = async () => {
|
|
628
|
+
if (validationError) throw validationError;
|
|
629
|
+
if (backlog.length === 0) return;
|
|
630
|
+
await mkdir(dirname(location), { recursive: true });
|
|
631
|
+
await withCacheLock(location, dependencies.lock, getCacheLockOptions({
|
|
632
|
+
factor: 2,
|
|
633
|
+
maxTimeout: 16e3,
|
|
634
|
+
minTimeout: 2e3,
|
|
635
|
+
retries: 4
|
|
636
|
+
}), async () => {
|
|
637
|
+
await ensureCompatibleMetadata(location, metadata, dependencies.write);
|
|
638
|
+
const batchSize = backlog.length;
|
|
639
|
+
await appendBatch(location, backlog.slice(0, batchSize).map((event) => `${JSON.stringify(event)}\n`).join(""), dependencies.append);
|
|
640
|
+
backlog.splice(0, batchSize);
|
|
449
641
|
});
|
|
450
|
-
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
severity: existing?.severity === "error" ? "error" : severity
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
/** One diagnostic per extension, sorted, for the run to prepend to its results. */
|
|
470
|
-
function unregisteredLanguageDiagnostics(unregistered) {
|
|
471
|
-
return [...unregistered.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([extension, entry]) => ({
|
|
472
|
-
filePath: entry.filePath,
|
|
473
|
-
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.`,
|
|
474
|
-
ruleId: UNREGISTERED_LANGUAGE_RULE,
|
|
475
|
-
severity: entry.severity
|
|
476
|
-
}));
|
|
477
|
-
}
|
|
478
|
-
function unregisteredLanguageSeverity(linterOptions) {
|
|
479
|
-
const value = linterOptions.reportUnregisteredLanguages;
|
|
480
|
-
return value === "error" || value === "off" ? value : "warn";
|
|
481
|
-
}
|
|
482
|
-
//#endregion
|
|
483
|
-
//#region src/core/languages/rule-languages.ts
|
|
484
|
-
/**
|
|
485
|
-
* The language a file falls back to when no registered language claims its extension. Not a real
|
|
486
|
-
* language, which is why `'any'` excludes it.
|
|
487
|
-
*/
|
|
488
|
-
const PLAIN_TEXT = "plaintext";
|
|
489
|
-
/**
|
|
490
|
-
* Whether a rule reads this target.
|
|
491
|
-
*
|
|
492
|
-
* A rule with language off still reads file targets, because reading raw text, or asking a model about a
|
|
493
|
-
* whole file, needs no language. However, it never reads the function and class targets extraction produces.
|
|
494
|
-
* `'file'` is the one kind every language yields, plain text included, so it is the dividing line.
|
|
495
|
-
*
|
|
496
|
-
* `'any'` means any real language, so a file that fell back to plain text because nothing claimed
|
|
497
|
-
* its extension is withheld: the rule asked for structure and there is none to give. The run says so
|
|
498
|
-
* once per extension through `alint/unregistered-language` rather than letting each rule report an
|
|
499
|
-
* empty file.
|
|
500
|
-
*/
|
|
501
|
-
function isTargetLanguageAccepted(languages, targetKind, targetLanguage) {
|
|
502
|
-
if (languages.kind === "off") return targetKind === "file";
|
|
503
|
-
if (languages.kind === "any") return targetLanguage !== PLAIN_TEXT;
|
|
504
|
-
return languages.ids.includes(targetLanguage);
|
|
642
|
+
validationError = void 0;
|
|
643
|
+
};
|
|
644
|
+
return {
|
|
645
|
+
enqueue: (event) => {
|
|
646
|
+
try {
|
|
647
|
+
backlog.push(parse(CacheEventSchema, event));
|
|
648
|
+
} catch (error) {
|
|
649
|
+
validationError ??= error;
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
flush: (finalAttempt) => {
|
|
653
|
+
const write = previousWrite.then(persistBacklog);
|
|
654
|
+
previousWrite = write.catch(() => {});
|
|
655
|
+
return finalAttempt ? write : write.catch(() => {});
|
|
656
|
+
}
|
|
657
|
+
};
|
|
505
658
|
}
|
|
506
|
-
function
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
659
|
+
function createNoopCacheStore(location) {
|
|
660
|
+
const transaction = {
|
|
661
|
+
checkpoint: async () => {},
|
|
662
|
+
commit: () => {},
|
|
663
|
+
discard: () => {},
|
|
664
|
+
lookup: () => void 0,
|
|
665
|
+
put: () => {}
|
|
513
666
|
};
|
|
514
667
|
return {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
668
|
+
beginOwner: () => transaction,
|
|
669
|
+
flush: async () => {},
|
|
670
|
+
location,
|
|
671
|
+
reconcile: async () => {}
|
|
518
672
|
};
|
|
519
673
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
return async (filePath, options = {}) => {
|
|
536
|
-
const path = resolve(cwd, filePath);
|
|
537
|
-
const resolvedConfig = resolveConfigForFile(path, config, { cwd });
|
|
538
|
-
if (resolvedConfig.ignored) return [];
|
|
539
|
-
const effectiveConfig = resolvedConfig.config;
|
|
540
|
-
const language = resolveLanguageForPath(path, createLanguageRegistry(effectiveConfig), { language: options.language ?? effectiveConfig.language });
|
|
541
|
-
const src = getSrc();
|
|
542
|
-
const file = await src.readFile(path);
|
|
543
|
-
return language.extract(file, {
|
|
544
|
-
cwd,
|
|
545
|
-
languageOptions: effectiveConfig.languageOptions,
|
|
546
|
-
src
|
|
547
|
-
});
|
|
674
|
+
function createReadOnlyCacheStore(location, body, cwd) {
|
|
675
|
+
return {
|
|
676
|
+
beginOwner: (owner) => ({
|
|
677
|
+
checkpoint: async () => {},
|
|
678
|
+
commit: () => {},
|
|
679
|
+
discard: () => {},
|
|
680
|
+
lookup: (slot, fingerprint) => {
|
|
681
|
+
const cached = body.entries[slotKey(owner, slot, cwd)];
|
|
682
|
+
return cached && fingerprintsEqual(cached.fingerprint, fingerprint) ? cached : void 0;
|
|
683
|
+
},
|
|
684
|
+
put: () => {}
|
|
685
|
+
}),
|
|
686
|
+
flush: async () => {},
|
|
687
|
+
location,
|
|
688
|
+
reconcile: async () => {}
|
|
548
689
|
};
|
|
549
690
|
}
|
|
550
|
-
function
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
for (const filePath of options.files ?? []) {
|
|
558
|
-
const path = resolve(cwd$2, filePath);
|
|
559
|
-
const resolvedConfig = resolveConfigForFile(path, config, { cwd: cwd$2 });
|
|
560
|
-
if (resolvedConfig.ignored) continue;
|
|
561
|
-
const effectiveConfig = resolvedConfig.config;
|
|
562
|
-
const languageRegistry = createLanguageRegistry(effectiveConfig);
|
|
563
|
-
const language = resolveLanguageForPath(path, languageRegistry, { language: effectiveConfig.language });
|
|
564
|
-
const rules = prepareRules(effectiveConfig);
|
|
565
|
-
recordMissingLanguages(missingLanguages, rules, languageRegistry, path);
|
|
566
|
-
if (effectiveConfig.language === void 0 && language.name === "plaintext" && rules.some(({ enabledRule }) => !isTargetLanguageAccepted(resolveRuleLanguages(enabledRule.rule.languages), "file", "plaintext"))) recordUnregistered(unregisteredLanguages, path, unregisteredLanguageSeverity(effectiveConfig.linterOptions));
|
|
567
|
-
files.push({
|
|
568
|
-
agent: effectiveConfig.agent,
|
|
569
|
-
configHash: stableHash({
|
|
570
|
-
language: effectiveConfig.language,
|
|
571
|
-
languageOptions: effectiveConfig.languageOptions,
|
|
572
|
-
processor: effectiveConfig.processor,
|
|
573
|
-
resolvedLanguage: language.name,
|
|
574
|
-
settings: effectiveConfig.settings
|
|
575
|
-
}),
|
|
576
|
-
fileIndex: files.length,
|
|
577
|
-
language,
|
|
578
|
-
languageOptions: effectiveConfig.languageOptions,
|
|
579
|
-
path,
|
|
580
|
-
rules,
|
|
581
|
-
settings: effectiveConfig.settings
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
for (const directoryPath of options.directories ?? []) {
|
|
585
|
-
const path = resolve(cwd$2, directoryPath);
|
|
586
|
-
const resolvedConfig = resolveConfigForDirectory(path, config, { cwd: cwd$2 });
|
|
587
|
-
if (resolvedConfig.ignored) continue;
|
|
588
|
-
const effectiveConfig = resolvedConfig.config;
|
|
589
|
-
directories.push({
|
|
590
|
-
agent: effectiveConfig.agent,
|
|
591
|
-
configHash: stableHash({ settings: effectiveConfig.settings }),
|
|
592
|
-
directoryIndex: directories.length,
|
|
593
|
-
rules: prepareRules(effectiveConfig),
|
|
594
|
-
settings: effectiveConfig.settings,
|
|
595
|
-
target: {
|
|
596
|
-
kind: "directory",
|
|
597
|
-
path
|
|
598
|
-
}
|
|
599
|
-
});
|
|
691
|
+
async function defaultFileExists(path) {
|
|
692
|
+
try {
|
|
693
|
+
await access(path);
|
|
694
|
+
return true;
|
|
695
|
+
} catch (error) {
|
|
696
|
+
if (isMissingFileError(error)) return false;
|
|
697
|
+
throw error;
|
|
600
698
|
}
|
|
699
|
+
}
|
|
700
|
+
function emptyLoadedLog(alintVersion) {
|
|
701
|
+
const metadata = {
|
|
702
|
+
alintVersion,
|
|
703
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
704
|
+
magic: CACHE_MAGIC,
|
|
705
|
+
schemaVersion: 2,
|
|
706
|
+
type: "metadata"
|
|
707
|
+
};
|
|
601
708
|
return {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
missingLanguages,
|
|
605
|
-
project: options.projectTargets === false ? void 0 : prepareProject(cwd$2, config),
|
|
606
|
-
unregisteredLanguages
|
|
709
|
+
body: createEmptyCacheBody(metadata.createdAt),
|
|
710
|
+
metadata
|
|
607
711
|
};
|
|
608
712
|
}
|
|
609
|
-
function
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
713
|
+
async function ensureCompatibleMetadata(location, metadata, write) {
|
|
714
|
+
try {
|
|
715
|
+
if (parseMetadataLine(await readMetadataLine(location), metadata.alintVersion)) return;
|
|
716
|
+
} catch (error) {
|
|
717
|
+
if (!isMissingFileError(error)) throw error;
|
|
718
|
+
}
|
|
719
|
+
await write(location, `${JSON.stringify(metadata)}\n`);
|
|
613
720
|
}
|
|
614
|
-
function
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
721
|
+
function fingerprintsEqual(left, right) {
|
|
722
|
+
return left.configHash === right.configHash && left.modelHash === right.modelHash && left.ruleHash === right.ruleHash && left.targetHash === right.targetHash;
|
|
723
|
+
}
|
|
724
|
+
function getCacheLockOptions(retries) {
|
|
618
725
|
return {
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
settings: effectiveConfig.settings
|
|
726
|
+
realpath: false,
|
|
727
|
+
retries,
|
|
728
|
+
stale: LOCK_STALE_MS,
|
|
729
|
+
update: LOCK_UPDATE_MS
|
|
624
730
|
};
|
|
625
731
|
}
|
|
626
|
-
function
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
var version = "0.6.0";
|
|
642
|
-
//#endregion
|
|
643
|
-
//#region src/core/cache/types.ts
|
|
644
|
-
const CACHE_MAGIC = "ALINT_CACHE";
|
|
645
|
-
//#endregion
|
|
646
|
-
//#region src/core/cache/store.ts
|
|
647
|
-
const DEFAULT_CACHE_FILE_NAME = ".alintcache";
|
|
648
|
-
const LOCK_STALE_MS = 2e4;
|
|
649
|
-
const LOCK_UPDATE_MS = 3e3;
|
|
650
|
-
const METADATA_READ_LIMIT = 4096;
|
|
651
|
-
const FiniteNumberSchema = pipe(number(), finite());
|
|
652
|
-
const PositionSchema = object({
|
|
653
|
-
column: FiniteNumberSchema,
|
|
654
|
-
line: FiniteNumberSchema
|
|
655
|
-
});
|
|
656
|
-
const SourceLocationSchema = object({
|
|
657
|
-
end: PositionSchema,
|
|
658
|
-
start: PositionSchema
|
|
659
|
-
});
|
|
660
|
-
const DiagnosticLocationSchema = object({
|
|
661
|
-
end: optional(PositionSchema),
|
|
662
|
-
start: PositionSchema
|
|
663
|
-
});
|
|
664
|
-
const SourceRangeSchema = object({
|
|
665
|
-
end: FiniteNumberSchema,
|
|
666
|
-
start: FiniteNumberSchema
|
|
667
|
-
});
|
|
668
|
-
const DiagnosticSchema = object({
|
|
669
|
-
cached: optional(boolean()),
|
|
670
|
-
evidence: optional(custom(isJsonValue)),
|
|
671
|
-
filePath: string(),
|
|
672
|
-
loc: optional(DiagnosticLocationSchema),
|
|
673
|
-
message: string(),
|
|
674
|
-
model: optional(object({
|
|
675
|
-
providerId: string(),
|
|
676
|
-
requested: optional(string()),
|
|
677
|
-
resolvedId: string()
|
|
678
|
-
})),
|
|
679
|
-
ruleId: string(),
|
|
680
|
-
severity: union([literal("error"), literal("warn")])
|
|
681
|
-
});
|
|
682
|
-
const UsageSchema = object({
|
|
683
|
-
filePath: optional(string()),
|
|
684
|
-
inputTokens: optional(FiniteNumberSchema),
|
|
685
|
-
metadata: optional(custom(isJsonValue)),
|
|
686
|
-
modelId: string(),
|
|
687
|
-
outputTokens: optional(FiniteNumberSchema),
|
|
688
|
-
providerId: string(),
|
|
689
|
-
ruleId: string(),
|
|
690
|
-
totalTokens: optional(FiniteNumberSchema)
|
|
691
|
-
});
|
|
692
|
-
const CacheEntrySchema = object({
|
|
693
|
-
diagnostics: array(DiagnosticSchema),
|
|
694
|
-
fingerprint: object({
|
|
695
|
-
configHash: string(),
|
|
696
|
-
modelHash: string(),
|
|
697
|
-
ruleHash: string(),
|
|
698
|
-
targetHash: string()
|
|
699
|
-
}),
|
|
700
|
-
target: object({
|
|
701
|
-
hash: string(),
|
|
702
|
-
identity: string(),
|
|
703
|
-
kind: string(),
|
|
704
|
-
loc: optional(SourceLocationSchema),
|
|
705
|
-
name: optional(string()),
|
|
706
|
-
range: optional(SourceRangeSchema)
|
|
707
|
-
}),
|
|
708
|
-
usage: array(UsageSchema)
|
|
709
|
-
});
|
|
710
|
-
const CachedOwnerSchema = object({
|
|
711
|
-
contentHash: optional(string()),
|
|
712
|
-
kind: union([literal("file"), literal("project")]),
|
|
713
|
-
path: string(),
|
|
714
|
-
slots: array(string())
|
|
715
|
-
});
|
|
716
|
-
function objectRecord(value) {
|
|
717
|
-
return pipe(unknown(), check((input) => typeof input === "object" && input !== null && !Array.isArray(input)), record(string(), value));
|
|
718
|
-
}
|
|
719
|
-
const CacheFileBodySchema = pipe(object({
|
|
720
|
-
createdAt: string(),
|
|
721
|
-
entries: objectRecord(CacheEntrySchema),
|
|
722
|
-
owners: objectRecord(CachedOwnerSchema),
|
|
723
|
-
updatedAt: string()
|
|
724
|
-
}), check((body) => {
|
|
725
|
-
const entryKeys = Object.keys(body.entries);
|
|
726
|
-
const referencedSlots = Object.values(body.owners).flatMap((owner) => owner.slots);
|
|
727
|
-
const uniqueSlots = new Set(referencedSlots);
|
|
728
|
-
return uniqueSlots.size === referencedSlots.length && uniqueSlots.size === entryKeys.length && entryKeys.every((key) => uniqueSlots.has(key));
|
|
729
|
-
}));
|
|
730
|
-
const CacheMetadataSchema = object({
|
|
731
|
-
alintVersion: string(),
|
|
732
|
-
createdAt: string(),
|
|
733
|
-
magic: literal(CACHE_MAGIC),
|
|
734
|
-
schemaVersion: literal(2),
|
|
735
|
-
type: literal("metadata")
|
|
736
|
-
});
|
|
737
|
-
const PutEventSchema = object({
|
|
738
|
-
at: string(),
|
|
739
|
-
entry: CacheEntrySchema,
|
|
740
|
-
owner: CachedOwnerSchema,
|
|
741
|
-
ownerKey: string(),
|
|
742
|
-
slotKey: string(),
|
|
743
|
-
type: literal("put")
|
|
744
|
-
});
|
|
745
|
-
const ReplaceOwnerEventSchema = object({
|
|
746
|
-
at: string(),
|
|
747
|
-
entries: array(object({
|
|
748
|
-
entry: CacheEntrySchema,
|
|
749
|
-
slotKey: string()
|
|
750
|
-
})),
|
|
751
|
-
mode: union([literal("merge"), literal("replace")]),
|
|
752
|
-
owner: CachedOwnerSchema,
|
|
753
|
-
ownerKey: string(),
|
|
754
|
-
type: literal("replace-owner")
|
|
755
|
-
});
|
|
756
|
-
const RemoveOwnerEventSchema = object({
|
|
757
|
-
at: string(),
|
|
758
|
-
ownerKey: string(),
|
|
759
|
-
type: literal("remove-owner")
|
|
760
|
-
});
|
|
761
|
-
const CacheEventSchema = union([
|
|
762
|
-
PutEventSchema,
|
|
763
|
-
ReplaceOwnerEventSchema,
|
|
764
|
-
RemoveOwnerEventSchema
|
|
765
|
-
]);
|
|
766
|
-
async function createCacheStore(options) {
|
|
767
|
-
const location = resolveCacheLocation(options.cwd, options.location);
|
|
768
|
-
if (!options.enabled) return createNoopCacheStore(location);
|
|
769
|
-
const alintVersion = options.alintVersion ?? version;
|
|
770
|
-
const initial = await loadCacheLog(location, alintVersion);
|
|
771
|
-
if (options.readOnly) return createReadOnlyCacheStore(location, initial.body, options.cwd);
|
|
772
|
-
const dependencies = {
|
|
773
|
-
append: options.appendFile ?? appendFile,
|
|
774
|
-
lock: options.lock ?? { acquire: lockfile.lock },
|
|
775
|
-
write: options.writeFile ?? writeFile
|
|
776
|
-
};
|
|
777
|
-
const trimmed = await trimOnce(location, alintVersion, initial, dependencies);
|
|
778
|
-
const writer = createEventWriter(location, trimmed.metadata, dependencies);
|
|
779
|
-
const fileExists = options.fileExists ?? defaultFileExists;
|
|
780
|
-
return {
|
|
781
|
-
beginOwner: (owner, metadata) => beginOwner(trimmed.body, owner, options.cwd, writer, metadata),
|
|
782
|
-
flush: () => writer.flush(false),
|
|
783
|
-
location,
|
|
784
|
-
reconcile: async () => {
|
|
785
|
-
const removedOwners = await collectMissingFileOwners(trimmed.body, options.cwd, fileExists);
|
|
786
|
-
for (const ownerKey of removedOwners) writer.enqueue({
|
|
787
|
-
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
788
|
-
ownerKey,
|
|
789
|
-
type: "remove-owner"
|
|
790
|
-
});
|
|
791
|
-
await writer.flush(true);
|
|
792
|
-
}
|
|
793
|
-
};
|
|
794
|
-
}
|
|
795
|
-
function createTargetIdentityResolver(targets) {
|
|
796
|
-
const baseCounts = /* @__PURE__ */ new Map();
|
|
797
|
-
const duplicateCandidateCounts = /* @__PURE__ */ new Map();
|
|
798
|
-
for (const target of targets) {
|
|
799
|
-
const base = createBaseTargetIdentity(target);
|
|
800
|
-
baseCounts.set(base, (baseCounts.get(base) ?? 0) + 1);
|
|
801
|
-
}
|
|
802
|
-
for (const target of targets) {
|
|
803
|
-
const base = createBaseTargetIdentity(target);
|
|
804
|
-
if ((baseCounts.get(base) ?? 0) <= 1) continue;
|
|
805
|
-
const candidate = target.range ? `${base}:${target.range.start}:${target.range.end}` : base;
|
|
806
|
-
duplicateCandidateCounts.set(candidate, (duplicateCandidateCounts.get(candidate) ?? 0) + 1);
|
|
807
|
-
}
|
|
808
|
-
return (target, targetIndex) => {
|
|
809
|
-
const base = createBaseTargetIdentity(target);
|
|
810
|
-
if ((baseCounts.get(base) ?? 0) <= 1) return base;
|
|
811
|
-
const candidate = target.range ? `${base}:${target.range.start}:${target.range.end}` : base;
|
|
812
|
-
if ((duplicateCandidateCounts.get(candidate) ?? 0) <= 1) return candidate;
|
|
813
|
-
return `${candidate}:${targetIndex}`;
|
|
814
|
-
};
|
|
815
|
-
}
|
|
816
|
-
function normalizeCachePath(cwd, filePath) {
|
|
817
|
-
const resolvedPath = isAbsolute$1(filePath) ? resolve$1(filePath) : resolve$1(cwd, filePath);
|
|
818
|
-
return relative$1(cwd, resolvedPath).split(sep).join("/") || ".";
|
|
819
|
-
}
|
|
820
|
-
function normalizeRunnerCacheConfig(cache, cwd) {
|
|
821
|
-
if (cache === false) return {
|
|
822
|
-
enabled: false,
|
|
823
|
-
location: resolveCacheLocation(cwd)
|
|
824
|
-
};
|
|
825
|
-
if (cache === true || cache === void 0) return {
|
|
826
|
-
enabled: true,
|
|
827
|
-
location: resolveCacheLocation(cwd)
|
|
828
|
-
};
|
|
829
|
-
return {
|
|
830
|
-
enabled: cache.enabled ?? true,
|
|
831
|
-
location: resolveCacheLocation(cwd, cache.location)
|
|
832
|
-
};
|
|
833
|
-
}
|
|
834
|
-
function resolveCacheLocation(cwd, location) {
|
|
835
|
-
if (!location) return join(cwd, DEFAULT_CACHE_FILE_NAME);
|
|
836
|
-
const resolved = isAbsolute$1(location) ? resolve$1(location) : resolve$1(cwd, location);
|
|
837
|
-
if (location.endsWith("/") || location.endsWith("\\")) return join(resolved, DEFAULT_CACHE_FILE_NAME);
|
|
838
|
-
try {
|
|
839
|
-
if (statSync(resolved).isDirectory()) return join(resolved, DEFAULT_CACHE_FILE_NAME);
|
|
840
|
-
} catch {}
|
|
841
|
-
return resolved;
|
|
842
|
-
}
|
|
843
|
-
async function appendBatch(location, payload, append) {
|
|
844
|
-
const handle = await open(location, "a+");
|
|
845
|
-
try {
|
|
846
|
-
const { size } = await handle.stat();
|
|
847
|
-
let separator = "";
|
|
848
|
-
if (size > 0) {
|
|
849
|
-
const finalByte = Buffer.alloc(1);
|
|
850
|
-
await handle.read(finalByte, 0, 1, size - 1);
|
|
851
|
-
separator = finalByte[0] === 10 ? "" : "\n";
|
|
852
|
-
}
|
|
853
|
-
await append(location, `${separator}${payload}`, "utf8");
|
|
854
|
-
} finally {
|
|
855
|
-
await handle.close();
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
function applyEvent(body, event) {
|
|
859
|
-
if (event.type === "remove-owner") {
|
|
860
|
-
removeOwner(body, event.ownerKey);
|
|
861
|
-
body.updatedAt = event.at;
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
if (event.type === "put") {
|
|
865
|
-
const current = body.owners[event.ownerKey];
|
|
866
|
-
const slots = new Set(current?.slots ?? []);
|
|
867
|
-
slots.add(event.slotKey);
|
|
868
|
-
body.entries[event.slotKey] = event.entry;
|
|
869
|
-
body.owners[event.ownerKey] = {
|
|
870
|
-
...event.owner,
|
|
871
|
-
slots: [...slots].sort()
|
|
872
|
-
};
|
|
873
|
-
body.updatedAt = event.at;
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
876
|
-
const entries = event.mode === "merge" ? ownerEntries(body, body.owners[event.ownerKey]) : /* @__PURE__ */ new Map();
|
|
877
|
-
for (const item of event.entries) entries.set(item.slotKey, item.entry);
|
|
878
|
-
for (const slot of body.owners[event.ownerKey]?.slots ?? []) delete body.entries[slot];
|
|
879
|
-
for (const [slotKey, entry] of entries) body.entries[slotKey] = entry;
|
|
880
|
-
body.owners[event.ownerKey] = {
|
|
881
|
-
...event.owner,
|
|
882
|
-
slots: [...entries.keys()].sort()
|
|
883
|
-
};
|
|
884
|
-
body.updatedAt = event.at;
|
|
885
|
-
}
|
|
886
|
-
function beginOwner(body, owner, cwd, writer, ownerMetadata = {}) {
|
|
887
|
-
const normalizedOwner = {
|
|
888
|
-
kind: owner.kind,
|
|
889
|
-
path: normalizeCachePath(cwd, owner.path)
|
|
890
|
-
};
|
|
891
|
-
const key = ownerKey(normalizedOwner, cwd);
|
|
892
|
-
const previousOwner = body.owners[key];
|
|
893
|
-
const nextEntries = /* @__PURE__ */ new Map();
|
|
894
|
-
const checkpointEntries = /* @__PURE__ */ new Map();
|
|
895
|
-
const commit = (metadata = {}) => {
|
|
896
|
-
const contentHash = metadata.contentHash ?? ownerMetadata.contentHash;
|
|
897
|
-
const committedEntries = metadata.mode === "merge" ? ownerEntries(body, body.owners[key]) : /* @__PURE__ */ new Map();
|
|
898
|
-
for (const [entryKey, cacheEntry] of nextEntries) committedEntries.set(entryKey, cacheEntry);
|
|
899
|
-
const replacedSlots = /* @__PURE__ */ new Set([...previousOwner?.slots ?? [], ...body.owners[key]?.slots ?? []]);
|
|
900
|
-
for (const replacedSlot of replacedSlots) delete body.entries[replacedSlot];
|
|
901
|
-
for (const [entryKey, cacheEntry] of committedEntries) body.entries[entryKey] = cacheEntry;
|
|
902
|
-
const cachedOwner = {
|
|
903
|
-
contentHash,
|
|
904
|
-
kind: normalizedOwner.kind,
|
|
905
|
-
path: normalizedOwner.path,
|
|
906
|
-
slots: [...committedEntries.keys()].sort()
|
|
907
|
-
};
|
|
908
|
-
body.owners[key] = cachedOwner;
|
|
909
|
-
body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
910
|
-
writer.enqueue({
|
|
911
|
-
at: body.updatedAt,
|
|
912
|
-
entries: [...committedEntries].map(([slotKey, entry]) => ({
|
|
913
|
-
entry,
|
|
914
|
-
slotKey
|
|
915
|
-
})),
|
|
916
|
-
mode: metadata.mode ?? "replace",
|
|
917
|
-
owner: cachedOwner,
|
|
918
|
-
ownerKey: key,
|
|
919
|
-
type: "replace-owner"
|
|
920
|
-
});
|
|
921
|
-
checkpointEntries.clear();
|
|
922
|
-
};
|
|
923
|
-
return {
|
|
924
|
-
checkpoint: async () => {
|
|
925
|
-
const cachedOwner = {
|
|
926
|
-
contentHash: ownerMetadata.contentHash,
|
|
927
|
-
kind: normalizedOwner.kind,
|
|
928
|
-
path: normalizedOwner.path,
|
|
929
|
-
slots: []
|
|
930
|
-
};
|
|
931
|
-
for (const [entryKey, cacheEntry] of checkpointEntries) {
|
|
932
|
-
const event = {
|
|
933
|
-
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
934
|
-
entry: cacheEntry,
|
|
935
|
-
owner: cachedOwner,
|
|
936
|
-
ownerKey: key,
|
|
937
|
-
slotKey: entryKey,
|
|
938
|
-
type: "put"
|
|
939
|
-
};
|
|
940
|
-
applyEvent(body, event);
|
|
941
|
-
writer.enqueue(event);
|
|
942
|
-
}
|
|
943
|
-
checkpointEntries.clear();
|
|
944
|
-
await writer.flush(false);
|
|
945
|
-
},
|
|
946
|
-
commit,
|
|
947
|
-
discard: (cacheSlot) => {
|
|
948
|
-
const entryKey = slotKey(normalizedOwner, cacheSlot, cwd);
|
|
949
|
-
nextEntries.delete(entryKey);
|
|
950
|
-
checkpointEntries.delete(entryKey);
|
|
951
|
-
},
|
|
952
|
-
lookup: (cacheSlot, fingerprint) => {
|
|
953
|
-
const entryKey = slotKey(normalizedOwner, cacheSlot, cwd);
|
|
954
|
-
const cached = body.entries[entryKey];
|
|
955
|
-
if (!cached || !fingerprintsEqual(cached.fingerprint, fingerprint)) return void 0;
|
|
956
|
-
nextEntries.set(entryKey, cached);
|
|
957
|
-
return cached;
|
|
958
|
-
},
|
|
959
|
-
put: (cacheSlot, cacheEntry) => {
|
|
960
|
-
const entryKey = slotKey(normalizedOwner, cacheSlot, cwd);
|
|
961
|
-
nextEntries.set(entryKey, cacheEntry);
|
|
962
|
-
checkpointEntries.set(entryKey, cacheEntry);
|
|
963
|
-
}
|
|
964
|
-
};
|
|
965
|
-
}
|
|
966
|
-
async function collectMissingFileOwners(body, cwd, fileExists) {
|
|
967
|
-
const removed = [];
|
|
968
|
-
for (const [key, owner] of Object.entries(body.owners)) {
|
|
969
|
-
if (owner.kind !== "file" || await fileExists(resolve$1(cwd, owner.path))) continue;
|
|
970
|
-
removeOwner(body, key);
|
|
971
|
-
removed.push(key);
|
|
972
|
-
}
|
|
973
|
-
return removed;
|
|
974
|
-
}
|
|
975
|
-
function createBaseTargetIdentity(target) {
|
|
976
|
-
if (target.identity && (target.kind !== "file" || target.identity !== "file")) return target.filePath ? `${target.kind}:${target.filePath}:${target.identity}` : `${target.kind}:${target.identity}`;
|
|
977
|
-
if (target.kind === "file") return target.filePath ? `file:${target.filePath}` : "file";
|
|
978
|
-
if (target.name) return `${target.kind}:${target.name}`;
|
|
979
|
-
if (target.range) return `${target.kind}:${target.range.start}:${target.range.end}`;
|
|
980
|
-
return target.kind;
|
|
981
|
-
}
|
|
982
|
-
function createEmptyCacheBody(createdAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
983
|
-
return {
|
|
984
|
-
createdAt,
|
|
985
|
-
entries: {},
|
|
986
|
-
owners: {},
|
|
987
|
-
updatedAt: createdAt
|
|
988
|
-
};
|
|
989
|
-
}
|
|
990
|
-
function createEventWriter(location, metadata, dependencies) {
|
|
991
|
-
const backlog = [];
|
|
992
|
-
let validationError;
|
|
993
|
-
let previousWrite = Promise.resolve();
|
|
994
|
-
const persistBacklog = async () => {
|
|
995
|
-
if (validationError) throw validationError;
|
|
996
|
-
if (backlog.length === 0) return;
|
|
997
|
-
await mkdir(dirname(location), { recursive: true });
|
|
998
|
-
await withCacheLock(location, dependencies.lock, getCacheLockOptions({
|
|
999
|
-
factor: 2,
|
|
1000
|
-
maxTimeout: 16e3,
|
|
1001
|
-
minTimeout: 2e3,
|
|
1002
|
-
retries: 4
|
|
1003
|
-
}), async () => {
|
|
1004
|
-
await ensureCompatibleMetadata(location, metadata, dependencies.write);
|
|
1005
|
-
const batchSize = backlog.length;
|
|
1006
|
-
await appendBatch(location, backlog.slice(0, batchSize).map((event) => `${JSON.stringify(event)}\n`).join(""), dependencies.append);
|
|
1007
|
-
backlog.splice(0, batchSize);
|
|
1008
|
-
});
|
|
1009
|
-
validationError = void 0;
|
|
1010
|
-
};
|
|
1011
|
-
return {
|
|
1012
|
-
enqueue: (event) => {
|
|
1013
|
-
try {
|
|
1014
|
-
backlog.push(parse(CacheEventSchema, event));
|
|
1015
|
-
} catch (error) {
|
|
1016
|
-
validationError ??= error;
|
|
1017
|
-
}
|
|
1018
|
-
},
|
|
1019
|
-
flush: (finalAttempt) => {
|
|
1020
|
-
const write = previousWrite.then(persistBacklog);
|
|
1021
|
-
previousWrite = write.catch(() => {});
|
|
1022
|
-
return finalAttempt ? write : write.catch(() => {});
|
|
1023
|
-
}
|
|
1024
|
-
};
|
|
1025
|
-
}
|
|
1026
|
-
function createNoopCacheStore(location) {
|
|
1027
|
-
const transaction = {
|
|
1028
|
-
checkpoint: async () => {},
|
|
1029
|
-
commit: () => {},
|
|
1030
|
-
discard: () => {},
|
|
1031
|
-
lookup: () => void 0,
|
|
1032
|
-
put: () => {}
|
|
1033
|
-
};
|
|
1034
|
-
return {
|
|
1035
|
-
beginOwner: () => transaction,
|
|
1036
|
-
flush: async () => {},
|
|
1037
|
-
location,
|
|
1038
|
-
reconcile: async () => {}
|
|
1039
|
-
};
|
|
1040
|
-
}
|
|
1041
|
-
function createReadOnlyCacheStore(location, body, cwd) {
|
|
1042
|
-
return {
|
|
1043
|
-
beginOwner: (owner) => ({
|
|
1044
|
-
checkpoint: async () => {},
|
|
1045
|
-
commit: () => {},
|
|
1046
|
-
discard: () => {},
|
|
1047
|
-
lookup: (slot, fingerprint) => {
|
|
1048
|
-
const cached = body.entries[slotKey(owner, slot, cwd)];
|
|
1049
|
-
return cached && fingerprintsEqual(cached.fingerprint, fingerprint) ? cached : void 0;
|
|
1050
|
-
},
|
|
1051
|
-
put: () => {}
|
|
1052
|
-
}),
|
|
1053
|
-
flush: async () => {},
|
|
1054
|
-
location,
|
|
1055
|
-
reconcile: async () => {}
|
|
1056
|
-
};
|
|
1057
|
-
}
|
|
1058
|
-
async function defaultFileExists(path) {
|
|
1059
|
-
try {
|
|
1060
|
-
await access(path);
|
|
1061
|
-
return true;
|
|
1062
|
-
} catch (error) {
|
|
1063
|
-
if (isMissingFileError(error)) return false;
|
|
1064
|
-
throw error;
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
function emptyLoadedLog(alintVersion) {
|
|
1068
|
-
const metadata = {
|
|
1069
|
-
alintVersion,
|
|
1070
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1071
|
-
magic: CACHE_MAGIC,
|
|
1072
|
-
schemaVersion: 2,
|
|
1073
|
-
type: "metadata"
|
|
1074
|
-
};
|
|
1075
|
-
return {
|
|
1076
|
-
body: createEmptyCacheBody(metadata.createdAt),
|
|
1077
|
-
metadata
|
|
1078
|
-
};
|
|
1079
|
-
}
|
|
1080
|
-
async function ensureCompatibleMetadata(location, metadata, write) {
|
|
1081
|
-
try {
|
|
1082
|
-
if (parseMetadataLine(await readMetadataLine(location), metadata.alintVersion)) return;
|
|
1083
|
-
} catch (error) {
|
|
1084
|
-
if (!isMissingFileError(error)) throw error;
|
|
1085
|
-
}
|
|
1086
|
-
await write(location, `${JSON.stringify(metadata)}\n`);
|
|
1087
|
-
}
|
|
1088
|
-
function fingerprintsEqual(left, right) {
|
|
1089
|
-
return left.configHash === right.configHash && left.modelHash === right.modelHash && left.ruleHash === right.ruleHash && left.targetHash === right.targetHash;
|
|
1090
|
-
}
|
|
1091
|
-
function getCacheLockOptions(retries) {
|
|
1092
|
-
return {
|
|
1093
|
-
realpath: false,
|
|
1094
|
-
retries,
|
|
1095
|
-
stale: LOCK_STALE_MS,
|
|
1096
|
-
update: LOCK_UPDATE_MS
|
|
1097
|
-
};
|
|
1098
|
-
}
|
|
1099
|
-
function hasJsonArrayProperties(input, ancestors) {
|
|
1100
|
-
let arrayLength;
|
|
1101
|
-
let indexCount = 0;
|
|
1102
|
-
for (const key of Reflect.ownKeys(input)) {
|
|
1103
|
-
const descriptor = Object.getOwnPropertyDescriptor(input, key);
|
|
1104
|
-
if (!descriptor || !("value" in descriptor)) return false;
|
|
1105
|
-
if (key === "length") {
|
|
1106
|
-
if (descriptor.enumerable || typeof descriptor.value !== "number") return false;
|
|
1107
|
-
arrayLength = descriptor.value;
|
|
1108
|
-
continue;
|
|
1109
|
-
}
|
|
1110
|
-
const index = typeof key === "string" ? Number(key) : NaN;
|
|
1111
|
-
if (typeof key !== "string" || !Number.isInteger(index) || index < 0 || index >= 2 ** 32 - 1 || String(index) !== key || !descriptor.enumerable) return false;
|
|
1112
|
-
if (!isJsonValueAt(descriptor.value, ancestors)) return false;
|
|
1113
|
-
indexCount += 1;
|
|
732
|
+
function hasJsonArrayProperties(input, ancestors) {
|
|
733
|
+
let arrayLength;
|
|
734
|
+
let indexCount = 0;
|
|
735
|
+
for (const key of Reflect.ownKeys(input)) {
|
|
736
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, key);
|
|
737
|
+
if (!descriptor || !("value" in descriptor)) return false;
|
|
738
|
+
if (key === "length") {
|
|
739
|
+
if (descriptor.enumerable || typeof descriptor.value !== "number") return false;
|
|
740
|
+
arrayLength = descriptor.value;
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
const index = typeof key === "string" ? Number(key) : NaN;
|
|
744
|
+
if (typeof key !== "string" || !Number.isInteger(index) || index < 0 || index >= 2 ** 32 - 1 || String(index) !== key || !descriptor.enumerable) return false;
|
|
745
|
+
if (!isJsonValueAt(descriptor.value, ancestors)) return false;
|
|
746
|
+
indexCount += 1;
|
|
1114
747
|
}
|
|
1115
748
|
return arrayLength !== void 0 && indexCount === arrayLength;
|
|
1116
749
|
}
|
|
@@ -1145,141 +778,509 @@ function isJsonValueAt(input, ancestors) {
|
|
|
1145
778
|
ancestors.delete(input);
|
|
1146
779
|
}
|
|
1147
780
|
}
|
|
1148
|
-
function isMissingFileError(error) {
|
|
1149
|
-
return isNodeErrorCode(error, "ENOENT") || isNodeErrorCode(error, "ENOTDIR");
|
|
781
|
+
function isMissingFileError(error) {
|
|
782
|
+
return isNodeErrorCode(error, "ENOENT") || isNodeErrorCode(error, "ENOTDIR");
|
|
783
|
+
}
|
|
784
|
+
async function loadCacheLog(location, alintVersion) {
|
|
785
|
+
try {
|
|
786
|
+
const metadata = parseMetadataLine(await readMetadataLine(location), alintVersion);
|
|
787
|
+
if (!metadata) return emptyLoadedLog(alintVersion);
|
|
788
|
+
return replayLog(await readFile(location, "utf8"), metadata);
|
|
789
|
+
} catch (error) {
|
|
790
|
+
if (isMissingFileError(error)) return emptyLoadedLog(alintVersion);
|
|
791
|
+
throw error;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
function ownerEntries(body, owner) {
|
|
795
|
+
const entries = /* @__PURE__ */ new Map();
|
|
796
|
+
for (const slot of owner?.slots ?? []) {
|
|
797
|
+
const cacheEntry = body.entries[slot];
|
|
798
|
+
if (cacheEntry) entries.set(slot, cacheEntry);
|
|
799
|
+
}
|
|
800
|
+
return entries;
|
|
801
|
+
}
|
|
802
|
+
function ownerKey(owner, cwd) {
|
|
803
|
+
return stableHash({
|
|
804
|
+
kind: owner.kind,
|
|
805
|
+
path: normalizeCachePath(cwd, owner.path)
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
function parseMetadataLine(line, expectedAlintVersion) {
|
|
809
|
+
if (!line) return void 0;
|
|
810
|
+
try {
|
|
811
|
+
const value = JSON.parse(line);
|
|
812
|
+
const metadata = parse(CacheMetadataSchema, value);
|
|
813
|
+
if (expectedAlintVersion !== void 0 && metadata.alintVersion !== expectedAlintVersion) return void 0;
|
|
814
|
+
return metadata;
|
|
815
|
+
} catch {
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
async function persistCompactedLog(location, loaded, write) {
|
|
820
|
+
const metadata = parse(CacheMetadataSchema, loaded.metadata);
|
|
821
|
+
const events = Object.entries(loaded.body.owners).map(([ownerKey, owner]) => ({
|
|
822
|
+
at: loaded.body.updatedAt,
|
|
823
|
+
entries: [...ownerEntries(loaded.body, owner)].map(([slotKey, entry]) => ({
|
|
824
|
+
entry,
|
|
825
|
+
slotKey
|
|
826
|
+
})),
|
|
827
|
+
mode: "replace",
|
|
828
|
+
owner,
|
|
829
|
+
ownerKey,
|
|
830
|
+
type: "replace-owner"
|
|
831
|
+
})).map((event) => parse(CacheEventSchema, event));
|
|
832
|
+
const contents = [`${JSON.stringify(metadata)}\n`, ...events.map((event) => `${JSON.stringify(event)}\n`)].join("");
|
|
833
|
+
const tempPath = join(dirname(location), `.${basename$1(location)}.${process.pid}.${randomUUID()}.tmp`);
|
|
834
|
+
try {
|
|
835
|
+
await write(tempPath, contents);
|
|
836
|
+
await rename(tempPath, location);
|
|
837
|
+
} catch (error) {
|
|
838
|
+
await rm(tempPath, { force: true }).catch(() => {});
|
|
839
|
+
throw error;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
async function readMetadataLine(location) {
|
|
843
|
+
const handle = await open(location, "r");
|
|
844
|
+
try {
|
|
845
|
+
const buffer = Buffer.alloc(METADATA_READ_LIMIT);
|
|
846
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
847
|
+
const newlineIndex = buffer.subarray(0, bytesRead).indexOf(10);
|
|
848
|
+
if (newlineIndex < 0) return void 0;
|
|
849
|
+
const lineEnd = newlineIndex > 0 && buffer[newlineIndex - 1] === 13 ? newlineIndex - 1 : newlineIndex;
|
|
850
|
+
return buffer.toString("utf8", 0, lineEnd);
|
|
851
|
+
} finally {
|
|
852
|
+
await handle.close();
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
function removeOwner(body, key) {
|
|
856
|
+
for (const slot of body.owners[key]?.slots ?? []) delete body.entries[slot];
|
|
857
|
+
delete body.owners[key];
|
|
858
|
+
}
|
|
859
|
+
function replayLog(text, metadata) {
|
|
860
|
+
const body = createEmptyCacheBody(metadata.createdAt);
|
|
861
|
+
const lines = text.split(/\r?\n/);
|
|
862
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
863
|
+
const line = lines[index];
|
|
864
|
+
if (!line) continue;
|
|
865
|
+
try {
|
|
866
|
+
const value = JSON.parse(line);
|
|
867
|
+
applyEvent(body, parse(CacheEventSchema, value));
|
|
868
|
+
} catch {}
|
|
869
|
+
}
|
|
870
|
+
return {
|
|
871
|
+
body: parse(CacheFileBodySchema, body),
|
|
872
|
+
metadata
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
function slotKey(owner, slot, cwd) {
|
|
876
|
+
return stableHash({
|
|
877
|
+
owner: ownerKey(owner, cwd),
|
|
878
|
+
...slot
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
async function trimOnce(location, alintVersion, fallback, dependencies) {
|
|
882
|
+
await mkdir(dirname(location), { recursive: true });
|
|
883
|
+
try {
|
|
884
|
+
return await withCacheLock(location, dependencies.lock, getCacheLockOptions(0), async () => {
|
|
885
|
+
const latest = await loadCacheLog(location, alintVersion);
|
|
886
|
+
await persistCompactedLog(location, latest, dependencies.write);
|
|
887
|
+
return latest;
|
|
888
|
+
});
|
|
889
|
+
} catch (error) {
|
|
890
|
+
if (isNodeErrorCode(error, "ELOCKED")) return fallback;
|
|
891
|
+
throw error;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
async function withCacheLock(location, lock, options, operation) {
|
|
895
|
+
let compromised;
|
|
896
|
+
const release = await lock.acquire(location, {
|
|
897
|
+
...options,
|
|
898
|
+
onCompromised: (error) => {
|
|
899
|
+
compromised = error;
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
let operationError;
|
|
903
|
+
try {
|
|
904
|
+
const result = await operation();
|
|
905
|
+
if (compromised) throw compromised;
|
|
906
|
+
return result;
|
|
907
|
+
} catch (error) {
|
|
908
|
+
operationError = error;
|
|
909
|
+
throw error;
|
|
910
|
+
} finally {
|
|
911
|
+
try {
|
|
912
|
+
await release();
|
|
913
|
+
} catch (error) {
|
|
914
|
+
if (operationError === void 0) throw error;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
//#endregion
|
|
919
|
+
//#region src/core/languages/js/index.ts
|
|
920
|
+
const javascriptLanguage = {
|
|
921
|
+
extensions: [
|
|
922
|
+
".cjs",
|
|
923
|
+
".js",
|
|
924
|
+
".jsx",
|
|
925
|
+
".mjs"
|
|
926
|
+
],
|
|
927
|
+
extract: (file) => extractJsSourceTargets(withLanguage(file, "javascript")),
|
|
928
|
+
name: "javascript"
|
|
929
|
+
};
|
|
930
|
+
//#endregion
|
|
931
|
+
//#region src/core/languages/registry.ts
|
|
932
|
+
function createLanguageRegistry$1() {
|
|
933
|
+
return {
|
|
934
|
+
byExtension: /* @__PURE__ */ new Map(),
|
|
935
|
+
languages: /* @__PURE__ */ new Map()
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
function registerLanguage(registry, language) {
|
|
939
|
+
const existing = registry.languages.get(language.name);
|
|
940
|
+
if (existing && existing !== language) throw new Error(`Duplicate language "${language.name}".`);
|
|
941
|
+
for (const extension of language.extensions ?? []) {
|
|
942
|
+
const existingOwner = registry.byExtension.get(extension);
|
|
943
|
+
if (existingOwner && existingOwner !== language.name) throw new Error(`Duplicate language extension "${extension}".`);
|
|
944
|
+
}
|
|
945
|
+
registry.languages.set(language.name, language);
|
|
946
|
+
for (const extension of language.extensions ?? []) registry.byExtension.set(extension, language.name);
|
|
947
|
+
}
|
|
948
|
+
//#endregion
|
|
949
|
+
//#region src/core/languages/text.ts
|
|
950
|
+
const textLanguage = {
|
|
951
|
+
extensions: [],
|
|
952
|
+
extract: (file) => [{
|
|
953
|
+
file,
|
|
954
|
+
identity: "file",
|
|
955
|
+
kind: "file",
|
|
956
|
+
language: "plaintext",
|
|
957
|
+
origin: { physicalPath: file.path },
|
|
958
|
+
text: file.text
|
|
959
|
+
}],
|
|
960
|
+
name: "plaintext"
|
|
961
|
+
};
|
|
962
|
+
//#endregion
|
|
963
|
+
//#region src/core/languages/ts/index.ts
|
|
964
|
+
const typescriptLanguage = {
|
|
965
|
+
extensions: [
|
|
966
|
+
".cts",
|
|
967
|
+
".mts",
|
|
968
|
+
".ts",
|
|
969
|
+
".tsx"
|
|
970
|
+
],
|
|
971
|
+
extract: (file) => extractJsSourceTargets(withLanguage(file, "typescript")),
|
|
972
|
+
name: "typescript"
|
|
973
|
+
};
|
|
974
|
+
//#endregion
|
|
975
|
+
//#region src/core/languages/resolve.ts
|
|
976
|
+
function resolveLanguage(file, registry, options) {
|
|
977
|
+
return resolveLanguageForPath(file.path, registry, options);
|
|
978
|
+
}
|
|
979
|
+
function resolveLanguageForPath(filePath, registry, options) {
|
|
980
|
+
const languageName = options.language ?? options.processedLanguage ?? registry.byExtension.get(extname$1(filePath)) ?? "plaintext";
|
|
981
|
+
const language = registry.languages.get(languageName);
|
|
982
|
+
if (!language) {
|
|
983
|
+
const registered = [...registry.languages.keys()].sort().join(", ");
|
|
984
|
+
throw new Error(`Unknown language "${languageName}". Languages come from plugins, and these are registered: ${registered}.`);
|
|
985
|
+
}
|
|
986
|
+
return language;
|
|
987
|
+
}
|
|
988
|
+
//#endregion
|
|
989
|
+
//#region src/core/languages/index.ts
|
|
990
|
+
function createBuiltInLanguageRegistry() {
|
|
991
|
+
const registry = createLanguageRegistry$1();
|
|
992
|
+
registerLanguage(registry, textLanguage);
|
|
993
|
+
registerLanguage(registry, javascriptLanguage);
|
|
994
|
+
registerLanguage(registry, typescriptLanguage);
|
|
995
|
+
return registry;
|
|
996
|
+
}
|
|
997
|
+
//#endregion
|
|
998
|
+
//#region src/dsl/registry.ts
|
|
999
|
+
function buildRuleRegistry(config) {
|
|
1000
|
+
const rules = /* @__PURE__ */ new Map();
|
|
1001
|
+
const localIds = /* @__PURE__ */ new Map();
|
|
1002
|
+
const enabledRules = [];
|
|
1003
|
+
for (const [alias, plugin] of Object.entries(config.plugins)) for (const [localId, rule] of Object.entries(plugin.rules ?? {})) {
|
|
1004
|
+
const id = `${alias}/${localId}`;
|
|
1005
|
+
if (rules.has(id)) throw new Error(`Duplicate rule id "${id}".`);
|
|
1006
|
+
rules.set(id, rule);
|
|
1007
|
+
localIds.set(id, localId);
|
|
1008
|
+
}
|
|
1009
|
+
for (const [id, entry] of Object.entries(config.rules)) {
|
|
1010
|
+
const rule = rules.get(id);
|
|
1011
|
+
if (!rule) throw new Error(`Unknown rule "${id}".`);
|
|
1012
|
+
const normalizedEntry = normalizeRuleConfigEntry(entry);
|
|
1013
|
+
const severity = normalizedEntry.severity;
|
|
1014
|
+
if (severity === "off") continue;
|
|
1015
|
+
enabledRules.push({
|
|
1016
|
+
id,
|
|
1017
|
+
localId: localIds.get(id) ?? id,
|
|
1018
|
+
options: parseRuleOptions(id, rule.options, normalizedEntry.options),
|
|
1019
|
+
rule,
|
|
1020
|
+
severity
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
return {
|
|
1024
|
+
enabledRules,
|
|
1025
|
+
rules
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
function formatRuleOptionIssues(index, issues) {
|
|
1029
|
+
return issues.map((issue) => {
|
|
1030
|
+
const path = getDotPath(issue);
|
|
1031
|
+
return path === null ? `"${index}": ${issue.message}` : `"${index}.${path}": ${issue.message}`;
|
|
1032
|
+
}).join("; ");
|
|
1150
1033
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1034
|
+
function isRuleConfigTuple(entry) {
|
|
1035
|
+
return Array.isArray(entry);
|
|
1036
|
+
}
|
|
1037
|
+
function normalizeRuleConfigEntry(entry) {
|
|
1038
|
+
if (isRuleConfigTuple(entry)) {
|
|
1039
|
+
const [severity = "warn", ...options] = entry;
|
|
1040
|
+
return {
|
|
1041
|
+
options,
|
|
1042
|
+
severity
|
|
1043
|
+
};
|
|
1159
1044
|
}
|
|
1045
|
+
return {
|
|
1046
|
+
options: [],
|
|
1047
|
+
severity: entry ?? "warn"
|
|
1048
|
+
};
|
|
1160
1049
|
}
|
|
1161
|
-
function
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1050
|
+
function parseRuleOption(id, index, schema, value) {
|
|
1051
|
+
const result = safeParse(schema, value);
|
|
1052
|
+
if (result.success) return result.output;
|
|
1053
|
+
if (value === void 0) {
|
|
1054
|
+
const emptyObjectResult = safeParse(schema, {});
|
|
1055
|
+
if (emptyObjectResult.success) return emptyObjectResult.output;
|
|
1166
1056
|
}
|
|
1167
|
-
|
|
1057
|
+
throw new TypeError(`Invalid options for rule "${id}": ${formatRuleOptionIssues(index, result.issues)}`);
|
|
1168
1058
|
}
|
|
1169
|
-
function
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1059
|
+
function parseRuleOptions(id, schema, options) {
|
|
1060
|
+
if (!schema) {
|
|
1061
|
+
if (options.length > 0) throw new TypeError(`Rule "${id}" does not accept options.`);
|
|
1062
|
+
return [];
|
|
1063
|
+
}
|
|
1064
|
+
if (options.length > schema.length) throw new TypeError(`Invalid options for rule "${id}": Unexpected option at index ${schema.length}.`);
|
|
1065
|
+
return schema.map((optionSchema, index) => parseRuleOption(id, index, optionSchema, options[index]));
|
|
1066
|
+
}
|
|
1067
|
+
//#endregion
|
|
1068
|
+
//#region src/core/languages/diagnostics.ts
|
|
1069
|
+
/** A rule named a language and nothing registered it. This is a builtin rule. Not a real rule. */
|
|
1070
|
+
const MISSING_LANGUAGE_RULE = "alint/missing-language";
|
|
1071
|
+
/** A linted file's extension belongs to no registered language. This is a builtin rule. Not a real rule either. */
|
|
1072
|
+
const UNREGISTERED_LANGUAGE_RULE = "alint/unregistered-language";
|
|
1073
|
+
/**
|
|
1074
|
+
* One diagnostic per missing language, always an error: a rule scoped to a language nothing provides
|
|
1075
|
+
* matches no file, so leaving it at a warning would let a run pass having silently checked nothing.
|
|
1076
|
+
*/
|
|
1077
|
+
function missingLanguageDiagnostics(missing) {
|
|
1078
|
+
return [...missing.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([languageId, entry]) => {
|
|
1079
|
+
const ruleIds = [...entry.ruleIds].sort();
|
|
1080
|
+
const subject = ruleIds.length === 1 ? `Rule "${ruleIds[0]}" needs` : `Rules ${ruleIds.map((id) => `"${id}"`).join(", ")} need`;
|
|
1081
|
+
return {
|
|
1082
|
+
filePath: entry.filePath,
|
|
1083
|
+
message: `${subject} the "${languageId}" language, which no plugin registered. Add a plugin that provides "${languageId}", or turn the rule off.`,
|
|
1084
|
+
ruleId: MISSING_LANGUAGE_RULE,
|
|
1085
|
+
severity: "error"
|
|
1086
|
+
};
|
|
1173
1087
|
});
|
|
1174
1088
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1089
|
+
/** Records one rule that named a language the config never registered, grouped by language id. */
|
|
1090
|
+
function recordMissingLanguage(into, languageId, ruleId, path) {
|
|
1091
|
+
const existing = into.get(languageId);
|
|
1092
|
+
if (existing === void 0) {
|
|
1093
|
+
into.set(languageId, {
|
|
1094
|
+
filePath: path,
|
|
1095
|
+
ruleIds: /* @__PURE__ */ new Set([ruleId])
|
|
1096
|
+
});
|
|
1183
1097
|
return;
|
|
1184
1098
|
}
|
|
1099
|
+
existing.ruleIds.add(ruleId);
|
|
1100
|
+
if (path < existing.filePath) existing.filePath = path;
|
|
1185
1101
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
})
|
|
1199
|
-
const contents = [`${JSON.stringify(metadata)}\n`, ...events.map((event) => `${JSON.stringify(event)}\n`)].join("");
|
|
1200
|
-
const tempPath = join(dirname(location), `.${basename$1(location)}.${process.pid}.${randomUUID()}.tmp`);
|
|
1201
|
-
try {
|
|
1202
|
-
await write(tempPath, contents);
|
|
1203
|
-
await rename(tempPath, location);
|
|
1204
|
-
} catch (error) {
|
|
1205
|
-
await rm(tempPath, { force: true }).catch(() => {});
|
|
1206
|
-
throw error;
|
|
1207
|
-
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Records one linted file no language claimed, grouped by extension so a missing pack is one
|
|
1104
|
+
* diagnostic, not one per file. `'off'` records nothing.
|
|
1105
|
+
*/
|
|
1106
|
+
function recordUnregistered(into, path, severity) {
|
|
1107
|
+
if (severity === "off") return;
|
|
1108
|
+
const key = extname(path) || basename(path);
|
|
1109
|
+
const existing = into.get(key);
|
|
1110
|
+
into.set(key, {
|
|
1111
|
+
count: (existing?.count ?? 0) + 1,
|
|
1112
|
+
filePath: existing !== void 0 && existing.filePath < path ? existing.filePath : path,
|
|
1113
|
+
severity: existing?.severity === "error" ? "error" : severity
|
|
1114
|
+
});
|
|
1208
1115
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1116
|
+
/** One diagnostic per extension, sorted, for the run to prepend to its results. */
|
|
1117
|
+
function unregisteredLanguageDiagnostics(unregistered) {
|
|
1118
|
+
return [...unregistered.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([extension, entry]) => ({
|
|
1119
|
+
filePath: entry.filePath,
|
|
1120
|
+
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.`,
|
|
1121
|
+
ruleId: UNREGISTERED_LANGUAGE_RULE,
|
|
1122
|
+
severity: entry.severity
|
|
1123
|
+
}));
|
|
1124
|
+
}
|
|
1125
|
+
function unregisteredLanguageSeverity(linterOptions) {
|
|
1126
|
+
const value = linterOptions.reportUnregisteredLanguages;
|
|
1127
|
+
return value === "error" || value === "off" ? value : "warn";
|
|
1128
|
+
}
|
|
1129
|
+
//#endregion
|
|
1130
|
+
//#region src/core/languages/rule-languages.ts
|
|
1131
|
+
/**
|
|
1132
|
+
* The language a file falls back to when no registered language claims its extension. Not a real
|
|
1133
|
+
* language, which is why `'any'` excludes it.
|
|
1134
|
+
*/
|
|
1135
|
+
const PLAIN_TEXT = "plaintext";
|
|
1136
|
+
/**
|
|
1137
|
+
* Whether a rule reads this target.
|
|
1138
|
+
*
|
|
1139
|
+
* A rule with language off still reads file targets, because reading raw text, or asking a model about a
|
|
1140
|
+
* whole file, needs no language. However, it never reads the function and class targets extraction produces.
|
|
1141
|
+
* `'file'` is the one kind every language yields, plain text included, so it is the dividing line.
|
|
1142
|
+
*
|
|
1143
|
+
* `'any'` means any real language, so a file that fell back to plain text because nothing claimed
|
|
1144
|
+
* its extension is withheld: the rule asked for structure and there is none to give. The run says so
|
|
1145
|
+
* once per extension through `alint/unregistered-language` rather than letting each rule report an
|
|
1146
|
+
* empty file.
|
|
1147
|
+
*/
|
|
1148
|
+
function isTargetLanguageAccepted(languages, targetKind, targetLanguage) {
|
|
1149
|
+
if (languages.kind === "off") return targetKind === "file";
|
|
1150
|
+
if (languages.kind === "any") return targetLanguage !== PLAIN_TEXT;
|
|
1151
|
+
return languages.ids.includes(targetLanguage);
|
|
1152
|
+
}
|
|
1153
|
+
function resolveRuleLanguages(languages) {
|
|
1154
|
+
if (languages === void 0) return { kind: "off" };
|
|
1155
|
+
if (languages === "any") return { kind: "any" };
|
|
1156
|
+
if ("ids" in languages) return {
|
|
1157
|
+
ids: languages.ids,
|
|
1158
|
+
kind: "list",
|
|
1159
|
+
skipMissing: languages.skipMissing ?? false
|
|
1160
|
+
};
|
|
1161
|
+
return {
|
|
1162
|
+
ids: languages,
|
|
1163
|
+
kind: "list",
|
|
1164
|
+
skipMissing: false
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
//#endregion
|
|
1168
|
+
//#region src/core/preparation.ts
|
|
1169
|
+
/**
|
|
1170
|
+
* The `ctx.src.extract` a run hands to rules, for parsing files it was never asked to lint, which
|
|
1171
|
+
* an index builder sweeping the workspace needs.
|
|
1172
|
+
*
|
|
1173
|
+
* It resolves each file's OWN config rather than a caller's, because the language a file resolves to
|
|
1174
|
+
* is a config decision and two config groups may register different plugins. It extracts on demand
|
|
1175
|
+
* and holds nothing: source planning releases rich extractor values after compact jobs are admitted,
|
|
1176
|
+
* and a run-wide parse memo here would put that retention back. A caller keeps only what it derives.
|
|
1177
|
+
*
|
|
1178
|
+
* `getSrc` defers reading the runtime because the runtime is built around this closure. It is assigned
|
|
1179
|
+
* before any rule runs, so by the first call it is present.
|
|
1180
|
+
*/
|
|
1181
|
+
function createSourceExtractor(cwd, config, getSrc) {
|
|
1182
|
+
return async (filePath, options = {}) => {
|
|
1183
|
+
const path = resolve(cwd, filePath);
|
|
1184
|
+
const resolvedConfig = resolveConfigForFile(path, config, { cwd });
|
|
1185
|
+
if (resolvedConfig.ignored) return [];
|
|
1186
|
+
const effectiveConfig = resolvedConfig.config;
|
|
1187
|
+
const language = resolveLanguageForPath(path, createLanguageRegistry(effectiveConfig), { language: options.language ?? effectiveConfig.language });
|
|
1188
|
+
const src = getSrc();
|
|
1189
|
+
const file = await src.readFile(path);
|
|
1190
|
+
return language.extract(file, {
|
|
1191
|
+
cwd,
|
|
1192
|
+
languageOptions: effectiveConfig.languageOptions,
|
|
1193
|
+
src
|
|
1194
|
+
});
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
function prepareRun(options = {}) {
|
|
1198
|
+
const cwd$2 = options.cwd ?? cwd();
|
|
1199
|
+
const config = options.config ?? [];
|
|
1200
|
+
const files = [];
|
|
1201
|
+
const directories = [];
|
|
1202
|
+
const missingLanguages = /* @__PURE__ */ new Map();
|
|
1203
|
+
const unregisteredLanguages = /* @__PURE__ */ new Map();
|
|
1204
|
+
for (const filePath of options.files ?? []) {
|
|
1205
|
+
const path = resolve(cwd$2, filePath);
|
|
1206
|
+
const resolvedConfig = resolveConfigForFile(path, config, { cwd: cwd$2 });
|
|
1207
|
+
if (resolvedConfig.ignored) continue;
|
|
1208
|
+
const effectiveConfig = resolvedConfig.config;
|
|
1209
|
+
const languageRegistry = createLanguageRegistry(effectiveConfig);
|
|
1210
|
+
const language = resolveLanguageForPath(path, languageRegistry, { language: effectiveConfig.language });
|
|
1211
|
+
const rules = prepareRules(effectiveConfig);
|
|
1212
|
+
recordMissingLanguages(missingLanguages, rules, languageRegistry, path);
|
|
1213
|
+
if (effectiveConfig.language === void 0 && language.name === "plaintext" && rules.some(({ enabledRule }) => !isTargetLanguageAccepted(resolveRuleLanguages(enabledRule.rule.languages), "file", "plaintext"))) recordUnregistered(unregisteredLanguages, path, unregisteredLanguageSeverity(effectiveConfig.linterOptions));
|
|
1214
|
+
files.push({
|
|
1215
|
+
agent: effectiveConfig.agent,
|
|
1216
|
+
configHash: stableHash({
|
|
1217
|
+
language: effectiveConfig.language,
|
|
1218
|
+
languageOptions: effectiveConfig.languageOptions,
|
|
1219
|
+
processor: effectiveConfig.processor,
|
|
1220
|
+
resolvedLanguage: language.name,
|
|
1221
|
+
settings: effectiveConfig.settings
|
|
1222
|
+
}),
|
|
1223
|
+
fileIndex: files.length,
|
|
1224
|
+
language,
|
|
1225
|
+
languageOptions: effectiveConfig.languageOptions,
|
|
1226
|
+
path,
|
|
1227
|
+
rules,
|
|
1228
|
+
settings: effectiveConfig.settings
|
|
1229
|
+
});
|
|
1220
1230
|
}
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1231
|
+
for (const directoryPath of options.directories ?? []) {
|
|
1232
|
+
const path = resolve(cwd$2, directoryPath);
|
|
1233
|
+
const resolvedConfig = resolveConfigForDirectory(path, config, { cwd: cwd$2 });
|
|
1234
|
+
if (resolvedConfig.ignored) continue;
|
|
1235
|
+
const effectiveConfig = resolvedConfig.config;
|
|
1236
|
+
directories.push({
|
|
1237
|
+
agent: effectiveConfig.agent,
|
|
1238
|
+
configHash: stableHash({ settings: effectiveConfig.settings }),
|
|
1239
|
+
directoryIndex: directories.length,
|
|
1240
|
+
rules: prepareRules(effectiveConfig),
|
|
1241
|
+
settings: effectiveConfig.settings,
|
|
1242
|
+
target: {
|
|
1243
|
+
kind: "directory",
|
|
1244
|
+
path
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1236
1247
|
}
|
|
1237
1248
|
return {
|
|
1238
|
-
|
|
1239
|
-
|
|
1249
|
+
directories,
|
|
1250
|
+
files,
|
|
1251
|
+
missingLanguages,
|
|
1252
|
+
project: options.projectTargets === false ? void 0 : prepareProject(cwd$2, config),
|
|
1253
|
+
unregisteredLanguages
|
|
1240
1254
|
};
|
|
1241
1255
|
}
|
|
1242
|
-
function
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
});
|
|
1256
|
+
function createLanguageRegistry(config) {
|
|
1257
|
+
const registry = createBuiltInLanguageRegistry();
|
|
1258
|
+
for (const plugin of Object.values(config.plugins)) for (const language of Object.values(plugin.languages ?? {})) registerLanguage(registry, language);
|
|
1259
|
+
return registry;
|
|
1247
1260
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
}
|
|
1261
|
+
function prepareProject(root, config) {
|
|
1262
|
+
const resolvedConfig = resolveConfigForProject(root, config, { cwd: root });
|
|
1263
|
+
if (resolvedConfig.ignored) return void 0;
|
|
1264
|
+
const effectiveConfig = resolvedConfig.config;
|
|
1265
|
+
return {
|
|
1266
|
+
agent: effectiveConfig.agent,
|
|
1267
|
+
configHash: stableHash({ settings: effectiveConfig.settings }),
|
|
1268
|
+
root,
|
|
1269
|
+
rules: prepareRules(effectiveConfig),
|
|
1270
|
+
settings: effectiveConfig.settings
|
|
1271
|
+
};
|
|
1260
1272
|
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
})
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
const
|
|
1272
|
-
if (compromised) throw compromised;
|
|
1273
|
-
return result;
|
|
1274
|
-
} catch (error) {
|
|
1275
|
-
operationError = error;
|
|
1276
|
-
throw error;
|
|
1277
|
-
} finally {
|
|
1278
|
-
try {
|
|
1279
|
-
await release();
|
|
1280
|
-
} catch (error) {
|
|
1281
|
-
if (operationError === void 0) throw error;
|
|
1282
|
-
}
|
|
1273
|
+
function prepareRules(config) {
|
|
1274
|
+
return buildRuleRegistry(config).enabledRules.map((enabledRule, ruleIndex) => ({
|
|
1275
|
+
enabledRule,
|
|
1276
|
+
ruleIndex
|
|
1277
|
+
}));
|
|
1278
|
+
}
|
|
1279
|
+
function recordMissingLanguages(into, rules, registry, path) {
|
|
1280
|
+
for (const { enabledRule } of rules) {
|
|
1281
|
+
const languages = resolveRuleLanguages(enabledRule.rule.languages);
|
|
1282
|
+
if (languages.kind !== "list" || languages.skipMissing) continue;
|
|
1283
|
+
for (const languageId of languages.ids) if (!registry.languages.has(languageId)) recordMissingLanguage(into, languageId, enabledRule.id, path);
|
|
1283
1284
|
}
|
|
1284
1285
|
}
|
|
1285
1286
|
//#endregion
|
|
@@ -2363,88 +2364,14 @@ function projectExecution(runtime) {
|
|
|
2363
2364
|
//#endregion
|
|
2364
2365
|
//#region src/core/source/planner.ts
|
|
2365
2366
|
/**
|
|
2366
|
-
*
|
|
2367
|
-
*
|
|
2368
|
-
* The returned outcome promise captures only cache ownership and hashes. This function does not
|
|
2369
|
-
* await rule execution, so its `SourceFile`, parser output, and rich targets become unreachable
|
|
2370
|
-
* when planning returns.
|
|
2371
|
-
*/
|
|
2372
|
-
async function planSource(input, options) {
|
|
2373
|
-
if (options.signal?.aborted) return { outcomes: Promise.resolve([]) };
|
|
2374
|
-
let file;
|
|
2375
|
-
try {
|
|
2376
|
-
file = await options.src.readFile(input.path);
|
|
2377
|
-
} catch (error) {
|
|
2378
|
-
reportFilePlanningComplete(input, 0, options);
|
|
2379
|
-
return {
|
|
2380
|
-
failure: fileFailure(input, "read", failureMessage(error, "Failed to read source file.")),
|
|
2381
|
-
outcomes: Promise.resolve([])
|
|
2382
|
-
};
|
|
2383
|
-
}
|
|
2384
|
-
if (options.signal?.aborted) return { outcomes: Promise.resolve([]) };
|
|
2385
|
-
let targets;
|
|
2386
|
-
try {
|
|
2387
|
-
targets = await input.language.extract(file, {
|
|
2388
|
-
cwd: options.cwd,
|
|
2389
|
-
languageOptions: input.languageOptions,
|
|
2390
|
-
src: options.src
|
|
2391
|
-
});
|
|
2392
|
-
} catch (error) {
|
|
2393
|
-
reportFilePlanningComplete(input, 0, options);
|
|
2394
|
-
return {
|
|
2395
|
-
failure: fileFailure(input, "extract", failureMessage(error, "Failed to extract source targets.")),
|
|
2396
|
-
outcomes: Promise.resolve([])
|
|
2397
|
-
};
|
|
2398
|
-
}
|
|
2399
|
-
const { contentHash } = file;
|
|
2400
|
-
let cacheOwner;
|
|
2401
|
-
let jobs;
|
|
2402
|
-
let project;
|
|
2403
|
-
try {
|
|
2404
|
-
project = options.projectSnapshots === false ? void 0 : createProjectSnapshot(input, file.language, file.path, contentHash, targets);
|
|
2405
|
-
cacheOwner = options.cacheStore.beginOwner({
|
|
2406
|
-
kind: "file",
|
|
2407
|
-
path: file.path
|
|
2408
|
-
}, { contentHash });
|
|
2409
|
-
const baseFile = {
|
|
2410
|
-
contentHash,
|
|
2411
|
-
language: file.language,
|
|
2412
|
-
path: file.path
|
|
2413
|
-
};
|
|
2414
|
-
jobs = createSourceJobs(input, createExecutionTargets(input, targets, baseFile, options.ruleRuntimes, options.cwd, cacheOwner));
|
|
2415
|
-
} catch (error) {
|
|
2416
|
-
reportFilePlanningComplete(input, 0, options);
|
|
2417
|
-
return {
|
|
2418
|
-
failure: fileFailure(input, "extract", failureMessage(error, "Failed to plan source targets.")),
|
|
2419
|
-
outcomes: Promise.resolve([])
|
|
2420
|
-
};
|
|
2421
|
-
}
|
|
2422
|
-
if (options.signal?.aborted) {
|
|
2423
|
-
cacheOwner.commit({ mode: "merge" });
|
|
2424
|
-
return {
|
|
2425
|
-
outcomes: Promise.resolve([]),
|
|
2426
|
-
project
|
|
2427
|
-
};
|
|
2428
|
-
}
|
|
2429
|
-
const batch = options.scheduler.schedule(jobs);
|
|
2430
|
-
reportFilePlanningComplete(input, batch.jobsAdded, options);
|
|
2431
|
-
return {
|
|
2432
|
-
outcomes: batch.outcomes.then(async (settled) => {
|
|
2433
|
-
cacheOwner.commit(options.signal?.aborted ? { mode: "merge" } : void 0);
|
|
2434
|
-
await options.cacheStore.flush();
|
|
2435
|
-
return settled;
|
|
2436
|
-
}),
|
|
2437
|
-
project
|
|
2438
|
-
};
|
|
2439
|
-
}
|
|
2440
|
-
/**
|
|
2441
|
-
* Starts every input plan independently and preserves input-ordered results.
|
|
2367
|
+
* Starts every input read independently, then batches CPU-heavy extraction and job admission.
|
|
2442
2368
|
*/
|
|
2443
2369
|
async function planSources(inputs, options) {
|
|
2444
|
-
|
|
2370
|
+
const waitForPlanning = createPlanningAdmission();
|
|
2371
|
+
return Promise.all(inputs.map((input) => planSourceWithAdmission(input, {
|
|
2445
2372
|
...options,
|
|
2446
2373
|
ruleRuntimes: options.createRuleRuntimes(input)
|
|
2447
|
-
})));
|
|
2374
|
+
}, waitForPlanning)));
|
|
2448
2375
|
}
|
|
2449
2376
|
function createExecutionTargets(input, targets, file, runtimes, cwd, cacheOwner) {
|
|
2450
2377
|
const resolveIdentity = createTargetIdentityResolver(targets.map((target) => ({
|
|
@@ -2499,6 +2426,28 @@ function createExecutionTargets(input, targets, file, runtimes, cwd, cacheOwner)
|
|
|
2499
2426
|
};
|
|
2500
2427
|
});
|
|
2501
2428
|
}
|
|
2429
|
+
function createPlanningAdmission() {
|
|
2430
|
+
const batchSize = 32;
|
|
2431
|
+
let admitted = 0;
|
|
2432
|
+
let pendingYield;
|
|
2433
|
+
return async () => {
|
|
2434
|
+
let currentPendingYield = pendingYield;
|
|
2435
|
+
while (currentPendingYield) {
|
|
2436
|
+
await currentPendingYield;
|
|
2437
|
+
currentPendingYield = pendingYield;
|
|
2438
|
+
}
|
|
2439
|
+
admitted += 1;
|
|
2440
|
+
if (admitted < batchSize) return;
|
|
2441
|
+
admitted = 0;
|
|
2442
|
+
const currentYield = setImmediate();
|
|
2443
|
+
pendingYield = currentYield;
|
|
2444
|
+
try {
|
|
2445
|
+
await currentYield;
|
|
2446
|
+
} finally {
|
|
2447
|
+
if (pendingYield === currentYield) pendingYield = void 0;
|
|
2448
|
+
}
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2502
2451
|
function createProjectSnapshot(input, language, path, contentHash, targets) {
|
|
2503
2452
|
return {
|
|
2504
2453
|
configHash: input.configHash,
|
|
@@ -2570,6 +2519,76 @@ function fileFailure(input, kind, message) {
|
|
|
2570
2519
|
message
|
|
2571
2520
|
};
|
|
2572
2521
|
}
|
|
2522
|
+
async function planSourceWithAdmission(input, options, waitForPlanning) {
|
|
2523
|
+
if (options.signal?.aborted) return { outcomes: Promise.resolve([]) };
|
|
2524
|
+
let file;
|
|
2525
|
+
try {
|
|
2526
|
+
file = await options.src.readFile(input.path);
|
|
2527
|
+
} catch (error) {
|
|
2528
|
+
reportFilePlanningComplete(input, 0, options);
|
|
2529
|
+
return {
|
|
2530
|
+
failure: fileFailure(input, "read", failureMessage(error, "Failed to read source file.")),
|
|
2531
|
+
outcomes: Promise.resolve([])
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
if (options.signal?.aborted) return { outcomes: Promise.resolve([]) };
|
|
2535
|
+
await waitForPlanning?.();
|
|
2536
|
+
if (options.signal?.aborted) return { outcomes: Promise.resolve([]) };
|
|
2537
|
+
let targets;
|
|
2538
|
+
try {
|
|
2539
|
+
targets = await input.language.extract(file, {
|
|
2540
|
+
cwd: options.cwd,
|
|
2541
|
+
languageOptions: input.languageOptions,
|
|
2542
|
+
src: options.src
|
|
2543
|
+
});
|
|
2544
|
+
} catch (error) {
|
|
2545
|
+
reportFilePlanningComplete(input, 0, options);
|
|
2546
|
+
return {
|
|
2547
|
+
failure: fileFailure(input, "extract", failureMessage(error, "Failed to extract source targets.")),
|
|
2548
|
+
outcomes: Promise.resolve([])
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2551
|
+
const { contentHash } = file;
|
|
2552
|
+
let cacheOwner;
|
|
2553
|
+
let jobs;
|
|
2554
|
+
let project;
|
|
2555
|
+
try {
|
|
2556
|
+
project = options.projectSnapshots === false ? void 0 : createProjectSnapshot(input, file.language, file.path, contentHash, targets);
|
|
2557
|
+
cacheOwner = options.cacheStore.beginOwner({
|
|
2558
|
+
kind: "file",
|
|
2559
|
+
path: file.path
|
|
2560
|
+
}, { contentHash });
|
|
2561
|
+
const baseFile = {
|
|
2562
|
+
contentHash,
|
|
2563
|
+
language: file.language,
|
|
2564
|
+
path: file.path
|
|
2565
|
+
};
|
|
2566
|
+
jobs = createSourceJobs(input, createExecutionTargets(input, targets, baseFile, options.ruleRuntimes, options.cwd, cacheOwner));
|
|
2567
|
+
} catch (error) {
|
|
2568
|
+
reportFilePlanningComplete(input, 0, options);
|
|
2569
|
+
return {
|
|
2570
|
+
failure: fileFailure(input, "extract", failureMessage(error, "Failed to plan source targets.")),
|
|
2571
|
+
outcomes: Promise.resolve([])
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
if (options.signal?.aborted) {
|
|
2575
|
+
cacheOwner.commit({ mode: "merge" });
|
|
2576
|
+
return {
|
|
2577
|
+
outcomes: Promise.resolve([]),
|
|
2578
|
+
project
|
|
2579
|
+
};
|
|
2580
|
+
}
|
|
2581
|
+
const batch = options.scheduler.schedule(jobs);
|
|
2582
|
+
reportFilePlanningComplete(input, batch.jobsAdded, options);
|
|
2583
|
+
return {
|
|
2584
|
+
outcomes: batch.outcomes.then(async (settled) => {
|
|
2585
|
+
cacheOwner.commit(options.signal?.aborted ? { mode: "merge" } : void 0);
|
|
2586
|
+
await options.cacheStore.flush();
|
|
2587
|
+
return settled;
|
|
2588
|
+
}),
|
|
2589
|
+
project
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2573
2592
|
function reportFilePlanningComplete(input, jobsAdded, options) {
|
|
2574
2593
|
try {
|
|
2575
2594
|
const progress = options.scheduler.completeFilePlanning();
|
|
@@ -3120,6 +3139,7 @@ async function measureModel({ model, prompt, signal }) {
|
|
|
3120
3139
|
}];
|
|
3121
3140
|
const startedAt = performance.now();
|
|
3122
3141
|
let firstOutputAt;
|
|
3142
|
+
let outputDeltaCount = 0;
|
|
3123
3143
|
const result = streamText({
|
|
3124
3144
|
...model.params,
|
|
3125
3145
|
abortSignal: signal,
|
|
@@ -3139,7 +3159,10 @@ async function measureModel({ model, prompt, signal }) {
|
|
|
3139
3159
|
]);
|
|
3140
3160
|
let settled;
|
|
3141
3161
|
try {
|
|
3142
|
-
for await (const event of result.eventStream) if (
|
|
3162
|
+
for await (const event of result.eventStream) if ((event.type === "reasoning.delta" || event.type === "text.delta") && event.delta.length > 0) {
|
|
3163
|
+
outputDeltaCount += 1;
|
|
3164
|
+
firstOutputAt ??= performance.now();
|
|
3165
|
+
}
|
|
3143
3166
|
} finally {
|
|
3144
3167
|
settled = await completion;
|
|
3145
3168
|
}
|
|
@@ -3148,7 +3171,7 @@ async function measureModel({ model, prompt, signal }) {
|
|
|
3148
3171
|
if (usageResult.status === "rejected") throw usageResult.reason;
|
|
3149
3172
|
return {
|
|
3150
3173
|
durationMs: endedAt - startedAt,
|
|
3151
|
-
firstOutputMs: firstOutputAt === void 0 ? void 0 : firstOutputAt - startedAt,
|
|
3174
|
+
firstOutputMs: firstOutputAt === void 0 || outputDeltaCount < 2 ? void 0 : firstOutputAt - startedAt,
|
|
3152
3175
|
outputTokens: usageResult.value?.outputTokens
|
|
3153
3176
|
};
|
|
3154
3177
|
}
|
|
@@ -3245,4 +3268,4 @@ function sampleCorpus(seed) {
|
|
|
3245
3268
|
].map((section) => `--- corpus section ${section} ---\n${rotated}`).join("\n\n");
|
|
3246
3269
|
}
|
|
3247
3270
|
//#endregion
|
|
3248
|
-
export { AlintAbortError, AlintCachePersistenceError, AlintRunCancelledError, AlintRunError, benchmarkModels, buildRuleRegistry, createBuiltInLanguageRegistry, createSourceExtractor, createSourceFile, createSourceRuntime, defaultStopGateConfig, defineConfig, definePlugin, defineRule, hasDiscoveryFilePatterns, matchesDiscoveryFile, maximumStopGateTimeoutMs, normalizeConfig, registerLanguage, resolveConfigForDirectory, resolveConfigForFile, resolveConfigForProject, resolveLanguage, resolveModel, resolveStopGateConfig, runAlint, sliceLines, sliceRange, stableHash, targetIdentity, withLanguage, withStableIdentities };
|
|
3271
|
+
export { AlintAbortError, AlintCachePersistenceError, AlintRunCancelledError, AlintRunError, benchmarkModels, buildRuleRegistry, createBuiltInLanguageRegistry, createSourceExtractor, createSourceFile, createSourceRuntime, defaultStopGateConfig, defineConfig, definePlugin, defineRule, hasDiscoveryFilePatterns, matchesDiscoveryFile, maximumStopGateTimeoutMs, normalizeConfig, normalizeRunnerCacheConfig, registerLanguage, resolveConfigForDirectory, resolveConfigForFile, resolveConfigForProject, resolveLanguage, resolveModel, resolveStopGateConfig, runAlint, sliceLines, sliceRange, stableHash, targetIdentity, withLanguage, withStableIdentities };
|