@alint-js/core 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +9 -0
- package/dist/index.mjs +140 -87
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -203,6 +203,15 @@ interface RunOptions {
|
|
|
203
203
|
* Defaults to `true`. Pass `false` for runs that target partial subsets of a project.
|
|
204
204
|
*/
|
|
205
205
|
projectTargets?: boolean;
|
|
206
|
+
/**
|
|
207
|
+
* Only run rules whose configured id (`plugin/rule`) or own name (`rule`) matches one of these
|
|
208
|
+
* glob patterns. Everything else stays unplanned, so a filtered run neither calls its model nor
|
|
209
|
+
* reports its diagnostics.
|
|
210
|
+
*
|
|
211
|
+
* A filter that matches no enabled rule fails the run, because a silent empty run is
|
|
212
|
+
* indistinguishable from a clean one. Pass the patterns you typed on the command line.
|
|
213
|
+
*/
|
|
214
|
+
ruleFilter?: readonly string[];
|
|
206
215
|
runner?: RunnerOptions;
|
|
207
216
|
setupConfig?: SetupConfig;
|
|
208
217
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -14,6 +14,7 @@ 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");
|
|
@@ -270,7 +271,7 @@ function mergeStopGateConfig(resolved, item) {
|
|
|
270
271
|
}
|
|
271
272
|
//#endregion
|
|
272
273
|
//#region package.json
|
|
273
|
-
var version = "0.7.
|
|
274
|
+
var version = "0.7.2";
|
|
274
275
|
//#endregion
|
|
275
276
|
//#region src/core/cache/types.ts
|
|
276
277
|
const CACHE_MAGIC = "ALINT_CACHE";
|
|
@@ -1164,6 +1165,19 @@ function resolveRuleLanguages(languages) {
|
|
|
1164
1165
|
};
|
|
1165
1166
|
}
|
|
1166
1167
|
//#endregion
|
|
1168
|
+
//#region src/core/rule-filter.ts
|
|
1169
|
+
const minimatchOptions = { dot: true };
|
|
1170
|
+
/**
|
|
1171
|
+
* Whether one enabled rule survives a `--rule` filter.
|
|
1172
|
+
*
|
|
1173
|
+
* A pattern matches the configured rule id (`plugin/rule`) or the rule's own name
|
|
1174
|
+
* (`rule`), so `--rule docs/review-copy` narrows a run to a single rule while
|
|
1175
|
+
* `--rule 'docs/*'` keeps a plugin's rules. Patterns are matched with glob syntax.
|
|
1176
|
+
*/
|
|
1177
|
+
function matchesRuleFilter(rule, patterns) {
|
|
1178
|
+
return patterns.some((pattern) => minimatch(rule.id, pattern, minimatchOptions) || minimatch(rule.localId, pattern, minimatchOptions));
|
|
1179
|
+
}
|
|
1180
|
+
//#endregion
|
|
1167
1181
|
//#region src/core/preparation.ts
|
|
1168
1182
|
/**
|
|
1169
1183
|
* The `ctx.src.extract` a run hands to rules, for parsing files it was never asked to lint, which
|
|
@@ -1196,10 +1210,17 @@ function createSourceExtractor(cwd, config, getSrc) {
|
|
|
1196
1210
|
function prepareRun(options = {}) {
|
|
1197
1211
|
const cwd$2 = options.cwd ?? cwd();
|
|
1198
1212
|
const config = options.config ?? [];
|
|
1213
|
+
const ruleFilter = options.ruleFilter ?? [];
|
|
1199
1214
|
const files = [];
|
|
1200
1215
|
const directories = [];
|
|
1216
|
+
const knownRuleIds = /* @__PURE__ */ new Set();
|
|
1201
1217
|
const missingLanguages = /* @__PURE__ */ new Map();
|
|
1202
1218
|
const unregisteredLanguages = /* @__PURE__ */ new Map();
|
|
1219
|
+
const prepareRulesFor = (effectiveConfig) => {
|
|
1220
|
+
const enabledRules = buildRuleRegistry(effectiveConfig).enabledRules;
|
|
1221
|
+
for (const enabledRule of enabledRules) knownRuleIds.add(enabledRule.id);
|
|
1222
|
+
return prepareRules(enabledRules, ruleFilter);
|
|
1223
|
+
};
|
|
1203
1224
|
for (const filePath of options.files ?? []) {
|
|
1204
1225
|
const path = resolve(cwd$2, filePath);
|
|
1205
1226
|
const resolvedConfig = resolveConfigForFile(path, config, { cwd: cwd$2 });
|
|
@@ -1207,7 +1228,7 @@ function prepareRun(options = {}) {
|
|
|
1207
1228
|
const effectiveConfig = resolvedConfig.config;
|
|
1208
1229
|
const languageRegistry = createLanguageRegistry(effectiveConfig);
|
|
1209
1230
|
const language = resolveLanguageForPath(path, languageRegistry, { language: effectiveConfig.language });
|
|
1210
|
-
const rules =
|
|
1231
|
+
const rules = prepareRulesFor(effectiveConfig);
|
|
1211
1232
|
recordMissingLanguages(missingLanguages, rules, languageRegistry, path);
|
|
1212
1233
|
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
1234
|
files.push({
|
|
@@ -1236,7 +1257,7 @@ function prepareRun(options = {}) {
|
|
|
1236
1257
|
agent: effectiveConfig.agent,
|
|
1237
1258
|
configHash: stableHash({ settings: effectiveConfig.settings }),
|
|
1238
1259
|
directoryIndex: directories.length,
|
|
1239
|
-
rules:
|
|
1260
|
+
rules: prepareRulesFor(effectiveConfig),
|
|
1240
1261
|
settings: effectiveConfig.settings,
|
|
1241
1262
|
target: {
|
|
1242
1263
|
kind: "directory",
|
|
@@ -1244,11 +1265,13 @@ function prepareRun(options = {}) {
|
|
|
1244
1265
|
}
|
|
1245
1266
|
});
|
|
1246
1267
|
}
|
|
1268
|
+
const project = options.projectTargets === false ? void 0 : prepareProject(cwd$2, config, prepareRulesFor);
|
|
1269
|
+
if (ruleFilter.length > 0 && !hasPreparedRule(files, directories, project)) throw new Error(formatUnmatchedRuleFilter(ruleFilter, knownRuleIds));
|
|
1247
1270
|
return {
|
|
1248
1271
|
directories,
|
|
1249
1272
|
files,
|
|
1250
1273
|
missingLanguages,
|
|
1251
|
-
project
|
|
1274
|
+
project,
|
|
1252
1275
|
unregisteredLanguages
|
|
1253
1276
|
};
|
|
1254
1277
|
}
|
|
@@ -1257,7 +1280,15 @@ function createLanguageRegistry(config) {
|
|
|
1257
1280
|
for (const plugin of Object.values(config.plugins)) for (const language of Object.values(plugin.languages ?? {})) registerLanguage(registry, language);
|
|
1258
1281
|
return registry;
|
|
1259
1282
|
}
|
|
1260
|
-
function
|
|
1283
|
+
function formatUnmatchedRuleFilter(ruleFilter, knownRuleIds) {
|
|
1284
|
+
const patterns = ruleFilter.map((pattern) => `"${pattern}"`).join(", ");
|
|
1285
|
+
const knownIds = [...knownRuleIds].sort();
|
|
1286
|
+
return `No enabled rule matched --rule ${patterns}. ${knownIds.length === 0 ? "This run enabled no rule at all. Run `alint config inspect <file>` to see which rules apply to a file." : `Enabled rules: ${knownIds.slice(0, 20).join(", ")}${knownIds.length > 20 ? ", ..." : ""}.`}`;
|
|
1287
|
+
}
|
|
1288
|
+
function hasPreparedRule(files, directories, project) {
|
|
1289
|
+
return files.some((input) => input.rules.length > 0) || directories.some((input) => input.rules.length > 0) || (project?.rules.length ?? 0) > 0;
|
|
1290
|
+
}
|
|
1291
|
+
function prepareProject(root, config, prepareRulesFor) {
|
|
1261
1292
|
const resolvedConfig = resolveConfigForProject(root, config, { cwd: root });
|
|
1262
1293
|
if (resolvedConfig.ignored) return void 0;
|
|
1263
1294
|
const effectiveConfig = resolvedConfig.config;
|
|
@@ -1265,15 +1296,19 @@ function prepareProject(root, config) {
|
|
|
1265
1296
|
agent: effectiveConfig.agent,
|
|
1266
1297
|
configHash: stableHash({ settings: effectiveConfig.settings }),
|
|
1267
1298
|
root,
|
|
1268
|
-
rules:
|
|
1299
|
+
rules: prepareRulesFor(effectiveConfig),
|
|
1269
1300
|
settings: effectiveConfig.settings
|
|
1270
1301
|
};
|
|
1271
1302
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1303
|
+
/**
|
|
1304
|
+
* Keeps the registry position of every surviving rule: `ruleIndex` is an enabled-registry
|
|
1305
|
+
* position that jobs and progress refs share, so filtering must not renumber the rules that stay.
|
|
1306
|
+
*/
|
|
1307
|
+
function prepareRules(enabledRules, ruleFilter) {
|
|
1308
|
+
return enabledRules.map((enabledRule, ruleIndex) => ({
|
|
1274
1309
|
enabledRule,
|
|
1275
1310
|
ruleIndex
|
|
1276
|
-
}));
|
|
1311
|
+
})).filter((rule) => ruleFilter.length === 0 || matchesRuleFilter(rule.enabledRule, ruleFilter));
|
|
1277
1312
|
}
|
|
1278
1313
|
function recordMissingLanguages(into, rules, registry, path) {
|
|
1279
1314
|
for (const { enabledRule } of rules) {
|
|
@@ -2363,88 +2398,14 @@ function projectExecution(runtime) {
|
|
|
2363
2398
|
//#endregion
|
|
2364
2399
|
//#region src/core/source/planner.ts
|
|
2365
2400
|
/**
|
|
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.
|
|
2401
|
+
* Starts every input read independently, then batches CPU-heavy extraction and job admission.
|
|
2442
2402
|
*/
|
|
2443
2403
|
async function planSources(inputs, options) {
|
|
2444
|
-
|
|
2404
|
+
const waitForPlanning = createPlanningAdmission();
|
|
2405
|
+
return Promise.all(inputs.map((input) => planSourceWithAdmission(input, {
|
|
2445
2406
|
...options,
|
|
2446
2407
|
ruleRuntimes: options.createRuleRuntimes(input)
|
|
2447
|
-
})));
|
|
2408
|
+
}, waitForPlanning)));
|
|
2448
2409
|
}
|
|
2449
2410
|
function createExecutionTargets(input, targets, file, runtimes, cwd, cacheOwner) {
|
|
2450
2411
|
const resolveIdentity = createTargetIdentityResolver(targets.map((target) => ({
|
|
@@ -2499,6 +2460,28 @@ function createExecutionTargets(input, targets, file, runtimes, cwd, cacheOwner)
|
|
|
2499
2460
|
};
|
|
2500
2461
|
});
|
|
2501
2462
|
}
|
|
2463
|
+
function createPlanningAdmission() {
|
|
2464
|
+
const batchSize = 32;
|
|
2465
|
+
let admitted = 0;
|
|
2466
|
+
let pendingYield;
|
|
2467
|
+
return async () => {
|
|
2468
|
+
let currentPendingYield = pendingYield;
|
|
2469
|
+
while (currentPendingYield) {
|
|
2470
|
+
await currentPendingYield;
|
|
2471
|
+
currentPendingYield = pendingYield;
|
|
2472
|
+
}
|
|
2473
|
+
admitted += 1;
|
|
2474
|
+
if (admitted < batchSize) return;
|
|
2475
|
+
admitted = 0;
|
|
2476
|
+
const currentYield = setImmediate();
|
|
2477
|
+
pendingYield = currentYield;
|
|
2478
|
+
try {
|
|
2479
|
+
await currentYield;
|
|
2480
|
+
} finally {
|
|
2481
|
+
if (pendingYield === currentYield) pendingYield = void 0;
|
|
2482
|
+
}
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2502
2485
|
function createProjectSnapshot(input, language, path, contentHash, targets) {
|
|
2503
2486
|
return {
|
|
2504
2487
|
configHash: input.configHash,
|
|
@@ -2570,6 +2553,76 @@ function fileFailure(input, kind, message) {
|
|
|
2570
2553
|
message
|
|
2571
2554
|
};
|
|
2572
2555
|
}
|
|
2556
|
+
async function planSourceWithAdmission(input, options, waitForPlanning) {
|
|
2557
|
+
if (options.signal?.aborted) return { outcomes: Promise.resolve([]) };
|
|
2558
|
+
let file;
|
|
2559
|
+
try {
|
|
2560
|
+
file = await options.src.readFile(input.path);
|
|
2561
|
+
} catch (error) {
|
|
2562
|
+
reportFilePlanningComplete(input, 0, options);
|
|
2563
|
+
return {
|
|
2564
|
+
failure: fileFailure(input, "read", failureMessage(error, "Failed to read source file.")),
|
|
2565
|
+
outcomes: Promise.resolve([])
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
if (options.signal?.aborted) return { outcomes: Promise.resolve([]) };
|
|
2569
|
+
await waitForPlanning?.();
|
|
2570
|
+
if (options.signal?.aborted) return { outcomes: Promise.resolve([]) };
|
|
2571
|
+
let targets;
|
|
2572
|
+
try {
|
|
2573
|
+
targets = await input.language.extract(file, {
|
|
2574
|
+
cwd: options.cwd,
|
|
2575
|
+
languageOptions: input.languageOptions,
|
|
2576
|
+
src: options.src
|
|
2577
|
+
});
|
|
2578
|
+
} catch (error) {
|
|
2579
|
+
reportFilePlanningComplete(input, 0, options);
|
|
2580
|
+
return {
|
|
2581
|
+
failure: fileFailure(input, "extract", failureMessage(error, "Failed to extract source targets.")),
|
|
2582
|
+
outcomes: Promise.resolve([])
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
const { contentHash } = file;
|
|
2586
|
+
let cacheOwner;
|
|
2587
|
+
let jobs;
|
|
2588
|
+
let project;
|
|
2589
|
+
try {
|
|
2590
|
+
project = options.projectSnapshots === false ? void 0 : createProjectSnapshot(input, file.language, file.path, contentHash, targets);
|
|
2591
|
+
cacheOwner = options.cacheStore.beginOwner({
|
|
2592
|
+
kind: "file",
|
|
2593
|
+
path: file.path
|
|
2594
|
+
}, { contentHash });
|
|
2595
|
+
const baseFile = {
|
|
2596
|
+
contentHash,
|
|
2597
|
+
language: file.language,
|
|
2598
|
+
path: file.path
|
|
2599
|
+
};
|
|
2600
|
+
jobs = createSourceJobs(input, createExecutionTargets(input, targets, baseFile, options.ruleRuntimes, options.cwd, cacheOwner));
|
|
2601
|
+
} catch (error) {
|
|
2602
|
+
reportFilePlanningComplete(input, 0, options);
|
|
2603
|
+
return {
|
|
2604
|
+
failure: fileFailure(input, "extract", failureMessage(error, "Failed to plan source targets.")),
|
|
2605
|
+
outcomes: Promise.resolve([])
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2608
|
+
if (options.signal?.aborted) {
|
|
2609
|
+
cacheOwner.commit({ mode: "merge" });
|
|
2610
|
+
return {
|
|
2611
|
+
outcomes: Promise.resolve([]),
|
|
2612
|
+
project
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2615
|
+
const batch = options.scheduler.schedule(jobs);
|
|
2616
|
+
reportFilePlanningComplete(input, batch.jobsAdded, options);
|
|
2617
|
+
return {
|
|
2618
|
+
outcomes: batch.outcomes.then(async (settled) => {
|
|
2619
|
+
cacheOwner.commit(options.signal?.aborted ? { mode: "merge" } : void 0);
|
|
2620
|
+
await options.cacheStore.flush();
|
|
2621
|
+
return settled;
|
|
2622
|
+
}),
|
|
2623
|
+
project
|
|
2624
|
+
};
|
|
2625
|
+
}
|
|
2573
2626
|
function reportFilePlanningComplete(input, jobsAdded, options) {
|
|
2574
2627
|
try {
|
|
2575
2628
|
const progress = options.scheduler.completeFilePlanning();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alint-js/core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.2",
|
|
5
5
|
"description": "Public rule and plugin SDK and run engine for alint",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"pathe": "^2.0.3",
|
|
57
57
|
"proper-lockfile": "4.1.2",
|
|
58
58
|
"valibot": "^1.4.2",
|
|
59
|
-
"@alint-js/utils": "0.7.
|
|
59
|
+
"@alint-js/utils": "0.7.2"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/proper-lockfile": "4.1.4",
|