@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 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.0";
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 = prepareRules(effectiveConfig);
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: prepareRules(effectiveConfig),
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: options.projectTargets === false ? void 0 : prepareProject(cwd$2, config),
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 prepareProject(root, config) {
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: prepareRules(effectiveConfig),
1299
+ rules: prepareRulesFor(effectiveConfig),
1269
1300
  settings: effectiveConfig.settings
1270
1301
  };
1271
1302
  }
1272
- function prepareRules(config) {
1273
- return buildRuleRegistry(config).enabledRules.map((enabledRule, ruleIndex) => ({
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
- * Converts one rich extractor result into detached descriptors and admits its compact jobs.
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
- return Promise.all(inputs.map((input) => planSource(input, {
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.0",
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.0"
59
+ "@alint-js/utils": "0.7.2"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/proper-lockfile": "4.1.4",