@alint-js/core 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +30 -21
- package/dist/index.mjs +745 -741
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -4,12 +4,12 @@ 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";
|
|
@@ -269,553 +269,185 @@ function mergeStopGateConfig(resolved, item) {
|
|
|
269
269
|
};
|
|
270
270
|
}
|
|
271
271
|
//#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
|
-
};
|
|
272
|
+
//#region package.json
|
|
273
|
+
var version = "0.7.0";
|
|
283
274
|
//#endregion
|
|
284
|
-
//#region src/core/
|
|
285
|
-
|
|
275
|
+
//#region src/core/cache/types.ts
|
|
276
|
+
const CACHE_MAGIC = "ALINT_CACHE";
|
|
277
|
+
//#endregion
|
|
278
|
+
//#region src/core/cache/store.ts
|
|
279
|
+
const DEFAULT_CACHE_FILE_NAME = ".alintcache";
|
|
280
|
+
const LOCK_STALE_MS = 2e4;
|
|
281
|
+
const LOCK_UPDATE_MS = 3e3;
|
|
282
|
+
const METADATA_READ_LIMIT = 4096;
|
|
283
|
+
const FiniteNumberSchema = pipe(number(), finite());
|
|
284
|
+
const PositionSchema = object({
|
|
285
|
+
column: FiniteNumberSchema,
|
|
286
|
+
line: FiniteNumberSchema
|
|
287
|
+
});
|
|
288
|
+
const SourceLocationSchema = object({
|
|
289
|
+
end: PositionSchema,
|
|
290
|
+
start: PositionSchema
|
|
291
|
+
});
|
|
292
|
+
const DiagnosticLocationSchema = object({
|
|
293
|
+
end: optional(PositionSchema),
|
|
294
|
+
start: PositionSchema
|
|
295
|
+
});
|
|
296
|
+
const SourceRangeSchema = object({
|
|
297
|
+
end: FiniteNumberSchema,
|
|
298
|
+
start: FiniteNumberSchema
|
|
299
|
+
});
|
|
300
|
+
const DiagnosticSchema = object({
|
|
301
|
+
cached: optional(boolean()),
|
|
302
|
+
evidence: optional(custom(isJsonValue)),
|
|
303
|
+
filePath: string(),
|
|
304
|
+
loc: optional(DiagnosticLocationSchema),
|
|
305
|
+
message: string(),
|
|
306
|
+
model: optional(object({
|
|
307
|
+
providerId: string(),
|
|
308
|
+
requested: optional(string()),
|
|
309
|
+
resolvedId: string()
|
|
310
|
+
})),
|
|
311
|
+
ruleId: string(),
|
|
312
|
+
severity: union([literal("error"), literal("warn")])
|
|
313
|
+
});
|
|
314
|
+
const UsageSchema = object({
|
|
315
|
+
filePath: optional(string()),
|
|
316
|
+
inputTokens: optional(FiniteNumberSchema),
|
|
317
|
+
metadata: optional(custom(isJsonValue)),
|
|
318
|
+
modelId: string(),
|
|
319
|
+
outputTokens: optional(FiniteNumberSchema),
|
|
320
|
+
providerId: string(),
|
|
321
|
+
ruleId: string(),
|
|
322
|
+
totalTokens: optional(FiniteNumberSchema)
|
|
323
|
+
});
|
|
324
|
+
const CacheEntrySchema = object({
|
|
325
|
+
diagnostics: array(DiagnosticSchema),
|
|
326
|
+
fingerprint: object({
|
|
327
|
+
configHash: string(),
|
|
328
|
+
modelHash: string(),
|
|
329
|
+
ruleHash: string(),
|
|
330
|
+
targetHash: string()
|
|
331
|
+
}),
|
|
332
|
+
target: object({
|
|
333
|
+
hash: string(),
|
|
334
|
+
identity: string(),
|
|
335
|
+
kind: string(),
|
|
336
|
+
loc: optional(SourceLocationSchema),
|
|
337
|
+
name: optional(string()),
|
|
338
|
+
range: optional(SourceRangeSchema)
|
|
339
|
+
}),
|
|
340
|
+
usage: array(UsageSchema)
|
|
341
|
+
});
|
|
342
|
+
const CachedOwnerSchema = object({
|
|
343
|
+
contentHash: optional(string()),
|
|
344
|
+
kind: union([literal("file"), literal("project")]),
|
|
345
|
+
path: string(),
|
|
346
|
+
slots: array(string())
|
|
347
|
+
});
|
|
348
|
+
function objectRecord(value) {
|
|
349
|
+
return pipe(unknown(), check((input) => typeof input === "object" && input !== null && !Array.isArray(input)), record(string(), value));
|
|
350
|
+
}
|
|
351
|
+
const CacheFileBodySchema = pipe(object({
|
|
352
|
+
createdAt: string(),
|
|
353
|
+
entries: objectRecord(CacheEntrySchema),
|
|
354
|
+
owners: objectRecord(CachedOwnerSchema),
|
|
355
|
+
updatedAt: string()
|
|
356
|
+
}), check((body) => {
|
|
357
|
+
const entryKeys = Object.keys(body.entries);
|
|
358
|
+
const referencedSlots = Object.values(body.owners).flatMap((owner) => owner.slots);
|
|
359
|
+
const uniqueSlots = new Set(referencedSlots);
|
|
360
|
+
return uniqueSlots.size === referencedSlots.length && uniqueSlots.size === entryKeys.length && entryKeys.every((key) => uniqueSlots.has(key));
|
|
361
|
+
}));
|
|
362
|
+
const CacheMetadataSchema = object({
|
|
363
|
+
alintVersion: string(),
|
|
364
|
+
createdAt: string(),
|
|
365
|
+
magic: literal(CACHE_MAGIC),
|
|
366
|
+
schemaVersion: literal(2),
|
|
367
|
+
type: literal("metadata")
|
|
368
|
+
});
|
|
369
|
+
const PutEventSchema = object({
|
|
370
|
+
at: string(),
|
|
371
|
+
entry: CacheEntrySchema,
|
|
372
|
+
owner: CachedOwnerSchema,
|
|
373
|
+
ownerKey: string(),
|
|
374
|
+
slotKey: string(),
|
|
375
|
+
type: literal("put")
|
|
376
|
+
});
|
|
377
|
+
const ReplaceOwnerEventSchema = object({
|
|
378
|
+
at: string(),
|
|
379
|
+
entries: array(object({
|
|
380
|
+
entry: CacheEntrySchema,
|
|
381
|
+
slotKey: string()
|
|
382
|
+
})),
|
|
383
|
+
mode: union([literal("merge"), literal("replace")]),
|
|
384
|
+
owner: CachedOwnerSchema,
|
|
385
|
+
ownerKey: string(),
|
|
386
|
+
type: literal("replace-owner")
|
|
387
|
+
});
|
|
388
|
+
const RemoveOwnerEventSchema = object({
|
|
389
|
+
at: string(),
|
|
390
|
+
ownerKey: string(),
|
|
391
|
+
type: literal("remove-owner")
|
|
392
|
+
});
|
|
393
|
+
const CacheEventSchema = union([
|
|
394
|
+
PutEventSchema,
|
|
395
|
+
ReplaceOwnerEventSchema,
|
|
396
|
+
RemoveOwnerEventSchema
|
|
397
|
+
]);
|
|
398
|
+
async function createCacheStore(options) {
|
|
399
|
+
const location = resolveCacheLocation(options.cwd, options.location);
|
|
400
|
+
if (!options.enabled) return createNoopCacheStore(location);
|
|
401
|
+
const alintVersion = options.alintVersion ?? version;
|
|
402
|
+
const initial = await loadCacheLog(location, alintVersion);
|
|
403
|
+
if (options.readOnly) return createReadOnlyCacheStore(location, initial.body, options.cwd);
|
|
404
|
+
const dependencies = {
|
|
405
|
+
append: options.appendFile ?? appendFile,
|
|
406
|
+
lock: options.lock ?? { acquire: lockfile.lock },
|
|
407
|
+
write: options.writeFile ?? writeFile
|
|
408
|
+
};
|
|
409
|
+
const trimmed = await trimOnce(location, alintVersion, initial, dependencies);
|
|
410
|
+
const writer = createEventWriter(location, trimmed.metadata, dependencies);
|
|
411
|
+
const fileExists = options.fileExists ?? defaultFileExists;
|
|
286
412
|
return {
|
|
287
|
-
|
|
288
|
-
|
|
413
|
+
beginOwner: (owner, metadata) => beginOwner(trimmed.body, owner, options.cwd, writer, metadata),
|
|
414
|
+
flush: () => writer.flush(false),
|
|
415
|
+
location,
|
|
416
|
+
reconcile: async () => {
|
|
417
|
+
const removedOwners = await collectMissingFileOwners(trimmed.body, options.cwd, fileExists);
|
|
418
|
+
for (const ownerKey of removedOwners) writer.enqueue({
|
|
419
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
420
|
+
ownerKey,
|
|
421
|
+
type: "remove-owner"
|
|
422
|
+
});
|
|
423
|
+
await writer.flush(true);
|
|
424
|
+
}
|
|
289
425
|
};
|
|
290
426
|
}
|
|
291
|
-
function
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
for (const
|
|
295
|
-
const
|
|
296
|
-
|
|
427
|
+
function createTargetIdentityResolver(targets) {
|
|
428
|
+
const baseCounts = /* @__PURE__ */ new Map();
|
|
429
|
+
const duplicateCandidateCounts = /* @__PURE__ */ new Map();
|
|
430
|
+
for (const target of targets) {
|
|
431
|
+
const base = createBaseTargetIdentity(target);
|
|
432
|
+
baseCounts.set(base, (baseCounts.get(base) ?? 0) + 1);
|
|
297
433
|
}
|
|
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}.`);
|
|
434
|
+
for (const target of targets) {
|
|
435
|
+
const base = createBaseTargetIdentity(target);
|
|
436
|
+
if ((baseCounts.get(base) ?? 0) <= 1) continue;
|
|
437
|
+
const candidate = target.range ? `${base}:${target.range.start}:${target.range.end}` : base;
|
|
438
|
+
duplicateCandidateCounts.set(candidate, (duplicateCandidateCounts.get(candidate) ?? 0) + 1);
|
|
338
439
|
}
|
|
339
|
-
return
|
|
440
|
+
return (target, targetIndex) => {
|
|
441
|
+
const base = createBaseTargetIdentity(target);
|
|
442
|
+
if ((baseCounts.get(base) ?? 0) <= 1) return base;
|
|
443
|
+
const candidate = target.range ? `${base}:${target.range.start}:${target.range.end}` : base;
|
|
444
|
+
if ((duplicateCandidateCounts.get(candidate) ?? 0) <= 1) return candidate;
|
|
445
|
+
return `${candidate}:${targetIndex}`;
|
|
446
|
+
};
|
|
340
447
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const registry = createLanguageRegistry$1();
|
|
345
|
-
registerLanguage(registry, textLanguage);
|
|
346
|
-
registerLanguage(registry, javascriptLanguage);
|
|
347
|
-
registerLanguage(registry, typescriptLanguage);
|
|
348
|
-
return registry;
|
|
349
|
-
}
|
|
350
|
-
//#endregion
|
|
351
|
-
//#region src/dsl/registry.ts
|
|
352
|
-
function buildRuleRegistry(config) {
|
|
353
|
-
const rules = /* @__PURE__ */ new Map();
|
|
354
|
-
const localIds = /* @__PURE__ */ new Map();
|
|
355
|
-
const enabledRules = [];
|
|
356
|
-
for (const [alias, plugin] of Object.entries(config.plugins)) for (const [localId, rule] of Object.entries(plugin.rules ?? {})) {
|
|
357
|
-
const id = `${alias}/${localId}`;
|
|
358
|
-
if (rules.has(id)) throw new Error(`Duplicate rule id "${id}".`);
|
|
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
|
-
}
|
|
376
|
-
return {
|
|
377
|
-
enabledRules,
|
|
378
|
-
rules
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
function formatRuleOptionIssues(index, issues) {
|
|
382
|
-
return issues.map((issue) => {
|
|
383
|
-
const path = getDotPath(issue);
|
|
384
|
-
return path === null ? `"${index}": ${issue.message}` : `"${index}.${path}": ${issue.message}`;
|
|
385
|
-
}).join("; ");
|
|
386
|
-
}
|
|
387
|
-
function isRuleConfigTuple(entry) {
|
|
388
|
-
return Array.isArray(entry);
|
|
389
|
-
}
|
|
390
|
-
function normalizeRuleConfigEntry(entry) {
|
|
391
|
-
if (isRuleConfigTuple(entry)) {
|
|
392
|
-
const [severity = "warn", ...options] = entry;
|
|
393
|
-
return {
|
|
394
|
-
options,
|
|
395
|
-
severity
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
return {
|
|
399
|
-
options: [],
|
|
400
|
-
severity: entry ?? "warn"
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
function parseRuleOption(id, index, schema, value) {
|
|
404
|
-
const result = safeParse(schema, value);
|
|
405
|
-
if (result.success) return result.output;
|
|
406
|
-
if (value === void 0) {
|
|
407
|
-
const emptyObjectResult = safeParse(schema, {});
|
|
408
|
-
if (emptyObjectResult.success) return emptyObjectResult.output;
|
|
409
|
-
}
|
|
410
|
-
throw new TypeError(`Invalid options for rule "${id}": ${formatRuleOptionIssues(index, result.issues)}`);
|
|
411
|
-
}
|
|
412
|
-
function parseRuleOptions(id, schema, options) {
|
|
413
|
-
if (!schema) {
|
|
414
|
-
if (options.length > 0) throw new TypeError(`Rule "${id}" does not accept options.`);
|
|
415
|
-
return [];
|
|
416
|
-
}
|
|
417
|
-
if (options.length > schema.length) throw new TypeError(`Invalid options for rule "${id}": Unexpected option at index ${schema.length}.`);
|
|
418
|
-
return schema.map((optionSchema, index) => parseRuleOption(id, index, optionSchema, options[index]));
|
|
419
|
-
}
|
|
420
|
-
//#endregion
|
|
421
|
-
//#region src/core/languages/diagnostics.ts
|
|
422
|
-
/** A rule named a language and nothing registered it. This is a builtin rule. Not a real rule. */
|
|
423
|
-
const MISSING_LANGUAGE_RULE = "alint/missing-language";
|
|
424
|
-
/** A linted file's extension belongs to no registered language. This is a builtin rule. Not a real rule either. */
|
|
425
|
-
const UNREGISTERED_LANGUAGE_RULE = "alint/unregistered-language";
|
|
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
|
-
});
|
|
441
|
-
}
|
|
442
|
-
/** Records one rule that named a language the config never registered, grouped by language id. */
|
|
443
|
-
function recordMissingLanguage(into, languageId, ruleId, path) {
|
|
444
|
-
const existing = into.get(languageId);
|
|
445
|
-
if (existing === void 0) {
|
|
446
|
-
into.set(languageId, {
|
|
447
|
-
filePath: path,
|
|
448
|
-
ruleIds: /* @__PURE__ */ new Set([ruleId])
|
|
449
|
-
});
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
existing.ruleIds.add(ruleId);
|
|
453
|
-
if (path < existing.filePath) existing.filePath = path;
|
|
454
|
-
}
|
|
455
|
-
/**
|
|
456
|
-
* Records one linted file no language claimed, grouped by extension so a missing pack is one
|
|
457
|
-
* diagnostic, not one per file. `'off'` records nothing.
|
|
458
|
-
*/
|
|
459
|
-
function recordUnregistered(into, path, severity) {
|
|
460
|
-
if (severity === "off") return;
|
|
461
|
-
const key = extname(path) || basename(path);
|
|
462
|
-
const existing = into.get(key);
|
|
463
|
-
into.set(key, {
|
|
464
|
-
count: (existing?.count ?? 0) + 1,
|
|
465
|
-
filePath: existing !== void 0 && existing.filePath < path ? existing.filePath : path,
|
|
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);
|
|
505
|
-
}
|
|
506
|
-
function resolveRuleLanguages(languages) {
|
|
507
|
-
if (languages === void 0) return { kind: "off" };
|
|
508
|
-
if (languages === "any") return { kind: "any" };
|
|
509
|
-
if ("ids" in languages) return {
|
|
510
|
-
ids: languages.ids,
|
|
511
|
-
kind: "list",
|
|
512
|
-
skipMissing: languages.skipMissing ?? false
|
|
513
|
-
};
|
|
514
|
-
return {
|
|
515
|
-
ids: languages,
|
|
516
|
-
kind: "list",
|
|
517
|
-
skipMissing: false
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
//#endregion
|
|
521
|
-
//#region src/core/preparation.ts
|
|
522
|
-
/**
|
|
523
|
-
* The `ctx.src.extract` a run hands to rules, for parsing files it was never asked to lint, which
|
|
524
|
-
* an index builder sweeping the workspace needs.
|
|
525
|
-
*
|
|
526
|
-
* It resolves each file's OWN config rather than a caller's, because the language a file resolves to
|
|
527
|
-
* is a config decision and two config groups may register different plugins. It extracts on demand
|
|
528
|
-
* and holds nothing: source planning releases rich extractor values after compact jobs are admitted,
|
|
529
|
-
* and a run-wide parse memo here would put that retention back. A caller keeps only what it derives.
|
|
530
|
-
*
|
|
531
|
-
* `getSrc` defers reading the runtime because the runtime is built around this closure. It is assigned
|
|
532
|
-
* before any rule runs, so by the first call it is present.
|
|
533
|
-
*/
|
|
534
|
-
function createSourceExtractor(cwd, config, getSrc) {
|
|
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
|
-
});
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
function prepareRun(options = {}) {
|
|
551
|
-
const cwd$2 = options.cwd ?? cwd();
|
|
552
|
-
const config = options.config ?? [];
|
|
553
|
-
const files = [];
|
|
554
|
-
const directories = [];
|
|
555
|
-
const missingLanguages = /* @__PURE__ */ new Map();
|
|
556
|
-
const unregisteredLanguages = /* @__PURE__ */ new Map();
|
|
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
|
-
});
|
|
600
|
-
}
|
|
601
|
-
return {
|
|
602
|
-
directories,
|
|
603
|
-
files,
|
|
604
|
-
missingLanguages,
|
|
605
|
-
project: options.projectTargets === false ? void 0 : prepareProject(cwd$2, config),
|
|
606
|
-
unregisteredLanguages
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
function createLanguageRegistry(config) {
|
|
610
|
-
const registry = createBuiltInLanguageRegistry();
|
|
611
|
-
for (const plugin of Object.values(config.plugins)) for (const language of Object.values(plugin.languages ?? {})) registerLanguage(registry, language);
|
|
612
|
-
return registry;
|
|
613
|
-
}
|
|
614
|
-
function prepareProject(root, config) {
|
|
615
|
-
const resolvedConfig = resolveConfigForProject(root, config, { cwd: root });
|
|
616
|
-
if (resolvedConfig.ignored) return void 0;
|
|
617
|
-
const effectiveConfig = resolvedConfig.config;
|
|
618
|
-
return {
|
|
619
|
-
agent: effectiveConfig.agent,
|
|
620
|
-
configHash: stableHash({ settings: effectiveConfig.settings }),
|
|
621
|
-
root,
|
|
622
|
-
rules: prepareRules(effectiveConfig),
|
|
623
|
-
settings: effectiveConfig.settings
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
function prepareRules(config) {
|
|
627
|
-
return buildRuleRegistry(config).enabledRules.map((enabledRule, ruleIndex) => ({
|
|
628
|
-
enabledRule,
|
|
629
|
-
ruleIndex
|
|
630
|
-
}));
|
|
631
|
-
}
|
|
632
|
-
function recordMissingLanguages(into, rules, registry, path) {
|
|
633
|
-
for (const { enabledRule } of rules) {
|
|
634
|
-
const languages = resolveRuleLanguages(enabledRule.rule.languages);
|
|
635
|
-
if (languages.kind !== "list" || languages.skipMissing) continue;
|
|
636
|
-
for (const languageId of languages.ids) if (!registry.languages.has(languageId)) recordMissingLanguage(into, languageId, enabledRule.id, path);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
//#endregion
|
|
640
|
-
//#region package.json
|
|
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("/") || ".";
|
|
448
|
+
function normalizeCachePath(cwd, filePath) {
|
|
449
|
+
const resolvedPath = isAbsolute$1(filePath) ? resolve$1(filePath) : resolve$1(cwd, filePath);
|
|
450
|
+
return relative$1(cwd, resolvedPath).split(sep).join("/") || ".";
|
|
819
451
|
}
|
|
820
452
|
function normalizeRunnerCacheConfig(cache, cwd) {
|
|
821
453
|
if (cache === false) return {
|
|
@@ -1055,231 +687,599 @@ function createReadOnlyCacheStore(location, body, cwd) {
|
|
|
1055
687
|
reconcile: async () => {}
|
|
1056
688
|
};
|
|
1057
689
|
}
|
|
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;
|
|
690
|
+
async function defaultFileExists(path) {
|
|
691
|
+
try {
|
|
692
|
+
await access(path);
|
|
693
|
+
return true;
|
|
694
|
+
} catch (error) {
|
|
695
|
+
if (isMissingFileError(error)) return false;
|
|
696
|
+
throw error;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function emptyLoadedLog(alintVersion) {
|
|
700
|
+
const metadata = {
|
|
701
|
+
alintVersion,
|
|
702
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
703
|
+
magic: CACHE_MAGIC,
|
|
704
|
+
schemaVersion: 2,
|
|
705
|
+
type: "metadata"
|
|
706
|
+
};
|
|
707
|
+
return {
|
|
708
|
+
body: createEmptyCacheBody(metadata.createdAt),
|
|
709
|
+
metadata
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
async function ensureCompatibleMetadata(location, metadata, write) {
|
|
713
|
+
try {
|
|
714
|
+
if (parseMetadataLine(await readMetadataLine(location), metadata.alintVersion)) return;
|
|
715
|
+
} catch (error) {
|
|
716
|
+
if (!isMissingFileError(error)) throw error;
|
|
717
|
+
}
|
|
718
|
+
await write(location, `${JSON.stringify(metadata)}\n`);
|
|
719
|
+
}
|
|
720
|
+
function fingerprintsEqual(left, right) {
|
|
721
|
+
return left.configHash === right.configHash && left.modelHash === right.modelHash && left.ruleHash === right.ruleHash && left.targetHash === right.targetHash;
|
|
722
|
+
}
|
|
723
|
+
function getCacheLockOptions(retries) {
|
|
724
|
+
return {
|
|
725
|
+
realpath: false,
|
|
726
|
+
retries,
|
|
727
|
+
stale: LOCK_STALE_MS,
|
|
728
|
+
update: LOCK_UPDATE_MS
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
function hasJsonArrayProperties(input, ancestors) {
|
|
732
|
+
let arrayLength;
|
|
733
|
+
let indexCount = 0;
|
|
734
|
+
for (const key of Reflect.ownKeys(input)) {
|
|
735
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, key);
|
|
736
|
+
if (!descriptor || !("value" in descriptor)) return false;
|
|
737
|
+
if (key === "length") {
|
|
738
|
+
if (descriptor.enumerable || typeof descriptor.value !== "number") return false;
|
|
739
|
+
arrayLength = descriptor.value;
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const index = typeof key === "string" ? Number(key) : NaN;
|
|
743
|
+
if (typeof key !== "string" || !Number.isInteger(index) || index < 0 || index >= 2 ** 32 - 1 || String(index) !== key || !descriptor.enumerable) return false;
|
|
744
|
+
if (!isJsonValueAt(descriptor.value, ancestors)) return false;
|
|
745
|
+
indexCount += 1;
|
|
746
|
+
}
|
|
747
|
+
return arrayLength !== void 0 && indexCount === arrayLength;
|
|
748
|
+
}
|
|
749
|
+
function hasJsonObjectProperties(input, ancestors) {
|
|
750
|
+
for (const key of Reflect.ownKeys(input)) {
|
|
751
|
+
if (typeof key !== "string") return false;
|
|
752
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, key);
|
|
753
|
+
if (!descriptor || !descriptor.enumerable || !("value" in descriptor)) return false;
|
|
754
|
+
if (!isJsonValueAt(descriptor.value, ancestors)) return false;
|
|
755
|
+
}
|
|
756
|
+
return true;
|
|
757
|
+
}
|
|
758
|
+
function isJsonValue(input) {
|
|
759
|
+
return isJsonValueAt(input, /* @__PURE__ */ new WeakSet());
|
|
760
|
+
}
|
|
761
|
+
function isJsonValueAt(input, ancestors) {
|
|
762
|
+
if (input === null || typeof input === "string" || typeof input === "boolean") return true;
|
|
763
|
+
if (typeof input === "number") return Number.isFinite(input);
|
|
764
|
+
if (typeof input !== "object") return false;
|
|
765
|
+
if (ancestors.has(input)) return false;
|
|
766
|
+
ancestors.add(input);
|
|
767
|
+
try {
|
|
768
|
+
if (Array.isArray(input)) {
|
|
769
|
+
if (Object.getPrototypeOf(input) !== Array.prototype) return false;
|
|
770
|
+
return hasJsonArrayProperties(input, ancestors);
|
|
771
|
+
}
|
|
772
|
+
if (Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null) return false;
|
|
773
|
+
return hasJsonObjectProperties(input, ancestors);
|
|
774
|
+
} catch {
|
|
775
|
+
return false;
|
|
776
|
+
} finally {
|
|
777
|
+
ancestors.delete(input);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
function isMissingFileError(error) {
|
|
781
|
+
return isNodeErrorCode(error, "ENOENT") || isNodeErrorCode(error, "ENOTDIR");
|
|
782
|
+
}
|
|
783
|
+
async function loadCacheLog(location, alintVersion) {
|
|
784
|
+
try {
|
|
785
|
+
const metadata = parseMetadataLine(await readMetadataLine(location), alintVersion);
|
|
786
|
+
if (!metadata) return emptyLoadedLog(alintVersion);
|
|
787
|
+
return replayLog(await readFile(location, "utf8"), metadata);
|
|
788
|
+
} catch (error) {
|
|
789
|
+
if (isMissingFileError(error)) return emptyLoadedLog(alintVersion);
|
|
790
|
+
throw error;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
function ownerEntries(body, owner) {
|
|
794
|
+
const entries = /* @__PURE__ */ new Map();
|
|
795
|
+
for (const slot of owner?.slots ?? []) {
|
|
796
|
+
const cacheEntry = body.entries[slot];
|
|
797
|
+
if (cacheEntry) entries.set(slot, cacheEntry);
|
|
798
|
+
}
|
|
799
|
+
return entries;
|
|
800
|
+
}
|
|
801
|
+
function ownerKey(owner, cwd) {
|
|
802
|
+
return stableHash({
|
|
803
|
+
kind: owner.kind,
|
|
804
|
+
path: normalizeCachePath(cwd, owner.path)
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
function parseMetadataLine(line, expectedAlintVersion) {
|
|
808
|
+
if (!line) return void 0;
|
|
809
|
+
try {
|
|
810
|
+
const value = JSON.parse(line);
|
|
811
|
+
const metadata = parse(CacheMetadataSchema, value);
|
|
812
|
+
if (expectedAlintVersion !== void 0 && metadata.alintVersion !== expectedAlintVersion) return void 0;
|
|
813
|
+
return metadata;
|
|
814
|
+
} catch {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
async function persistCompactedLog(location, loaded, write) {
|
|
819
|
+
const metadata = parse(CacheMetadataSchema, loaded.metadata);
|
|
820
|
+
const events = Object.entries(loaded.body.owners).map(([ownerKey, owner]) => ({
|
|
821
|
+
at: loaded.body.updatedAt,
|
|
822
|
+
entries: [...ownerEntries(loaded.body, owner)].map(([slotKey, entry]) => ({
|
|
823
|
+
entry,
|
|
824
|
+
slotKey
|
|
825
|
+
})),
|
|
826
|
+
mode: "replace",
|
|
827
|
+
owner,
|
|
828
|
+
ownerKey,
|
|
829
|
+
type: "replace-owner"
|
|
830
|
+
})).map((event) => parse(CacheEventSchema, event));
|
|
831
|
+
const contents = [`${JSON.stringify(metadata)}\n`, ...events.map((event) => `${JSON.stringify(event)}\n`)].join("");
|
|
832
|
+
const tempPath = join(dirname(location), `.${basename$1(location)}.${process.pid}.${randomUUID()}.tmp`);
|
|
833
|
+
try {
|
|
834
|
+
await write(tempPath, contents);
|
|
835
|
+
await rename(tempPath, location);
|
|
836
|
+
} catch (error) {
|
|
837
|
+
await rm(tempPath, { force: true }).catch(() => {});
|
|
838
|
+
throw error;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
async function readMetadataLine(location) {
|
|
842
|
+
const handle = await open(location, "r");
|
|
843
|
+
try {
|
|
844
|
+
const buffer = Buffer.alloc(METADATA_READ_LIMIT);
|
|
845
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
846
|
+
const newlineIndex = buffer.subarray(0, bytesRead).indexOf(10);
|
|
847
|
+
if (newlineIndex < 0) return void 0;
|
|
848
|
+
const lineEnd = newlineIndex > 0 && buffer[newlineIndex - 1] === 13 ? newlineIndex - 1 : newlineIndex;
|
|
849
|
+
return buffer.toString("utf8", 0, lineEnd);
|
|
850
|
+
} finally {
|
|
851
|
+
await handle.close();
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
function removeOwner(body, key) {
|
|
855
|
+
for (const slot of body.owners[key]?.slots ?? []) delete body.entries[slot];
|
|
856
|
+
delete body.owners[key];
|
|
857
|
+
}
|
|
858
|
+
function replayLog(text, metadata) {
|
|
859
|
+
const body = createEmptyCacheBody(metadata.createdAt);
|
|
860
|
+
const lines = text.split(/\r?\n/);
|
|
861
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
862
|
+
const line = lines[index];
|
|
863
|
+
if (!line) continue;
|
|
864
|
+
try {
|
|
865
|
+
const value = JSON.parse(line);
|
|
866
|
+
applyEvent(body, parse(CacheEventSchema, value));
|
|
867
|
+
} catch {}
|
|
868
|
+
}
|
|
869
|
+
return {
|
|
870
|
+
body: parse(CacheFileBodySchema, body),
|
|
871
|
+
metadata
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
function slotKey(owner, slot, cwd) {
|
|
875
|
+
return stableHash({
|
|
876
|
+
owner: ownerKey(owner, cwd),
|
|
877
|
+
...slot
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
async function trimOnce(location, alintVersion, fallback, dependencies) {
|
|
881
|
+
await mkdir(dirname(location), { recursive: true });
|
|
882
|
+
try {
|
|
883
|
+
return await withCacheLock(location, dependencies.lock, getCacheLockOptions(0), async () => {
|
|
884
|
+
const latest = await loadCacheLog(location, alintVersion);
|
|
885
|
+
await persistCompactedLog(location, latest, dependencies.write);
|
|
886
|
+
return latest;
|
|
887
|
+
});
|
|
888
|
+
} catch (error) {
|
|
889
|
+
if (isNodeErrorCode(error, "ELOCKED")) return fallback;
|
|
890
|
+
throw error;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
async function withCacheLock(location, lock, options, operation) {
|
|
894
|
+
let compromised;
|
|
895
|
+
const release = await lock.acquire(location, {
|
|
896
|
+
...options,
|
|
897
|
+
onCompromised: (error) => {
|
|
898
|
+
compromised = error;
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
let operationError;
|
|
902
|
+
try {
|
|
903
|
+
const result = await operation();
|
|
904
|
+
if (compromised) throw compromised;
|
|
905
|
+
return result;
|
|
906
|
+
} catch (error) {
|
|
907
|
+
operationError = error;
|
|
908
|
+
throw error;
|
|
909
|
+
} finally {
|
|
910
|
+
try {
|
|
911
|
+
await release();
|
|
912
|
+
} catch (error) {
|
|
913
|
+
if (operationError === void 0) throw error;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
//#endregion
|
|
918
|
+
//#region src/core/languages/js/index.ts
|
|
919
|
+
const javascriptLanguage = {
|
|
920
|
+
extensions: [
|
|
921
|
+
".cjs",
|
|
922
|
+
".js",
|
|
923
|
+
".jsx",
|
|
924
|
+
".mjs"
|
|
925
|
+
],
|
|
926
|
+
extract: (file) => extractJsSourceTargets(withLanguage(file, "javascript")),
|
|
927
|
+
name: "javascript"
|
|
928
|
+
};
|
|
929
|
+
//#endregion
|
|
930
|
+
//#region src/core/languages/registry.ts
|
|
931
|
+
function createLanguageRegistry$1() {
|
|
932
|
+
return {
|
|
933
|
+
byExtension: /* @__PURE__ */ new Map(),
|
|
934
|
+
languages: /* @__PURE__ */ new Map()
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
function registerLanguage(registry, language) {
|
|
938
|
+
const existing = registry.languages.get(language.name);
|
|
939
|
+
if (existing && existing !== language) throw new Error(`Duplicate language "${language.name}".`);
|
|
940
|
+
for (const extension of language.extensions ?? []) {
|
|
941
|
+
const existingOwner = registry.byExtension.get(extension);
|
|
942
|
+
if (existingOwner && existingOwner !== language.name) throw new Error(`Duplicate language extension "${extension}".`);
|
|
1065
943
|
}
|
|
944
|
+
registry.languages.set(language.name, language);
|
|
945
|
+
for (const extension of language.extensions ?? []) registry.byExtension.set(extension, language.name);
|
|
1066
946
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
947
|
+
//#endregion
|
|
948
|
+
//#region src/core/languages/text.ts
|
|
949
|
+
const textLanguage = {
|
|
950
|
+
extensions: [],
|
|
951
|
+
extract: (file) => [{
|
|
952
|
+
file,
|
|
953
|
+
identity: "file",
|
|
954
|
+
kind: "file",
|
|
955
|
+
language: "plaintext",
|
|
956
|
+
origin: { physicalPath: file.path },
|
|
957
|
+
text: file.text
|
|
958
|
+
}],
|
|
959
|
+
name: "plaintext"
|
|
960
|
+
};
|
|
961
|
+
//#endregion
|
|
962
|
+
//#region src/core/languages/ts/index.ts
|
|
963
|
+
const typescriptLanguage = {
|
|
964
|
+
extensions: [
|
|
965
|
+
".cts",
|
|
966
|
+
".mts",
|
|
967
|
+
".ts",
|
|
968
|
+
".tsx"
|
|
969
|
+
],
|
|
970
|
+
extract: (file) => extractJsSourceTargets(withLanguage(file, "typescript")),
|
|
971
|
+
name: "typescript"
|
|
972
|
+
};
|
|
973
|
+
//#endregion
|
|
974
|
+
//#region src/core/languages/resolve.ts
|
|
975
|
+
function resolveLanguage(file, registry, options) {
|
|
976
|
+
return resolveLanguageForPath(file.path, registry, options);
|
|
977
|
+
}
|
|
978
|
+
function resolveLanguageForPath(filePath, registry, options) {
|
|
979
|
+
const languageName = options.language ?? options.processedLanguage ?? registry.byExtension.get(extname$1(filePath)) ?? "plaintext";
|
|
980
|
+
const language = registry.languages.get(languageName);
|
|
981
|
+
if (!language) {
|
|
982
|
+
const registered = [...registry.languages.keys()].sort().join(", ");
|
|
983
|
+
throw new Error(`Unknown language "${languageName}". Languages come from plugins, and these are registered: ${registered}.`);
|
|
984
|
+
}
|
|
985
|
+
return language;
|
|
986
|
+
}
|
|
987
|
+
//#endregion
|
|
988
|
+
//#region src/core/languages/index.ts
|
|
989
|
+
function createBuiltInLanguageRegistry() {
|
|
990
|
+
const registry = createLanguageRegistry$1();
|
|
991
|
+
registerLanguage(registry, textLanguage);
|
|
992
|
+
registerLanguage(registry, javascriptLanguage);
|
|
993
|
+
registerLanguage(registry, typescriptLanguage);
|
|
994
|
+
return registry;
|
|
995
|
+
}
|
|
996
|
+
//#endregion
|
|
997
|
+
//#region src/dsl/registry.ts
|
|
998
|
+
function buildRuleRegistry(config) {
|
|
999
|
+
const rules = /* @__PURE__ */ new Map();
|
|
1000
|
+
const localIds = /* @__PURE__ */ new Map();
|
|
1001
|
+
const enabledRules = [];
|
|
1002
|
+
for (const [alias, plugin] of Object.entries(config.plugins)) for (const [localId, rule] of Object.entries(plugin.rules ?? {})) {
|
|
1003
|
+
const id = `${alias}/${localId}`;
|
|
1004
|
+
if (rules.has(id)) throw new Error(`Duplicate rule id "${id}".`);
|
|
1005
|
+
rules.set(id, rule);
|
|
1006
|
+
localIds.set(id, localId);
|
|
1007
|
+
}
|
|
1008
|
+
for (const [id, entry] of Object.entries(config.rules)) {
|
|
1009
|
+
const rule = rules.get(id);
|
|
1010
|
+
if (!rule) throw new Error(`Unknown rule "${id}".`);
|
|
1011
|
+
const normalizedEntry = normalizeRuleConfigEntry(entry);
|
|
1012
|
+
const severity = normalizedEntry.severity;
|
|
1013
|
+
if (severity === "off") continue;
|
|
1014
|
+
enabledRules.push({
|
|
1015
|
+
id,
|
|
1016
|
+
localId: localIds.get(id) ?? id,
|
|
1017
|
+
options: parseRuleOptions(id, rule.options, normalizedEntry.options),
|
|
1018
|
+
rule,
|
|
1019
|
+
severity
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1075
1022
|
return {
|
|
1076
|
-
|
|
1077
|
-
|
|
1023
|
+
enabledRules,
|
|
1024
|
+
rules
|
|
1078
1025
|
};
|
|
1079
1026
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
}
|
|
1086
|
-
await write(location, `${JSON.stringify(metadata)}\n`);
|
|
1027
|
+
function formatRuleOptionIssues(index, issues) {
|
|
1028
|
+
return issues.map((issue) => {
|
|
1029
|
+
const path = getDotPath(issue);
|
|
1030
|
+
return path === null ? `"${index}": ${issue.message}` : `"${index}.${path}": ${issue.message}`;
|
|
1031
|
+
}).join("; ");
|
|
1087
1032
|
}
|
|
1088
|
-
function
|
|
1089
|
-
return
|
|
1033
|
+
function isRuleConfigTuple(entry) {
|
|
1034
|
+
return Array.isArray(entry);
|
|
1090
1035
|
}
|
|
1091
|
-
function
|
|
1036
|
+
function normalizeRuleConfigEntry(entry) {
|
|
1037
|
+
if (isRuleConfigTuple(entry)) {
|
|
1038
|
+
const [severity = "warn", ...options] = entry;
|
|
1039
|
+
return {
|
|
1040
|
+
options,
|
|
1041
|
+
severity
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1092
1044
|
return {
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
stale: LOCK_STALE_MS,
|
|
1096
|
-
update: LOCK_UPDATE_MS
|
|
1045
|
+
options: [],
|
|
1046
|
+
severity: entry ?? "warn"
|
|
1097
1047
|
};
|
|
1098
1048
|
}
|
|
1099
|
-
function
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
const
|
|
1104
|
-
if (
|
|
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;
|
|
1049
|
+
function parseRuleOption(id, index, schema, value) {
|
|
1050
|
+
const result = safeParse(schema, value);
|
|
1051
|
+
if (result.success) return result.output;
|
|
1052
|
+
if (value === void 0) {
|
|
1053
|
+
const emptyObjectResult = safeParse(schema, {});
|
|
1054
|
+
if (emptyObjectResult.success) return emptyObjectResult.output;
|
|
1114
1055
|
}
|
|
1115
|
-
|
|
1056
|
+
throw new TypeError(`Invalid options for rule "${id}": ${formatRuleOptionIssues(index, result.issues)}`);
|
|
1116
1057
|
}
|
|
1117
|
-
function
|
|
1118
|
-
|
|
1119
|
-
if (
|
|
1120
|
-
|
|
1121
|
-
if (!descriptor || !descriptor.enumerable || !("value" in descriptor)) return false;
|
|
1122
|
-
if (!isJsonValueAt(descriptor.value, ancestors)) return false;
|
|
1058
|
+
function parseRuleOptions(id, schema, options) {
|
|
1059
|
+
if (!schema) {
|
|
1060
|
+
if (options.length > 0) throw new TypeError(`Rule "${id}" does not accept options.`);
|
|
1061
|
+
return [];
|
|
1123
1062
|
}
|
|
1124
|
-
|
|
1063
|
+
if (options.length > schema.length) throw new TypeError(`Invalid options for rule "${id}": Unexpected option at index ${schema.length}.`);
|
|
1064
|
+
return schema.map((optionSchema, index) => parseRuleOption(id, index, optionSchema, options[index]));
|
|
1125
1065
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1066
|
+
//#endregion
|
|
1067
|
+
//#region src/core/languages/diagnostics.ts
|
|
1068
|
+
/** A rule named a language and nothing registered it. This is a builtin rule. Not a real rule. */
|
|
1069
|
+
const MISSING_LANGUAGE_RULE = "alint/missing-language";
|
|
1070
|
+
/** A linted file's extension belongs to no registered language. This is a builtin rule. Not a real rule either. */
|
|
1071
|
+
const UNREGISTERED_LANGUAGE_RULE = "alint/unregistered-language";
|
|
1072
|
+
/**
|
|
1073
|
+
* One diagnostic per missing language, always an error: a rule scoped to a language nothing provides
|
|
1074
|
+
* matches no file, so leaving it at a warning would let a run pass having silently checked nothing.
|
|
1075
|
+
*/
|
|
1076
|
+
function missingLanguageDiagnostics(missing) {
|
|
1077
|
+
return [...missing.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([languageId, entry]) => {
|
|
1078
|
+
const ruleIds = [...entry.ruleIds].sort();
|
|
1079
|
+
const subject = ruleIds.length === 1 ? `Rule "${ruleIds[0]}" needs` : `Rules ${ruleIds.map((id) => `"${id}"`).join(", ")} need`;
|
|
1080
|
+
return {
|
|
1081
|
+
filePath: entry.filePath,
|
|
1082
|
+
message: `${subject} the "${languageId}" language, which no plugin registered. Add a plugin that provides "${languageId}", or turn the rule off.`,
|
|
1083
|
+
ruleId: MISSING_LANGUAGE_RULE,
|
|
1084
|
+
severity: "error"
|
|
1085
|
+
};
|
|
1086
|
+
});
|
|
1128
1087
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
if (
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
return hasJsonArrayProperties(input, ancestors);
|
|
1139
|
-
}
|
|
1140
|
-
if (Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null) return false;
|
|
1141
|
-
return hasJsonObjectProperties(input, ancestors);
|
|
1142
|
-
} catch {
|
|
1143
|
-
return false;
|
|
1144
|
-
} finally {
|
|
1145
|
-
ancestors.delete(input);
|
|
1088
|
+
/** Records one rule that named a language the config never registered, grouped by language id. */
|
|
1089
|
+
function recordMissingLanguage(into, languageId, ruleId, path) {
|
|
1090
|
+
const existing = into.get(languageId);
|
|
1091
|
+
if (existing === void 0) {
|
|
1092
|
+
into.set(languageId, {
|
|
1093
|
+
filePath: path,
|
|
1094
|
+
ruleIds: /* @__PURE__ */ new Set([ruleId])
|
|
1095
|
+
});
|
|
1096
|
+
return;
|
|
1146
1097
|
}
|
|
1098
|
+
existing.ruleIds.add(ruleId);
|
|
1099
|
+
if (path < existing.filePath) existing.filePath = path;
|
|
1147
1100
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1101
|
+
/**
|
|
1102
|
+
* Records one linted file no language claimed, grouped by extension so a missing pack is one
|
|
1103
|
+
* diagnostic, not one per file. `'off'` records nothing.
|
|
1104
|
+
*/
|
|
1105
|
+
function recordUnregistered(into, path, severity) {
|
|
1106
|
+
if (severity === "off") return;
|
|
1107
|
+
const key = extname(path) || basename(path);
|
|
1108
|
+
const existing = into.get(key);
|
|
1109
|
+
into.set(key, {
|
|
1110
|
+
count: (existing?.count ?? 0) + 1,
|
|
1111
|
+
filePath: existing !== void 0 && existing.filePath < path ? existing.filePath : path,
|
|
1112
|
+
severity: existing?.severity === "error" ? "error" : severity
|
|
1113
|
+
});
|
|
1150
1114
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1115
|
+
/** One diagnostic per extension, sorted, for the run to prepend to its results. */
|
|
1116
|
+
function unregisteredLanguageDiagnostics(unregistered) {
|
|
1117
|
+
return [...unregistered.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([extension, entry]) => ({
|
|
1118
|
+
filePath: entry.filePath,
|
|
1119
|
+
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.`,
|
|
1120
|
+
ruleId: UNREGISTERED_LANGUAGE_RULE,
|
|
1121
|
+
severity: entry.severity
|
|
1122
|
+
}));
|
|
1123
|
+
}
|
|
1124
|
+
function unregisteredLanguageSeverity(linterOptions) {
|
|
1125
|
+
const value = linterOptions.reportUnregisteredLanguages;
|
|
1126
|
+
return value === "error" || value === "off" ? value : "warn";
|
|
1160
1127
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1128
|
+
//#endregion
|
|
1129
|
+
//#region src/core/languages/rule-languages.ts
|
|
1130
|
+
/**
|
|
1131
|
+
* The language a file falls back to when no registered language claims its extension. Not a real
|
|
1132
|
+
* language, which is why `'any'` excludes it.
|
|
1133
|
+
*/
|
|
1134
|
+
const PLAIN_TEXT = "plaintext";
|
|
1135
|
+
/**
|
|
1136
|
+
* Whether a rule reads this target.
|
|
1137
|
+
*
|
|
1138
|
+
* A rule with language off still reads file targets, because reading raw text, or asking a model about a
|
|
1139
|
+
* whole file, needs no language. However, it never reads the function and class targets extraction produces.
|
|
1140
|
+
* `'file'` is the one kind every language yields, plain text included, so it is the dividing line.
|
|
1141
|
+
*
|
|
1142
|
+
* `'any'` means any real language, so a file that fell back to plain text because nothing claimed
|
|
1143
|
+
* its extension is withheld: the rule asked for structure and there is none to give. The run says so
|
|
1144
|
+
* once per extension through `alint/unregistered-language` rather than letting each rule report an
|
|
1145
|
+
* empty file.
|
|
1146
|
+
*/
|
|
1147
|
+
function isTargetLanguageAccepted(languages, targetKind, targetLanguage) {
|
|
1148
|
+
if (languages.kind === "off") return targetKind === "file";
|
|
1149
|
+
if (languages.kind === "any") return targetLanguage !== PLAIN_TEXT;
|
|
1150
|
+
return languages.ids.includes(targetLanguage);
|
|
1168
1151
|
}
|
|
1169
|
-
function
|
|
1170
|
-
return
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1152
|
+
function resolveRuleLanguages(languages) {
|
|
1153
|
+
if (languages === void 0) return { kind: "off" };
|
|
1154
|
+
if (languages === "any") return { kind: "any" };
|
|
1155
|
+
if ("ids" in languages) return {
|
|
1156
|
+
ids: languages.ids,
|
|
1157
|
+
kind: "list",
|
|
1158
|
+
skipMissing: languages.skipMissing ?? false
|
|
1159
|
+
};
|
|
1160
|
+
return {
|
|
1161
|
+
ids: languages,
|
|
1162
|
+
kind: "list",
|
|
1163
|
+
skipMissing: false
|
|
1164
|
+
};
|
|
1174
1165
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1166
|
+
//#endregion
|
|
1167
|
+
//#region src/core/preparation.ts
|
|
1168
|
+
/**
|
|
1169
|
+
* The `ctx.src.extract` a run hands to rules, for parsing files it was never asked to lint, which
|
|
1170
|
+
* an index builder sweeping the workspace needs.
|
|
1171
|
+
*
|
|
1172
|
+
* It resolves each file's OWN config rather than a caller's, because the language a file resolves to
|
|
1173
|
+
* is a config decision and two config groups may register different plugins. It extracts on demand
|
|
1174
|
+
* and holds nothing: source planning releases rich extractor values after compact jobs are admitted,
|
|
1175
|
+
* and a run-wide parse memo here would put that retention back. A caller keeps only what it derives.
|
|
1176
|
+
*
|
|
1177
|
+
* `getSrc` defers reading the runtime because the runtime is built around this closure. It is assigned
|
|
1178
|
+
* before any rule runs, so by the first call it is present.
|
|
1179
|
+
*/
|
|
1180
|
+
function createSourceExtractor(cwd, config, getSrc) {
|
|
1181
|
+
return async (filePath, options = {}) => {
|
|
1182
|
+
const path = resolve(cwd, filePath);
|
|
1183
|
+
const resolvedConfig = resolveConfigForFile(path, config, { cwd });
|
|
1184
|
+
if (resolvedConfig.ignored) return [];
|
|
1185
|
+
const effectiveConfig = resolvedConfig.config;
|
|
1186
|
+
const language = resolveLanguageForPath(path, createLanguageRegistry(effectiveConfig), { language: options.language ?? effectiveConfig.language });
|
|
1187
|
+
const src = getSrc();
|
|
1188
|
+
const file = await src.readFile(path);
|
|
1189
|
+
return language.extract(file, {
|
|
1190
|
+
cwd,
|
|
1191
|
+
languageOptions: effectiveConfig.languageOptions,
|
|
1192
|
+
src
|
|
1193
|
+
});
|
|
1194
|
+
};
|
|
1185
1195
|
}
|
|
1186
|
-
|
|
1187
|
-
const
|
|
1188
|
-
const
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1196
|
+
function prepareRun(options = {}) {
|
|
1197
|
+
const cwd$2 = options.cwd ?? cwd();
|
|
1198
|
+
const config = options.config ?? [];
|
|
1199
|
+
const files = [];
|
|
1200
|
+
const directories = [];
|
|
1201
|
+
const missingLanguages = /* @__PURE__ */ new Map();
|
|
1202
|
+
const unregisteredLanguages = /* @__PURE__ */ new Map();
|
|
1203
|
+
for (const filePath of options.files ?? []) {
|
|
1204
|
+
const path = resolve(cwd$2, filePath);
|
|
1205
|
+
const resolvedConfig = resolveConfigForFile(path, config, { cwd: cwd$2 });
|
|
1206
|
+
if (resolvedConfig.ignored) continue;
|
|
1207
|
+
const effectiveConfig = resolvedConfig.config;
|
|
1208
|
+
const languageRegistry = createLanguageRegistry(effectiveConfig);
|
|
1209
|
+
const language = resolveLanguageForPath(path, languageRegistry, { language: effectiveConfig.language });
|
|
1210
|
+
const rules = prepareRules(effectiveConfig);
|
|
1211
|
+
recordMissingLanguages(missingLanguages, rules, languageRegistry, path);
|
|
1212
|
+
if (effectiveConfig.language === void 0 && language.name === "plaintext" && rules.some(({ enabledRule }) => !isTargetLanguageAccepted(resolveRuleLanguages(enabledRule.rule.languages), "file", "plaintext"))) recordUnregistered(unregisteredLanguages, path, unregisteredLanguageSeverity(effectiveConfig.linterOptions));
|
|
1213
|
+
files.push({
|
|
1214
|
+
agent: effectiveConfig.agent,
|
|
1215
|
+
configHash: stableHash({
|
|
1216
|
+
language: effectiveConfig.language,
|
|
1217
|
+
languageOptions: effectiveConfig.languageOptions,
|
|
1218
|
+
processor: effectiveConfig.processor,
|
|
1219
|
+
resolvedLanguage: language.name,
|
|
1220
|
+
settings: effectiveConfig.settings
|
|
1221
|
+
}),
|
|
1222
|
+
fileIndex: files.length,
|
|
1223
|
+
language,
|
|
1224
|
+
languageOptions: effectiveConfig.languageOptions,
|
|
1225
|
+
path,
|
|
1226
|
+
rules,
|
|
1227
|
+
settings: effectiveConfig.settings
|
|
1228
|
+
});
|
|
1207
1229
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1230
|
+
for (const directoryPath of options.directories ?? []) {
|
|
1231
|
+
const path = resolve(cwd$2, directoryPath);
|
|
1232
|
+
const resolvedConfig = resolveConfigForDirectory(path, config, { cwd: cwd$2 });
|
|
1233
|
+
if (resolvedConfig.ignored) continue;
|
|
1234
|
+
const effectiveConfig = resolvedConfig.config;
|
|
1235
|
+
directories.push({
|
|
1236
|
+
agent: effectiveConfig.agent,
|
|
1237
|
+
configHash: stableHash({ settings: effectiveConfig.settings }),
|
|
1238
|
+
directoryIndex: directories.length,
|
|
1239
|
+
rules: prepareRules(effectiveConfig),
|
|
1240
|
+
settings: effectiveConfig.settings,
|
|
1241
|
+
target: {
|
|
1242
|
+
kind: "directory",
|
|
1243
|
+
path
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1220
1246
|
}
|
|
1247
|
+
return {
|
|
1248
|
+
directories,
|
|
1249
|
+
files,
|
|
1250
|
+
missingLanguages,
|
|
1251
|
+
project: options.projectTargets === false ? void 0 : prepareProject(cwd$2, config),
|
|
1252
|
+
unregisteredLanguages
|
|
1253
|
+
};
|
|
1221
1254
|
}
|
|
1222
|
-
function
|
|
1223
|
-
|
|
1224
|
-
|
|
1255
|
+
function createLanguageRegistry(config) {
|
|
1256
|
+
const registry = createBuiltInLanguageRegistry();
|
|
1257
|
+
for (const plugin of Object.values(config.plugins)) for (const language of Object.values(plugin.languages ?? {})) registerLanguage(registry, language);
|
|
1258
|
+
return registry;
|
|
1225
1259
|
}
|
|
1226
|
-
function
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
const line = lines[index];
|
|
1231
|
-
if (!line) continue;
|
|
1232
|
-
try {
|
|
1233
|
-
const value = JSON.parse(line);
|
|
1234
|
-
applyEvent(body, parse(CacheEventSchema, value));
|
|
1235
|
-
} catch {}
|
|
1236
|
-
}
|
|
1260
|
+
function prepareProject(root, config) {
|
|
1261
|
+
const resolvedConfig = resolveConfigForProject(root, config, { cwd: root });
|
|
1262
|
+
if (resolvedConfig.ignored) return void 0;
|
|
1263
|
+
const effectiveConfig = resolvedConfig.config;
|
|
1237
1264
|
return {
|
|
1238
|
-
|
|
1239
|
-
|
|
1265
|
+
agent: effectiveConfig.agent,
|
|
1266
|
+
configHash: stableHash({ settings: effectiveConfig.settings }),
|
|
1267
|
+
root,
|
|
1268
|
+
rules: prepareRules(effectiveConfig),
|
|
1269
|
+
settings: effectiveConfig.settings
|
|
1240
1270
|
};
|
|
1241
1271
|
}
|
|
1242
|
-
function
|
|
1243
|
-
return
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
});
|
|
1247
|
-
}
|
|
1248
|
-
async function trimOnce(location, alintVersion, fallback, dependencies) {
|
|
1249
|
-
await mkdir(dirname(location), { recursive: true });
|
|
1250
|
-
try {
|
|
1251
|
-
return await withCacheLock(location, dependencies.lock, getCacheLockOptions(0), async () => {
|
|
1252
|
-
const latest = await loadCacheLog(location, alintVersion);
|
|
1253
|
-
await persistCompactedLog(location, latest, dependencies.write);
|
|
1254
|
-
return latest;
|
|
1255
|
-
});
|
|
1256
|
-
} catch (error) {
|
|
1257
|
-
if (isNodeErrorCode(error, "ELOCKED")) return fallback;
|
|
1258
|
-
throw error;
|
|
1259
|
-
}
|
|
1272
|
+
function prepareRules(config) {
|
|
1273
|
+
return buildRuleRegistry(config).enabledRules.map((enabledRule, ruleIndex) => ({
|
|
1274
|
+
enabledRule,
|
|
1275
|
+
ruleIndex
|
|
1276
|
+
}));
|
|
1260
1277
|
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
compromised = error;
|
|
1267
|
-
}
|
|
1268
|
-
});
|
|
1269
|
-
let operationError;
|
|
1270
|
-
try {
|
|
1271
|
-
const result = await operation();
|
|
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
|
-
}
|
|
1278
|
+
function recordMissingLanguages(into, rules, registry, path) {
|
|
1279
|
+
for (const { enabledRule } of rules) {
|
|
1280
|
+
const languages = resolveRuleLanguages(enabledRule.rule.languages);
|
|
1281
|
+
if (languages.kind !== "list" || languages.skipMissing) continue;
|
|
1282
|
+
for (const languageId of languages.ids) if (!registry.languages.has(languageId)) recordMissingLanguage(into, languageId, enabledRule.id, path);
|
|
1283
1283
|
}
|
|
1284
1284
|
}
|
|
1285
1285
|
//#endregion
|
|
@@ -3120,6 +3120,7 @@ async function measureModel({ model, prompt, signal }) {
|
|
|
3120
3120
|
}];
|
|
3121
3121
|
const startedAt = performance.now();
|
|
3122
3122
|
let firstOutputAt;
|
|
3123
|
+
let outputDeltaCount = 0;
|
|
3123
3124
|
const result = streamText({
|
|
3124
3125
|
...model.params,
|
|
3125
3126
|
abortSignal: signal,
|
|
@@ -3139,7 +3140,10 @@ async function measureModel({ model, prompt, signal }) {
|
|
|
3139
3140
|
]);
|
|
3140
3141
|
let settled;
|
|
3141
3142
|
try {
|
|
3142
|
-
for await (const event of result.eventStream) if (
|
|
3143
|
+
for await (const event of result.eventStream) if ((event.type === "reasoning.delta" || event.type === "text.delta") && event.delta.length > 0) {
|
|
3144
|
+
outputDeltaCount += 1;
|
|
3145
|
+
firstOutputAt ??= performance.now();
|
|
3146
|
+
}
|
|
3143
3147
|
} finally {
|
|
3144
3148
|
settled = await completion;
|
|
3145
3149
|
}
|
|
@@ -3148,7 +3152,7 @@ async function measureModel({ model, prompt, signal }) {
|
|
|
3148
3152
|
if (usageResult.status === "rejected") throw usageResult.reason;
|
|
3149
3153
|
return {
|
|
3150
3154
|
durationMs: endedAt - startedAt,
|
|
3151
|
-
firstOutputMs: firstOutputAt === void 0 ? void 0 : firstOutputAt - startedAt,
|
|
3155
|
+
firstOutputMs: firstOutputAt === void 0 || outputDeltaCount < 2 ? void 0 : firstOutputAt - startedAt,
|
|
3152
3156
|
outputTokens: usageResult.value?.outputTokens
|
|
3153
3157
|
};
|
|
3154
3158
|
}
|
|
@@ -3245,4 +3249,4 @@ function sampleCorpus(seed) {
|
|
|
3245
3249
|
].map((section) => `--- corpus section ${section} ---\n${rotated}`).join("\n\n");
|
|
3246
3250
|
}
|
|
3247
3251
|
//#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 };
|
|
3252
|
+
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 };
|