@alint-js/core 0.0.5 → 0.0.6

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.mjs CHANGED
@@ -1,270 +1,214 @@
1
+ import { minimatch } from "minimatch";
2
+ import { isAbsolute, relative, resolve } from "pathe";
3
+ import { parseSync } from "oxc-parser";
4
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
5
+ import { dirname, extname, isAbsolute as isAbsolute$1, join, relative as relative$1, resolve as resolve$1, sep } from "node:path";
6
+ import { clamp } from "es-toolkit";
1
7
  import { AsyncLocalStorage } from "node:async_hooks";
2
8
  import process, { cwd } from "node:process";
3
9
  import { errorCauseFrom, errorMessageFrom } from "@moeru/std/error";
4
- import { resolve } from "pathe";
5
10
  import { createHash, randomUUID } from "node:crypto";
6
11
  import { statSync } from "node:fs";
7
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
8
- import { dirname, extname, isAbsolute, join, relative, resolve as resolve$1, sep } from "node:path";
9
12
  import { array, check, description, is, literal, number, object, optional, pipe, record, string, union, unknown } from "valibot";
10
- import { parseSync } from "oxc-parser";
11
- import { clamp } from "es-toolkit";
12
- //#region package.json
13
- var version = "0.0.5";
14
- //#endregion
15
- //#region src/dsl/registry.ts
16
- function buildRuleRegistry(config) {
17
- const rules = /* @__PURE__ */ new Map();
18
- const enabledRules = [];
19
- for (const plugin of config.plugins ?? []) for (const [localId, rule] of Object.entries(plugin.rules)) {
20
- const id = `${plugin.scope}/${localId}`;
21
- if (rules.has(id)) throw new Error(`Duplicate rule id "${id}".`);
22
- rules.set(id, rule);
23
- const severity = normalizeSeverity(config.rules?.[id]);
24
- if (severity !== "off") enabledRules.push({
25
- id,
26
- localId,
27
- rule,
28
- scope: plugin.scope,
29
- severity
30
- });
13
+ //#region src/config/config-array.ts
14
+ const effectiveBasePathSymbol = Symbol("effectiveBasePath");
15
+ const inheritedMatcherScopesSymbol = Symbol("inheritedMatcherScopes");
16
+ function hasDiscoveryFilePatterns(input) {
17
+ return expandConfig(input).some(hasDiscoveryMatcher);
18
+ }
19
+ function matchesDiscoveryFile(filePath, input, options) {
20
+ const result = resolveConfigForFile(filePath, input, options);
21
+ if (result.ignored) return false;
22
+ return result.matched.some(hasDiscoveryMatcher);
23
+ }
24
+ function normalizeConfig(input) {
25
+ return normalizeConfigItems(input, []);
26
+ }
27
+ function resolveConfigForFile(filePath, input, options) {
28
+ const items = expandConfig(input);
29
+ const matched = [];
30
+ const skipped = [];
31
+ const config = createEmptyConfig();
32
+ for (const item of items) {
33
+ if (isGlobalIgnoreItem(item)) {
34
+ if (matchesInheritedScopes(filePath, item, options.cwd) && matchesAny(filePath, item.ignores ?? [], options.cwd, getEffectiveBasePath(item))) return {
35
+ config: createEmptyConfig(),
36
+ ignored: true,
37
+ matched: [],
38
+ skipped
39
+ };
40
+ skipped.push({
41
+ item,
42
+ reason: "global ignores did not match"
43
+ });
44
+ continue;
45
+ }
46
+ if (!matchesConfigItem(filePath, item, options.cwd)) {
47
+ skipped.push({
48
+ item,
49
+ reason: "files or ignores did not match"
50
+ });
51
+ continue;
52
+ }
53
+ matched.push(item);
54
+ mergeConfig(config, item);
31
55
  }
32
- for (const id of Object.keys(config.rules ?? {})) if (!rules.has(id)) throw new Error(`Unknown rule "${id}".`);
33
56
  return {
34
- enabledRules,
35
- rules
57
+ config,
58
+ ignored: false,
59
+ matched,
60
+ skipped
36
61
  };
37
62
  }
38
- function normalizeSeverity(entry) {
39
- if (Array.isArray(entry)) return entry[0] ?? "warn";
40
- return entry ?? "warn";
41
- }
42
- //#endregion
43
- //#region src/models/resolve.ts
44
- function resolveModel(registry, options = {}) {
45
- const candidates = flattenModels(registry);
46
- const ruleId = options.ruleId ?? "<unknown>";
47
- if (options.request !== void 0) {
48
- const request = options.request;
49
- const candidate = candidates.find(({ model }) => matchesRequest(model, request));
50
- if (candidate === void 0) throw new Error(`Unknown model "${request}".`);
51
- if (!satisfiesHardRequirements(candidate.model, options.requirement)) throw new Error(`Model "${request}" does not satisfy requirement for rule "${ruleId}".`);
52
- return toResolvedModel(candidate, options.requirement);
53
- }
54
- const candidate = preferSize(candidates.filter(({ model }) => satisfiesHardRequirements(model, options.requirement)), options.requirement);
55
- if (candidate === void 0) throw new Error(`No model satisfies requirement for rule "${ruleId}".`);
56
- return toResolvedModel(candidate, options.requirement);
57
- }
58
- function flattenModels(registry) {
59
- return registry.providers.flatMap((provider) => provider.models.map((model) => ({
60
- model,
61
- provider
62
- })));
63
- }
64
- function matchesRequest(model, request) {
65
- return model.id === request || model.name === request || (model.aliases ?? []).includes(request);
66
- }
67
- function preferSize(candidates, requirement) {
68
- if (requirement?.size === void 0) return candidates[0];
69
- return candidates.find(({ model }) => model.size === requirement.size) ?? candidates[0];
70
- }
71
- function satisfiesHardRequirements(model, requirement) {
72
- if (requirement === void 0) return true;
73
- if (requirement.capabilities !== void 0 && !requirement.capabilities.every((capability) => (model.capabilities ?? []).includes(capability))) return false;
74
- if (requirement.minContextWindow !== void 0 && (model.contextWindow === void 0 || model.contextWindow < requirement.minContextWindow)) return false;
75
- return true;
76
- }
77
- function toResolvedModel(candidate, requirement) {
78
- const { model, provider } = candidate;
63
+ function createEmptyConfig() {
79
64
  return {
80
- aliases: [...model.aliases ?? []],
81
- capabilities: [...model.capabilities ?? []],
82
- contextWindow: model.contextWindow,
83
- id: model.id,
84
- name: model.name ?? model.id,
85
- params: {
86
- ...model.defaultParams ?? {},
87
- ...requirement?.params ?? {}
88
- },
89
- provider: {
90
- endpoint: provider.endpoint,
91
- headers: { ...provider.headers ?? {} },
92
- id: provider.id,
93
- type: provider.type
94
- },
95
- size: model.size
65
+ languageOptions: {},
66
+ linterOptions: {},
67
+ plugins: {},
68
+ rules: {},
69
+ settings: {}
96
70
  };
97
71
  }
98
- //#endregion
99
- //#region src/core/cache.ts
100
- const CACHE_SCHEMA_VERSION = 1;
101
- const DEFAULT_CACHE_FILE_NAME = ".alintcache";
102
- const CacheFileSchema = pipe(object({
103
- createdAt: pipe(string(), description("Cache file creation timestamp.")),
104
- entries: pipe(unknown(), check((value) => typeof value === "object" && value !== null && !Array.isArray(value)), record(string(), object({
105
- diagnostics: pipe(array(object({
106
- filePath: pipe(string(), description("Diagnostic file path.")),
107
- message: pipe(string(), description("Diagnostic message.")),
108
- ruleId: pipe(string(), description("Diagnostic rule id.")),
109
- severity: pipe(union([literal("error"), literal("warn")]), description("Diagnostic severity."))
110
- })), description("Diagnostics produced for the cached target.")),
111
- filePath: pipe(string(), description("Cache entry file path.")),
112
- fingerprint: pipe(object({
113
- alintVersion: pipe(string(), description("Alint version used to create the cache entry.")),
114
- configHash: pipe(string(), description("Runner config hash used to create the cache entry.")),
115
- modelHash: pipe(string(), description("Model configuration hash used to create the cache entry.")),
116
- ruleHash: pipe(string(), description("Rule configuration hash used to create the cache entry."))
117
- }), description("Cache entry fingerprint.")),
118
- target: pipe(object({
119
- hash: pipe(string(), description("Cached target hash.")),
120
- identity: pipe(string(), description("Stable cached target identity.")),
121
- kind: pipe(union([
122
- literal("file"),
123
- literal("class"),
124
- literal("function")
125
- ]), description("Cached target kind."))
126
- }), description("Cached target metadata.")),
127
- usage: pipe(array(object({
128
- inputTokens: pipe(optional(number()), description("Optional input token count.")),
129
- modelId: pipe(string(), description("Usage model id.")),
130
- outputTokens: pipe(optional(number()), description("Optional output token count.")),
131
- providerId: pipe(string(), description("Usage provider id.")),
132
- ruleId: pipe(string(), description("Usage rule id.")),
133
- totalTokens: pipe(optional(number()), description("Optional total token count."))
134
- })), description("Inference usage records for the cache entry."))
135
- })), description("Cache entries keyed by cache key.")),
136
- files: pipe(unknown(), check((value) => typeof value === "object" && value !== null && !Array.isArray(value)), record(string(), object({
137
- contentHash: pipe(string(), description("Cached file content hash.")),
138
- entries: pipe(array(string()), description("Cache entry keys associated with the file.")),
139
- path: pipe(string(), description("Normalized cached file path."))
140
- })), description("Cached files keyed by normalized file path.")),
141
- schemaVersion: pipe(literal(CACHE_SCHEMA_VERSION), description("Cache schema version.")),
142
- updatedAt: pipe(string(), description("Cache file update timestamp."))
143
- }), description("Alint cache file."));
144
- function createCacheKey(input) {
145
- return stableHash(input);
146
- }
147
- async function createCacheStore(options) {
148
- const location = resolveCacheLocation(options.cwd, options.location);
149
- if (!options.enabled) return createNoopCacheStore(location);
150
- const cacheFile = await readCacheFile(location);
72
+ function createMatcherScope(item, basePath) {
73
+ if (!item.files && !item.ignores) return;
151
74
  return {
152
- get: (key) => cacheFile.entries[key],
153
- location,
154
- markFile: (filePath, contentHash, entries) => {
155
- const path = normalizeCachePath(options.cwd, filePath);
156
- cacheFile.files[path] = {
157
- contentHash,
158
- entries,
159
- path
160
- };
161
- },
162
- reconcile: async () => {
163
- await mkdir(dirname(location), { recursive: true });
164
- cacheFile.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
165
- const tempPath = join(dirname(location), `.${DEFAULT_CACHE_FILE_NAME}.${process.pid}.${randomUUID()}.tmp`);
166
- await writeFile(tempPath, `${JSON.stringify(cacheFile, null, 2)}\n`);
167
- await rename(tempPath, location);
168
- },
169
- set: (key, entry) => {
170
- cacheFile.entries[key] = entry;
171
- }
75
+ basePath,
76
+ files: item.files,
77
+ ignores: item.ignores
172
78
  };
173
79
  }
174
- function createTargetIdentityResolver(targets) {
175
- const baseCounts = /* @__PURE__ */ new Map();
176
- for (const target of targets) {
177
- const base = createBaseTargetIdentity(target);
178
- baseCounts.set(base, (baseCounts.get(base) ?? 0) + 1);
80
+ function expandConfig(input) {
81
+ return expandExtends(normalizeConfig(input), {
82
+ inheritedBasePath: void 0,
83
+ inheritedMatcherScopes: [],
84
+ objectStack: [],
85
+ plugins: {},
86
+ stringStack: []
87
+ });
88
+ }
89
+ function expandExtends(items, state) {
90
+ const expanded = [];
91
+ for (const item of items) {
92
+ if (state.objectStack.includes(item)) throw new Error("Circular inline config extends.");
93
+ const effectiveBasePath = item.basePath ?? state.inheritedBasePath;
94
+ const itemObjectStack = [...state.objectStack, item];
95
+ const itemMatcherScope = createMatcherScope(item, effectiveBasePath);
96
+ const childMatcherScopes = itemMatcherScope ? [...state.inheritedMatcherScopes, itemMatcherScope] : state.inheritedMatcherScopes;
97
+ const plugins = {
98
+ ...state.plugins,
99
+ ...item.plugins
100
+ };
101
+ for (const extension of item.extends ?? []) if (typeof extension === "string") expanded.push(...resolvePluginConfig(extension, {
102
+ inheritedBasePath: effectiveBasePath,
103
+ inheritedMatcherScopes: childMatcherScopes,
104
+ objectStack: itemObjectStack,
105
+ plugins,
106
+ stringStack: state.stringStack
107
+ }));
108
+ else {
109
+ if (state.objectStack.includes(extension)) throw new Error("Circular inline config extends.");
110
+ expanded.push(...expandExtends(normalizeConfig([extension]), {
111
+ inheritedBasePath: effectiveBasePath,
112
+ inheritedMatcherScopes: childMatcherScopes,
113
+ objectStack: itemObjectStack,
114
+ plugins,
115
+ stringStack: state.stringStack
116
+ }));
117
+ }
118
+ expanded.push(withExpansionMetadata({
119
+ ...item,
120
+ extends: void 0
121
+ }, state.inheritedMatcherScopes, effectiveBasePath));
179
122
  }
180
- return (target) => {
181
- const base = createBaseTargetIdentity(target);
182
- if ((baseCounts.get(base) ?? 0) <= 1) return base;
183
- if (target.range) return `${base}:${target.range.start}:${target.range.end}`;
184
- return base;
185
- };
123
+ return expanded;
186
124
  }
187
- function hashText(text) {
188
- return createHash("sha256").update(text).digest("hex");
125
+ function getEffectiveBasePath(item) {
126
+ return item[effectiveBasePathSymbol] ?? item.basePath;
189
127
  }
190
- function normalizeCachePath(cwd, filePath) {
191
- return relative(cwd, isAbsolute(filePath) ? resolve$1(filePath) : resolve$1(cwd, filePath)).split(sep).join("/");
128
+ function getInheritedMatcherScopes(item) {
129
+ return item[inheritedMatcherScopesSymbol] ?? [];
192
130
  }
193
- function normalizeRunnerCacheConfig(cache, cwd) {
194
- if (cache === false) return {
195
- enabled: false,
196
- location: resolveCacheLocation(cwd)
197
- };
198
- if (cache === true || cache === void 0) return {
199
- enabled: true,
200
- location: resolveCacheLocation(cwd)
201
- };
202
- return {
203
- enabled: cache.enabled ?? true,
204
- location: resolveCacheLocation(cwd, cache.location)
205
- };
131
+ function hasDiscoveryMatcher(item) {
132
+ return hasPositiveFilePattern(item.files) || getInheritedMatcherScopes(item).some((scope) => hasPositiveFilePattern(scope.files));
206
133
  }
207
- function resolveCacheLocation(cwd, location) {
208
- if (!location) return join(cwd, DEFAULT_CACHE_FILE_NAME);
209
- const resolved = isAbsolute(location) ? resolve$1(location) : resolve$1(cwd, location);
210
- if (location.endsWith("/") || location.endsWith("\\")) return join(resolved, DEFAULT_CACHE_FILE_NAME);
211
- try {
212
- if (statSyncIsDirectory(resolved)) return join(resolved, DEFAULT_CACHE_FILE_NAME);
213
- } catch {}
214
- return resolved;
134
+ function hasPositiveFilePattern(files) {
135
+ return files?.some((pattern) => isPatternList(pattern) ? pattern.some(isPositivePattern) : isPositivePattern(pattern)) ?? false;
215
136
  }
216
- function stableHash(value) {
217
- return hashText(stableStringify(value));
137
+ function isConfigArrayInput(input) {
138
+ return Array.isArray(input);
218
139
  }
219
- function createBaseTargetIdentity(target) {
220
- if (target.kind === "file") return target.filePath ? `file:${target.filePath}` : "file";
221
- if (target.name) return `${target.kind}:${target.name}`;
222
- if (target.range) return `${target.kind}:${target.range.start}:${target.range.end}`;
223
- return target.kind;
140
+ function isGlobalIgnoreItem(item) {
141
+ const keys = Object.keys(item).filter((key) => item[key] !== void 0);
142
+ return item.ignores !== void 0 && keys.every((key) => key === "ignores" || key === "name");
224
143
  }
225
- function createEmptyCacheFile() {
226
- const now = (/* @__PURE__ */ new Date()).toISOString();
227
- return {
228
- createdAt: now,
229
- entries: {},
230
- files: {},
231
- schemaVersion: CACHE_SCHEMA_VERSION,
232
- updatedAt: now
233
- };
144
+ function isPatternList(pattern) {
145
+ return Array.isArray(pattern);
234
146
  }
235
- function createNoopCacheStore(location) {
236
- return {
237
- get: () => void 0,
238
- location,
239
- markFile: () => {},
240
- reconcile: async () => {},
241
- set: () => {}
242
- };
147
+ function isPositivePattern(pattern) {
148
+ return !pattern.startsWith("!");
243
149
  }
244
- function isCacheFile(value) {
245
- return is(CacheFileSchema, value);
150
+ function matchesAny(filePath, patterns, cwd, basePath) {
151
+ return patterns.some((pattern) => matchesPattern(filePath, pattern, cwd, basePath));
246
152
  }
247
- function isRecord(value) {
248
- return typeof value === "object" && value !== null && !Array.isArray(value);
153
+ function matchesConfigItem(filePath, item, cwd) {
154
+ const basePath = getEffectiveBasePath(item);
155
+ if (!matchesInheritedScopes(filePath, item, cwd)) return false;
156
+ if (matchesAny(filePath, item.ignores ?? [], cwd, basePath)) return false;
157
+ if (!item.files || item.files.length === 0) return true;
158
+ return item.files.some((pattern) => matchesPattern(filePath, pattern, cwd, basePath));
249
159
  }
250
- async function readCacheFile(location) {
251
- try {
252
- const parsed = JSON.parse(await readFile(location, "utf8"));
253
- if (isCacheFile(parsed)) return parsed;
254
- } catch {}
255
- return createEmptyCacheFile();
160
+ function matchesInheritedScopes(filePath, item, cwd) {
161
+ return getInheritedMatcherScopes(item).every((scope) => {
162
+ if (matchesAny(filePath, scope.ignores ?? [], cwd, scope.basePath)) return false;
163
+ if (!scope.files || scope.files.length === 0) return true;
164
+ return scope.files.some((pattern) => matchesPattern(filePath, pattern, cwd, scope.basePath));
165
+ });
256
166
  }
257
- function stableStringify(value) {
258
- if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(",")}]`;
259
- if (isRecord(value)) return `{${Object.keys(value).sort().map((key) => {
260
- const property = value[key];
261
- if (property === void 0) return;
262
- return `${JSON.stringify(key)}:${stableStringify(property)}`;
263
- }).filter((entry) => entry !== void 0).join(",")}}`;
264
- return JSON.stringify(value);
167
+ function matchesPattern(filePath, pattern, cwd, basePath) {
168
+ if (isPatternList(pattern)) return pattern.every((entry) => matchesPattern(filePath, entry, cwd, basePath));
169
+ const relativePath = relative(basePath ? resolve(cwd, basePath) : cwd, isAbsolute(filePath) ? filePath : resolve(cwd, filePath)).replaceAll("\\", "/");
170
+ if (relativePath === ".." || relativePath.startsWith("../")) return false;
171
+ return minimatch(relativePath, pattern, { dot: true });
172
+ }
173
+ function mergeConfig(config, item) {
174
+ if (item.plugins) for (const [alias, plugin] of Object.entries(item.plugins)) {
175
+ const existingPlugin = config.plugins[alias];
176
+ if (existingPlugin && existingPlugin !== plugin) throw new Error(`Duplicate plugin alias "${alias}".`);
177
+ config.plugins[alias] = plugin;
178
+ }
179
+ Object.assign(config.rules, item.rules);
180
+ Object.assign(config.settings, item.settings);
181
+ Object.assign(config.languageOptions, item.languageOptions);
182
+ Object.assign(config.linterOptions, item.linterOptions);
183
+ if (item.language !== void 0) config.language = item.language;
184
+ if (item.processor !== void 0) config.processor = item.processor;
185
+ if (item.runner !== void 0) config.runner = {
186
+ ...config.runner,
187
+ ...item.runner
188
+ };
265
189
  }
266
- function statSyncIsDirectory(path) {
267
- return statSync(path).isDirectory();
190
+ function normalizeConfigItems(input, arrayStack) {
191
+ if (arrayStack.includes(input)) throw new Error("Circular config array.");
192
+ return input.flatMap((item) => isConfigArrayInput(item) ? normalizeConfigItems(item, [...arrayStack, input]) : [item]);
193
+ }
194
+ function resolvePluginConfig(reference, state) {
195
+ if (state.stringStack.includes(reference)) throw new Error(`Circular config extends: ${[...state.stringStack, reference].join(" -> ")}`);
196
+ const separator = reference.indexOf("/");
197
+ const alias = reference.slice(0, separator);
198
+ const name = reference.slice(separator + 1);
199
+ const config = state.plugins[alias]?.configs?.[name];
200
+ if (!config) throw new Error(`Unknown config "${reference}".`);
201
+ return expandExtends(normalizeConfig([config]), {
202
+ ...state,
203
+ stringStack: [...state.stringStack, reference]
204
+ });
205
+ }
206
+ function withExpansionMetadata(item, inheritedMatcherScopes, effectiveBasePath) {
207
+ Object.defineProperties(item, {
208
+ [effectiveBasePathSymbol]: { value: effectiveBasePath },
209
+ [inheritedMatcherScopesSymbol]: { value: inheritedMatcherScopes }
210
+ });
211
+ return item;
268
212
  }
269
213
  //#endregion
270
214
  //#region src/core/source/runtime.ts
@@ -278,12 +222,15 @@ function createSourceFile(path, text) {
278
222
  }
279
223
  function createSourceRuntime() {
280
224
  return {
281
- getText: (target) => target.text,
225
+ getText,
282
226
  readFile: async (filePath) => createSourceFile(filePath, await readFile(filePath, "utf8")),
283
227
  sliceLines,
284
228
  sliceRange
285
229
  };
286
230
  }
231
+ function getText(target) {
232
+ return target.text;
233
+ }
287
234
  function sliceLines(file, range) {
288
235
  const startLine = clampLine(range.startLine, file.lines.length);
289
236
  const endLine = clampLine(range.endLine, file.lines.length);
@@ -361,41 +308,38 @@ function inferLanguage(path) {
361
308
  }
362
309
  }
363
310
  //#endregion
364
- //#region src/core/source/js.ts
365
- function extractJsSourceUnits(file) {
311
+ //#region src/core/languages/js/extract.ts
312
+ function extractJsSourceTargets(file) {
366
313
  const result = parseSync(file.path, file.text, { sourceType: "module" });
367
314
  const state = {
368
315
  bindingNodes: /* @__PURE__ */ new Map(),
369
- classes: [],
370
316
  exportedNodes: /* @__PURE__ */ new Set(),
371
317
  file,
372
- functions: [],
373
318
  inferredNames: /* @__PURE__ */ new Map(),
374
319
  seenClasses: /* @__PURE__ */ new Set(),
375
320
  seenFunctions: /* @__PURE__ */ new Set(),
321
+ targets: [],
376
322
  visited: /* @__PURE__ */ new Set()
377
323
  };
378
324
  collectModuleBindings(result.program, state);
379
325
  collectExportedBindings(result.program, state);
380
326
  visit(result.program, state);
381
- return {
382
- classes: state.classes,
383
- functions: state.functions
384
- };
327
+ const sortedTargets = [...state.targets].sort((left, right) => (left.range?.start ?? 0) - (right.range?.start ?? 0));
328
+ return [createFileTarget(file), ...withStableIdentities(sortedTargets)];
385
329
  }
386
- function addClassUnit(node, state) {
330
+ function addClassTarget(node, state) {
387
331
  if (state.seenClasses.has(node)) return;
388
- const unit = createClassUnit(node, state);
389
- if (!unit) return;
332
+ const target = createClassTarget(node, state);
333
+ if (!target) return;
390
334
  state.seenClasses.add(node);
391
- state.classes.push(unit);
335
+ state.targets.push(target);
392
336
  }
393
- function addFunctionUnit(node, state) {
337
+ function addFunctionTarget(node, state) {
394
338
  if (state.seenFunctions.has(node)) return;
395
- const unit = createFunctionUnit(node, state);
396
- if (!unit) return;
339
+ const target = createFunctionTarget(node, state);
340
+ if (!target) return;
397
341
  state.seenFunctions.add(node);
398
- state.functions.push(unit);
342
+ state.targets.push(target);
399
343
  }
400
344
  function asAstNode(value) {
401
345
  return isAstNode(value) ? value : void 0;
@@ -440,36 +384,69 @@ function collectModuleBindings(program, state) {
440
384
  if (declaration) collectDeclarationBindings(declaration, state);
441
385
  }
442
386
  }
443
- function createClassUnit(node, state) {
387
+ function createClassTarget(node, state) {
444
388
  const range = getRange(node);
445
389
  if (!range) return;
446
390
  const source = sliceRange(state.file, range);
391
+ const name = getNodeName(node) ?? state.inferredNames.get(node);
447
392
  return {
448
- exported: isExportedUnit(node, state),
449
393
  file: state.file,
394
+ identity: createRangeIdentity("class", name, range),
450
395
  kind: "class",
396
+ language: state.file.language,
451
397
  loc: source.loc,
452
- name: getNodeName(node) ?? state.inferredNames.get(node),
398
+ metadata: { exported: isExportedTarget(node, state) },
399
+ name,
400
+ origin: {
401
+ physicalPath: state.file.path,
402
+ range
403
+ },
453
404
  range,
454
405
  text: source.text
455
406
  };
456
407
  }
457
- function createFunctionUnit(node, state) {
408
+ function createFileTarget(file) {
409
+ return {
410
+ file,
411
+ identity: "file",
412
+ kind: "file",
413
+ language: file.language,
414
+ origin: { physicalPath: file.path },
415
+ text: file.text
416
+ };
417
+ }
418
+ function createFunctionTarget(node, state) {
458
419
  const range = getRange(node);
459
420
  if (!range) return;
460
421
  const source = sliceRange(state.file, range);
461
422
  const functionNode = node.type === "MethodDefinition" ? asAstNode(node.value) : node;
423
+ const name = getNodeName(node) ?? getNodeName(functionNode) ?? state.inferredNames.get(node);
462
424
  return {
463
- async: functionNode?.async === true,
464
- exported: isExportedUnit(node, state),
465
425
  file: state.file,
426
+ identity: createRangeIdentity("function", name, range),
466
427
  kind: "function",
428
+ language: state.file.language,
467
429
  loc: source.loc,
468
- name: getNodeName(node) ?? getNodeName(functionNode) ?? state.inferredNames.get(node),
430
+ metadata: {
431
+ async: functionNode?.async === true,
432
+ exported: isExportedTarget(node, state)
433
+ },
434
+ name,
435
+ origin: {
436
+ physicalPath: state.file.path,
437
+ range
438
+ },
469
439
  range,
470
440
  text: source.text
471
441
  };
472
442
  }
443
+ function createRangeIdentity(kind, name, range) {
444
+ return `${kind}:${name ?? "anonymous"}:${range.start}:${range.end}`;
445
+ }
446
+ function createSemanticIdentity(target) {
447
+ if (target.kind !== "class" && target.kind !== "function" || !target.name) return;
448
+ return `${target.kind}:${target.name}`;
449
+ }
473
450
  function getNodeName(node) {
474
451
  if (!node) return;
475
452
  if (typeof node.name === "string") return node.name;
@@ -509,7 +486,7 @@ function isClassNode(node) {
509
486
  function isExportDeclaration(node) {
510
487
  return node.type === "ExportDefaultDeclaration" || node.type === "ExportNamedDeclaration";
511
488
  }
512
- function isExportedUnit(node, state) {
489
+ function isExportedTarget(node, state) {
513
490
  return state.exportedNodes.has(node);
514
491
  }
515
492
  function isFunctionNode(node) {
@@ -544,9 +521,9 @@ function visit(node, state) {
544
521
  visitChildren(node, state, /* @__PURE__ */ new Set(["declaration"]));
545
522
  return;
546
523
  }
547
- if (isClassNode(node)) addClassUnit(node, state);
524
+ if (isClassNode(node)) addClassTarget(node, state);
548
525
  else if (isFunctionNode(node) || node.type === "MethodDefinition") {
549
- addFunctionUnit(node, state);
526
+ addFunctionTarget(node, state);
550
527
  const methodValue = node.type === "MethodDefinition" ? asAstNode(node.value) : void 0;
551
528
  if (methodValue) state.seenFunctions.add(methodValue);
552
529
  }
@@ -566,11 +543,373 @@ function visitChildren(node, state, skippedKeys = /* @__PURE__ */ new Set()) {
566
543
  }
567
544
  }
568
545
  }
569
- //#endregion
570
- //#region src/core/run.ts
571
- var AlintRuleExecutionError = class extends Error {
572
- failure;
573
- constructor(error, path) {
546
+ function withStableIdentities(targets) {
547
+ const semanticIdentityCounts = /* @__PURE__ */ new Map();
548
+ for (const target of targets) {
549
+ const semanticIdentity = createSemanticIdentity(target);
550
+ if (semanticIdentity) semanticIdentityCounts.set(semanticIdentity, (semanticIdentityCounts.get(semanticIdentity) ?? 0) + 1);
551
+ }
552
+ return targets.map((target) => {
553
+ const semanticIdentity = createSemanticIdentity(target);
554
+ if (!semanticIdentity || semanticIdentityCounts.get(semanticIdentity) !== 1) return target;
555
+ return {
556
+ ...target,
557
+ identity: semanticIdentity
558
+ };
559
+ });
560
+ }
561
+ //#endregion
562
+ //#region src/core/languages/js/index.ts
563
+ const javascriptLanguage = {
564
+ extensions: [
565
+ ".cjs",
566
+ ".js",
567
+ ".jsx",
568
+ ".mjs"
569
+ ],
570
+ extract: (file) => extractJsSourceTargets(withLanguage$1(file, "javascript")),
571
+ name: "javascript"
572
+ };
573
+ function withLanguage$1(file, language) {
574
+ if (file.language === language) return file;
575
+ return {
576
+ ...file,
577
+ language
578
+ };
579
+ }
580
+ //#endregion
581
+ //#region src/core/languages/registry.ts
582
+ function createLanguageRegistry() {
583
+ return {
584
+ byExtension: /* @__PURE__ */ new Map(),
585
+ languages: /* @__PURE__ */ new Map()
586
+ };
587
+ }
588
+ function registerLanguage(registry, language) {
589
+ const existing = registry.languages.get(language.name);
590
+ if (existing && existing !== language) throw new Error(`Duplicate language "${language.name}".`);
591
+ for (const extension of language.extensions ?? []) {
592
+ const existingOwner = registry.byExtension.get(extension);
593
+ if (existingOwner && existingOwner !== language.name) throw new Error(`Duplicate language extension "${extension}".`);
594
+ }
595
+ registry.languages.set(language.name, language);
596
+ for (const extension of language.extensions ?? []) registry.byExtension.set(extension, language.name);
597
+ }
598
+ //#endregion
599
+ //#region src/core/languages/text.ts
600
+ const textLanguage = {
601
+ extensions: [],
602
+ extract: (file) => [{
603
+ file,
604
+ identity: "file",
605
+ kind: "file",
606
+ language: "text/plain",
607
+ origin: { physicalPath: file.path },
608
+ text: file.text
609
+ }],
610
+ name: "text/plain"
611
+ };
612
+ //#endregion
613
+ //#region src/core/languages/ts/index.ts
614
+ const typescriptLanguage = {
615
+ extensions: [
616
+ ".cts",
617
+ ".mts",
618
+ ".ts",
619
+ ".tsx"
620
+ ],
621
+ extract: (file) => extractJsSourceTargets(withLanguage(file, "typescript")),
622
+ name: "typescript"
623
+ };
624
+ function withLanguage(file, language) {
625
+ if (file.language === language) return file;
626
+ return {
627
+ ...file,
628
+ language
629
+ };
630
+ }
631
+ //#endregion
632
+ //#region src/core/languages/resolve.ts
633
+ function resolveLanguage(file, registry, options) {
634
+ const languageName = options.language ?? options.processedLanguage ?? registry.byExtension.get(extname(file.path)) ?? "text/plain";
635
+ const language = registry.languages.get(languageName);
636
+ if (!language) throw new Error(`Unknown language "${languageName}".`);
637
+ return language;
638
+ }
639
+ //#endregion
640
+ //#region src/core/languages/index.ts
641
+ function createBuiltInLanguageRegistry() {
642
+ const registry = createLanguageRegistry();
643
+ registerLanguage(registry, textLanguage);
644
+ registerLanguage(registry, javascriptLanguage);
645
+ registerLanguage(registry, typescriptLanguage);
646
+ return registry;
647
+ }
648
+ //#endregion
649
+ //#region package.json
650
+ var version = "0.0.6";
651
+ //#endregion
652
+ //#region src/dsl/registry.ts
653
+ function buildRuleRegistry(config) {
654
+ const rules = /* @__PURE__ */ new Map();
655
+ const localIds = /* @__PURE__ */ new Map();
656
+ const enabledRules = [];
657
+ for (const [alias, plugin] of Object.entries(config.plugins)) for (const [localId, rule] of Object.entries(plugin.rules ?? {})) {
658
+ const id = `${alias}/${localId}`;
659
+ if (rules.has(id)) throw new Error(`Duplicate rule id "${id}".`);
660
+ rules.set(id, rule);
661
+ localIds.set(id, localId);
662
+ }
663
+ for (const [id, entry] of Object.entries(config.rules)) {
664
+ const rule = rules.get(id);
665
+ if (!rule) throw new Error(`Unknown rule "${id}".`);
666
+ const severity = normalizeSeverity(entry);
667
+ if (severity === "off") continue;
668
+ enabledRules.push({
669
+ id,
670
+ localId: localIds.get(id) ?? id,
671
+ rule,
672
+ severity
673
+ });
674
+ }
675
+ return {
676
+ enabledRules,
677
+ rules
678
+ };
679
+ }
680
+ function normalizeSeverity(entry) {
681
+ if (Array.isArray(entry)) return entry[0] ?? "warn";
682
+ return entry ?? "warn";
683
+ }
684
+ //#endregion
685
+ //#region src/models/resolve.ts
686
+ function resolveModel(registry, options = {}) {
687
+ const candidates = flattenModels(registry);
688
+ const ruleId = options.ruleId ?? "<unknown>";
689
+ if (options.request !== void 0) {
690
+ const request = options.request;
691
+ const candidate = candidates.find(({ model }) => matchesRequest(model, request));
692
+ if (candidate === void 0) throw new Error(`Unknown model "${request}".`);
693
+ if (!satisfiesHardRequirements(candidate.model, options.requirement)) throw new Error(`Model "${request}" does not satisfy requirement for rule "${ruleId}".`);
694
+ return toResolvedModel(candidate, options.requirement);
695
+ }
696
+ const candidate = preferSize(candidates.filter(({ model }) => satisfiesHardRequirements(model, options.requirement)), options.requirement);
697
+ if (candidate === void 0) throw new Error(`No model satisfies requirement for rule "${ruleId}".`);
698
+ return toResolvedModel(candidate, options.requirement);
699
+ }
700
+ function flattenModels(registry) {
701
+ return registry.providers.flatMap((provider) => provider.models.map((model) => ({
702
+ model,
703
+ provider
704
+ })));
705
+ }
706
+ function matchesRequest(model, request) {
707
+ return model.id === request || model.name === request || (model.aliases ?? []).includes(request);
708
+ }
709
+ function preferSize(candidates, requirement) {
710
+ if (requirement?.size === void 0) return candidates[0];
711
+ return candidates.find(({ model }) => model.size === requirement.size) ?? candidates[0];
712
+ }
713
+ function satisfiesHardRequirements(model, requirement) {
714
+ if (requirement === void 0) return true;
715
+ if (requirement.capabilities !== void 0 && !requirement.capabilities.every((capability) => (model.capabilities ?? []).includes(capability))) return false;
716
+ if (requirement.minContextWindow !== void 0 && (model.contextWindow === void 0 || model.contextWindow < requirement.minContextWindow)) return false;
717
+ return true;
718
+ }
719
+ function toResolvedModel(candidate, requirement) {
720
+ const { model, provider } = candidate;
721
+ return {
722
+ aliases: [...model.aliases ?? []],
723
+ capabilities: [...model.capabilities ?? []],
724
+ contextWindow: model.contextWindow,
725
+ id: model.id,
726
+ name: model.name ?? model.id,
727
+ params: {
728
+ ...model.defaultParams ?? {},
729
+ ...requirement?.params ?? {}
730
+ },
731
+ provider: {
732
+ endpoint: provider.endpoint,
733
+ headers: { ...provider.headers ?? {} },
734
+ id: provider.id,
735
+ type: provider.type
736
+ },
737
+ size: model.size
738
+ };
739
+ }
740
+ //#endregion
741
+ //#region src/core/cache.ts
742
+ const CACHE_SCHEMA_VERSION = 1;
743
+ const DEFAULT_CACHE_FILE_NAME = ".alintcache";
744
+ const CacheFileSchema = pipe(object({
745
+ createdAt: pipe(string(), description("Cache file creation timestamp.")),
746
+ entries: pipe(unknown(), check((value) => typeof value === "object" && value !== null && !Array.isArray(value)), record(string(), object({
747
+ diagnostics: pipe(array(object({
748
+ filePath: pipe(string(), description("Diagnostic file path.")),
749
+ message: pipe(string(), description("Diagnostic message.")),
750
+ ruleId: pipe(string(), description("Diagnostic rule id.")),
751
+ severity: pipe(union([literal("error"), literal("warn")]), description("Diagnostic severity."))
752
+ })), description("Diagnostics produced for the cached target.")),
753
+ filePath: pipe(string(), description("Cache entry file path.")),
754
+ fingerprint: pipe(object({
755
+ alintVersion: pipe(string(), description("Alint version used to create the cache entry.")),
756
+ configHash: pipe(string(), description("Runner config hash used to create the cache entry.")),
757
+ modelHash: pipe(string(), description("Model configuration hash used to create the cache entry.")),
758
+ ruleHash: pipe(string(), description("Rule configuration hash used to create the cache entry."))
759
+ }), description("Cache entry fingerprint.")),
760
+ target: pipe(object({
761
+ hash: pipe(string(), description("Cached target hash.")),
762
+ identity: pipe(string(), description("Stable cached target identity.")),
763
+ kind: pipe(string(), description("Cached target kind."))
764
+ }), description("Cached target metadata.")),
765
+ usage: pipe(array(object({
766
+ inputTokens: pipe(optional(number()), description("Optional input token count.")),
767
+ modelId: pipe(string(), description("Usage model id.")),
768
+ outputTokens: pipe(optional(number()), description("Optional output token count.")),
769
+ providerId: pipe(string(), description("Usage provider id.")),
770
+ ruleId: pipe(string(), description("Usage rule id.")),
771
+ totalTokens: pipe(optional(number()), description("Optional total token count."))
772
+ })), description("Inference usage records for the cache entry."))
773
+ })), description("Cache entries keyed by cache key.")),
774
+ files: pipe(unknown(), check((value) => typeof value === "object" && value !== null && !Array.isArray(value)), record(string(), object({
775
+ contentHash: pipe(string(), description("Cached file content hash.")),
776
+ entries: pipe(array(string()), description("Cache entry keys associated with the file.")),
777
+ path: pipe(string(), description("Normalized cached file path."))
778
+ })), description("Cached files keyed by normalized file path.")),
779
+ schemaVersion: pipe(literal(CACHE_SCHEMA_VERSION), description("Cache schema version.")),
780
+ updatedAt: pipe(string(), description("Cache file update timestamp."))
781
+ }), description("Alint cache file."));
782
+ function createCacheKey(input) {
783
+ return stableHash(input);
784
+ }
785
+ async function createCacheStore(options) {
786
+ const location = resolveCacheLocation(options.cwd, options.location);
787
+ if (!options.enabled) return createNoopCacheStore(location);
788
+ const cacheFile = await readCacheFile(location);
789
+ return {
790
+ get: (key) => cacheFile.entries[key],
791
+ location,
792
+ markFile: (filePath, contentHash, entries) => {
793
+ const path = normalizeCachePath(options.cwd, filePath);
794
+ cacheFile.files[path] = {
795
+ contentHash,
796
+ entries,
797
+ path
798
+ };
799
+ },
800
+ reconcile: async () => {
801
+ await mkdir(dirname(location), { recursive: true });
802
+ cacheFile.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
803
+ const tempPath = join(dirname(location), `.${DEFAULT_CACHE_FILE_NAME}.${process.pid}.${randomUUID()}.tmp`);
804
+ await writeFile(tempPath, `${JSON.stringify(cacheFile, null, 2)}\n`);
805
+ await rename(tempPath, location);
806
+ },
807
+ set: (key, entry) => {
808
+ cacheFile.entries[key] = entry;
809
+ }
810
+ };
811
+ }
812
+ function createTargetIdentityResolver(targets) {
813
+ const baseCounts = /* @__PURE__ */ new Map();
814
+ for (const target of targets) {
815
+ const base = createBaseTargetIdentity(target);
816
+ baseCounts.set(base, (baseCounts.get(base) ?? 0) + 1);
817
+ }
818
+ return (target) => {
819
+ const base = createBaseTargetIdentity(target);
820
+ if ((baseCounts.get(base) ?? 0) <= 1) return base;
821
+ if (target.range) return `${base}:${target.range.start}:${target.range.end}`;
822
+ return base;
823
+ };
824
+ }
825
+ function hashText(text) {
826
+ return createHash("sha256").update(text).digest("hex");
827
+ }
828
+ function normalizeCachePath(cwd, filePath) {
829
+ return relative$1(cwd, isAbsolute$1(filePath) ? resolve$1(filePath) : resolve$1(cwd, filePath)).split(sep).join("/");
830
+ }
831
+ function normalizeRunnerCacheConfig(cache, cwd) {
832
+ if (cache === false) return {
833
+ enabled: false,
834
+ location: resolveCacheLocation(cwd)
835
+ };
836
+ if (cache === true || cache === void 0) return {
837
+ enabled: true,
838
+ location: resolveCacheLocation(cwd)
839
+ };
840
+ return {
841
+ enabled: cache.enabled ?? true,
842
+ location: resolveCacheLocation(cwd, cache.location)
843
+ };
844
+ }
845
+ function resolveCacheLocation(cwd, location) {
846
+ if (!location) return join(cwd, DEFAULT_CACHE_FILE_NAME);
847
+ const resolved = isAbsolute$1(location) ? resolve$1(location) : resolve$1(cwd, location);
848
+ if (location.endsWith("/") || location.endsWith("\\")) return join(resolved, DEFAULT_CACHE_FILE_NAME);
849
+ try {
850
+ if (statSyncIsDirectory(resolved)) return join(resolved, DEFAULT_CACHE_FILE_NAME);
851
+ } catch {}
852
+ return resolved;
853
+ }
854
+ function stableHash(value) {
855
+ return hashText(stableStringify(value));
856
+ }
857
+ function createBaseTargetIdentity(target) {
858
+ if (target.identity && (target.kind !== "file" || target.identity !== "file")) return target.filePath ? `${target.kind}:${target.filePath}:${target.identity}` : `${target.kind}:${target.identity}`;
859
+ if (target.kind === "file") return target.filePath ? `file:${target.filePath}` : "file";
860
+ if (target.name) return `${target.kind}:${target.name}`;
861
+ if (target.range) return `${target.kind}:${target.range.start}:${target.range.end}`;
862
+ return target.kind;
863
+ }
864
+ function createEmptyCacheFile() {
865
+ const now = (/* @__PURE__ */ new Date()).toISOString();
866
+ return {
867
+ createdAt: now,
868
+ entries: {},
869
+ files: {},
870
+ schemaVersion: CACHE_SCHEMA_VERSION,
871
+ updatedAt: now
872
+ };
873
+ }
874
+ function createNoopCacheStore(location) {
875
+ return {
876
+ get: () => void 0,
877
+ location,
878
+ markFile: () => {},
879
+ reconcile: async () => {},
880
+ set: () => {}
881
+ };
882
+ }
883
+ function isCacheFile(value) {
884
+ return is(CacheFileSchema, value);
885
+ }
886
+ function isRecord(value) {
887
+ return typeof value === "object" && value !== null && !Array.isArray(value);
888
+ }
889
+ async function readCacheFile(location) {
890
+ try {
891
+ const parsed = JSON.parse(await readFile(location, "utf8"));
892
+ if (isCacheFile(parsed)) return parsed;
893
+ } catch {}
894
+ return createEmptyCacheFile();
895
+ }
896
+ function stableStringify(value) {
897
+ if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(",")}]`;
898
+ if (isRecord(value)) return `{${Object.keys(value).sort().map((key) => {
899
+ const property = value[key];
900
+ if (property === void 0) return;
901
+ return `${JSON.stringify(key)}:${stableStringify(property)}`;
902
+ }).filter((entry) => entry !== void 0).join(",")}}`;
903
+ return JSON.stringify(value);
904
+ }
905
+ function statSyncIsDirectory(path) {
906
+ return statSync(path).isDirectory();
907
+ }
908
+ //#endregion
909
+ //#region src/core/run.ts
910
+ var AlintRuleExecutionError = class extends Error {
911
+ failure;
912
+ constructor(error, path) {
574
913
  const message = errorMessageFrom(error) ?? String(error);
575
914
  super(message, { cause: error });
576
915
  this.name = "AlintRuleExecutionError";
@@ -597,7 +936,7 @@ var AlintRunError = class extends Error {
597
936
  };
598
937
  async function runAlint(options = {}) {
599
938
  const cwd$1 = options.cwd ?? cwd();
600
- const config = options.config ?? {};
939
+ const config = options.config ?? [];
601
940
  const setupConfig = options.setupConfig ?? {
602
941
  providers: [],
603
942
  version: 1
@@ -605,7 +944,6 @@ async function runAlint(options = {}) {
605
944
  const clock = options.runner?.clock ?? Date.now;
606
945
  const diagnostics = [];
607
946
  const usage = createUsageAccumulator();
608
- const registry = buildRuleRegistry(config);
609
947
  const src = createSourceRuntime();
610
948
  const normalizedCacheConfig = normalizeRunnerCacheConfig(options.runner?.cache, cwd$1);
611
949
  const cacheStore = await createCacheStore({
@@ -614,7 +952,6 @@ async function runAlint(options = {}) {
614
952
  location: normalizedCacheConfig.location
615
953
  });
616
954
  const cacheContext = {
617
- configHash: stableHash({ rules: config.rules ?? {} }),
618
955
  cwd: cwd$1,
619
956
  enabled: normalizedCacheConfig.enabled,
620
957
  fileEntryKeys: /* @__PURE__ */ new Map(),
@@ -624,13 +961,44 @@ async function runAlint(options = {}) {
624
961
  }),
625
962
  store: cacheStore
626
963
  };
627
- const files = await Promise.all((options.files ?? []).map(async (filePath) => {
964
+ const files = (await Promise.all((options.files ?? []).map(async (filePath) => {
628
965
  const file = await src.readFile(resolve(cwd$1, filePath));
966
+ const resolvedConfig = resolveConfigForFile(file.path, config, { cwd: cwd$1 });
967
+ if (resolvedConfig.ignored) return;
968
+ const effectiveConfig = resolvedConfig.config;
969
+ const languageRegistry = createBuiltInLanguageRegistry();
970
+ for (const plugin of Object.values(effectiveConfig.plugins)) for (const language of Object.values(plugin.languages ?? {})) registerLanguage(languageRegistry, language);
971
+ const language = resolveLanguage(file, languageRegistry, { language: effectiveConfig.language });
972
+ const targets = await language.extract(file, {
973
+ cwd: cwd$1,
974
+ languageOptions: effectiveConfig.languageOptions,
975
+ src
976
+ });
977
+ const registry = buildRuleRegistry(effectiveConfig);
978
+ const ruleRuntimes = createRuleRuntimes({
979
+ cwd: cwd$1,
980
+ diagnostics,
981
+ effectiveSettings: effectiveConfig.settings,
982
+ options,
983
+ registry,
984
+ setupConfig,
985
+ src,
986
+ usage
987
+ });
629
988
  return {
989
+ configHash: stableHash({
990
+ language: effectiveConfig.language,
991
+ languageOptions: effectiveConfig.languageOptions,
992
+ processor: effectiveConfig.processor,
993
+ resolvedLanguage: language.name,
994
+ rules: effectiveConfig.rules,
995
+ settings: effectiveConfig.settings
996
+ }),
630
997
  file,
631
- units: file.language === "javascript" || file.language === "typescript" ? extractJsSourceUnits(file) : void 0
998
+ ruleRuntimes,
999
+ targets
632
1000
  };
633
- }));
1001
+ }))).filter((file) => file !== void 0);
634
1002
  let planned = 0;
635
1003
  const counters = createRuleEndCounters();
636
1004
  const primaryError = createPrimaryErrorState();
@@ -638,86 +1006,14 @@ async function runAlint(options = {}) {
638
1006
  let runStartedAt;
639
1007
  let runError;
640
1008
  try {
641
- filePlans = createPreparedFileExecutionPlans(registry.enabledRules.map((enabledRule) => {
642
- const executionState = new AsyncLocalStorage();
643
- const context = {
644
- cwd: cwd$1,
645
- id: enabledRule.id,
646
- localId: enabledRule.localId,
647
- logger: { debug: () => {} },
648
- metering: { recordUsage: (record) => {
649
- const usageRecord = usage.record({
650
- ...record,
651
- ruleId: record.ruleId ?? enabledRule.id
652
- });
653
- const state = executionState.getStore();
654
- state?.cacheUsage?.push(usageRecord);
655
- options.progress?.onUsage?.({
656
- path: state?.progressPath,
657
- record: usageRecord,
658
- total: usage.toJSON()
659
- });
660
- } },
661
- model: async (selector) => {
662
- const request = options.modelOverride ?? (typeof selector === "string" ? selector : void 0);
663
- const resolvedModel = resolveModel(setupConfig, {
664
- request,
665
- requirement: mergeModelRequirement(enabledRule.rule.model, typeof selector === "string" ? void 0 : selector),
666
- ruleId: enabledRule.id
667
- });
668
- const state = executionState.getStore();
669
- if (state) state.currentModel = toDiagnosticModel(resolvedModel, request);
670
- return resolvedModel;
671
- },
672
- report: (descriptor) => {
673
- const state = executionState.getStore();
674
- const filePath = descriptor.filePath ?? state?.activeFilePath;
675
- if (!filePath) throw new Error(`Diagnostic for rule "${enabledRule.id}" is missing filePath.`);
676
- const diagnosticModel = state?.currentModel ? { ...state.currentModel } : void 0;
677
- if (state) state.currentModel = void 0;
678
- const diagnostic = {
679
- evidence: descriptor.evidence,
680
- filePath,
681
- loc: descriptor.loc,
682
- message: descriptor.message,
683
- model: diagnosticModel,
684
- ruleId: enabledRule.id,
685
- severity: enabledRule.severity
686
- };
687
- diagnostics.push(diagnostic);
688
- state?.cacheDiagnostics?.push(diagnostic);
689
- options.progress?.onDiagnostic?.({
690
- diagnostic,
691
- diagnostics: [...diagnostics],
692
- path: state?.progressPath
693
- });
694
- },
695
- scope: enabledRule.scope,
696
- src
697
- };
698
- return {
699
- cacheable: enabledRule.rule.cache !== false,
700
- enabledRule,
701
- executionState,
702
- handlers: enabledRule.rule.create(context),
703
- ruleHash: stableHash({
704
- cache: enabledRule.rule.cache ?? true,
705
- create: String(enabledRule.rule.create),
706
- id: enabledRule.id,
707
- localId: enabledRule.localId,
708
- model: enabledRule.rule.model,
709
- scope: enabledRule.scope,
710
- severity: enabledRule.severity
711
- })
712
- };
713
- }), files, cwd$1);
1009
+ filePlans = createPreparedFileExecutionPlans(files, cwd$1);
714
1010
  planned = calculatePlannedExecutions(filePlans);
715
1011
  runStartedAt = clock();
716
1012
  options.progress?.onRunStart?.({
717
1013
  files: filePlans.map((filePlan) => toProgressFilePath(filePlan, files.length)),
718
1014
  filesTotal: files.length,
719
1015
  planned,
720
- rulesTotal: registry.enabledRules.length,
1016
+ rulesTotal: countEnabledRuleIds(files),
721
1017
  startedAt: runStartedAt
722
1018
  });
723
1019
  await runConcurrently(filePlans.filter((filePlan) => filePlan.targets.length > 0), resolveFileConcurrency(options.runner?.fileConcurrency), (filePlan) => executeFilePlan(filePlan, files.length, clock, counters, diagnostics, usage, cacheContext, options));
@@ -750,61 +1046,38 @@ function calculateFilePlanExecutions(filePlan) {
750
1046
  function calculatePlannedExecutions(filePlans) {
751
1047
  return filePlans.reduce((total, filePlan) => total + filePlan.planned, 0);
752
1048
  }
753
- function collectExecutionTargets(ruleRuntimes, preparedFile) {
1049
+ function collectExecutionTargets(preparedFile) {
754
1050
  const targets = [];
755
- const fileExecutions = ruleRuntimes.map((runtime) => {
756
- if (!runtime.handlers.onFile) return void 0;
757
- return {
758
- run: () => runtime.handlers.onFile?.(preparedFile.file),
759
- runtime
760
- };
761
- }).filter((execution) => execution !== void 0);
762
- if (fileExecutions.length > 0) targets.push({
763
- executions: fileExecutions,
764
- identity: "",
765
- kind: "file",
766
- text: preparedFile.file.text
767
- });
768
- if (preparedFile.units) {
769
- for (const classNode of preparedFile.units.classes) {
770
- const executions = ruleRuntimes.map((runtime) => {
771
- if (!runtime.handlers.onClass) return void 0;
772
- return {
773
- run: () => runtime.handlers.onClass?.(classNode),
774
- runtime
775
- };
776
- }).filter((execution) => execution !== void 0);
777
- if (executions.length > 0) targets.push({
778
- executions,
779
- identity: "",
780
- kind: "class",
781
- loc: classNode.loc,
782
- name: classNode.name,
783
- range: classNode.range,
784
- text: classNode.text
785
- });
786
- }
787
- for (const functionNode of preparedFile.units.functions) {
788
- const executions = ruleRuntimes.map((runtime) => {
789
- if (!runtime.handlers.onFunction) return void 0;
790
- return {
791
- run: () => runtime.handlers.onFunction?.(functionNode),
792
- runtime
793
- };
794
- }).filter((execution) => execution !== void 0);
795
- if (executions.length > 0) targets.push({
796
- executions,
797
- identity: "",
798
- kind: "function",
799
- loc: functionNode.loc,
800
- name: functionNode.name,
801
- range: functionNode.range,
802
- text: functionNode.text
803
- });
804
- }
1051
+ for (const sourceTarget of preparedFile.targets) {
1052
+ const executions = preparedFile.ruleRuntimes.map((runtime) => {
1053
+ if (!runtime.handlers.onTarget) return;
1054
+ return {
1055
+ run: () => runtime.handlers.onTarget?.(sourceTarget),
1056
+ runtime
1057
+ };
1058
+ }).filter((execution) => execution !== void 0);
1059
+ if (executions.length === 0) continue;
1060
+ targets.push({
1061
+ configHash: preparedFile.configHash,
1062
+ executions,
1063
+ identity: sourceTarget.identity,
1064
+ kind: sourceTarget.kind,
1065
+ language: sourceTarget.language,
1066
+ loc: sourceTarget.loc,
1067
+ metadata: sourceTarget.metadata,
1068
+ name: sourceTarget.name,
1069
+ origin: sourceTarget.origin,
1070
+ range: sourceTarget.range,
1071
+ text: sourceTarget.text
1072
+ });
805
1073
  }
806
1074
  return targets;
807
1075
  }
1076
+ function countEnabledRuleIds(files) {
1077
+ const ids = /* @__PURE__ */ new Set();
1078
+ for (const file of files) for (const runtime of file.ruleRuntimes) ids.add(runtime.enabledRule.id);
1079
+ return ids.size;
1080
+ }
808
1081
  function createAlintRunError(error, result) {
809
1082
  if (error instanceof AlintRunError) return error;
810
1083
  if (error instanceof AlintRuleExecutionError) return new AlintRunError(error.message, result, {
@@ -817,19 +1090,19 @@ function createExecutionCacheKey(runtime, target, path, cacheContext) {
817
1090
  if (!cacheContext.enabled || !runtime.cacheable) return;
818
1091
  return createCacheKey({
819
1092
  alintVersion: version,
820
- configHash: cacheContext.configHash,
1093
+ configHash: target.configHash,
821
1094
  filePath: normalizeCachePath(cacheContext.cwd, path.file.path),
822
1095
  modelHash: cacheContext.modelHash,
823
1096
  ruleHash: runtime.ruleHash,
824
1097
  schemaVersion: 1,
825
- targetHash: hashText(target.text),
1098
+ targetHash: createTargetHash(target),
826
1099
  targetIdentity: target.identity,
827
1100
  targetKind: target.kind
828
1101
  });
829
1102
  }
830
- function createPreparedFileExecutionPlans(ruleRuntimes, files, cwd) {
1103
+ function createPreparedFileExecutionPlans(files, cwd) {
831
1104
  return files.map((preparedFile, fileOffset) => {
832
- const targets = collectExecutionTargets(ruleRuntimes, preparedFile);
1105
+ const targets = collectExecutionTargets(preparedFile);
833
1106
  const resolveTargetIdentity = createTargetIdentityResolver(targets.map((target) => toTargetIdentityInput(cwd, preparedFile.file.path, target)));
834
1107
  for (const target of targets) target.identity = resolveTargetIdentity(toTargetIdentityInput(cwd, preparedFile.file.path, target));
835
1108
  const filePlan = {
@@ -898,6 +1171,92 @@ function createRuleEndCounters() {
898
1171
  }
899
1172
  };
900
1173
  }
1174
+ function createRuleRuntimes(options) {
1175
+ return options.registry.enabledRules.map((enabledRule) => {
1176
+ const executionState = new AsyncLocalStorage();
1177
+ const context = {
1178
+ cwd: options.cwd,
1179
+ id: enabledRule.id,
1180
+ localId: enabledRule.localId,
1181
+ logger: { debug: () => {} },
1182
+ metering: { recordUsage: (record) => {
1183
+ const usageRecord = options.usage.record({
1184
+ ...record,
1185
+ ruleId: record.ruleId ?? enabledRule.id
1186
+ });
1187
+ const state = executionState.getStore();
1188
+ state?.cacheUsage?.push(usageRecord);
1189
+ options.options.progress?.onUsage?.({
1190
+ path: state?.progressPath,
1191
+ record: usageRecord,
1192
+ total: options.usage.toJSON()
1193
+ });
1194
+ } },
1195
+ model: async (selector) => {
1196
+ const request = options.options.modelOverride ?? (typeof selector === "string" ? selector : void 0);
1197
+ const requirement = mergeModelRequirement(enabledRule.rule.model, typeof selector === "string" ? void 0 : selector);
1198
+ const resolvedModel = resolveModel(options.setupConfig, {
1199
+ request,
1200
+ requirement,
1201
+ ruleId: enabledRule.id
1202
+ });
1203
+ const state = executionState.getStore();
1204
+ if (state) state.currentModel = toDiagnosticModel(resolvedModel, request);
1205
+ return resolvedModel;
1206
+ },
1207
+ report: (descriptor) => {
1208
+ const state = executionState.getStore();
1209
+ const filePath = descriptor.filePath ?? state?.activeFilePath;
1210
+ if (!filePath) throw new Error(`Diagnostic for rule "${enabledRule.id}" is missing filePath.`);
1211
+ const diagnosticModel = state?.currentModel ? { ...state.currentModel } : void 0;
1212
+ if (state) state.currentModel = void 0;
1213
+ const diagnostic = {
1214
+ evidence: descriptor.evidence,
1215
+ filePath,
1216
+ loc: descriptor.loc,
1217
+ message: descriptor.message,
1218
+ model: diagnosticModel,
1219
+ ruleId: enabledRule.id,
1220
+ severity: enabledRule.severity
1221
+ };
1222
+ options.diagnostics.push(diagnostic);
1223
+ state?.cacheDiagnostics?.push(diagnostic);
1224
+ options.options.progress?.onDiagnostic?.({
1225
+ diagnostic,
1226
+ diagnostics: [...options.diagnostics],
1227
+ path: state?.progressPath
1228
+ });
1229
+ },
1230
+ settings: options.effectiveSettings,
1231
+ src: options.src
1232
+ };
1233
+ return {
1234
+ cacheable: enabledRule.rule.cache !== false,
1235
+ enabledRule,
1236
+ executionState,
1237
+ handlers: enabledRule.rule.create(context),
1238
+ ruleHash: stableHash({
1239
+ cache: enabledRule.rule.cache ?? true,
1240
+ create: String(enabledRule.rule.create),
1241
+ id: enabledRule.id,
1242
+ localId: enabledRule.localId,
1243
+ model: enabledRule.rule.model,
1244
+ severity: enabledRule.severity
1245
+ })
1246
+ };
1247
+ });
1248
+ }
1249
+ function createTargetHash(target) {
1250
+ return stableHash({
1251
+ language: target.language,
1252
+ loc: target.loc,
1253
+ metadata: target.metadata,
1254
+ name: target.name,
1255
+ origin: target.origin,
1256
+ range: target.range,
1257
+ text: target.text
1258
+ });
1259
+ }
901
1260
  function createUsageAccumulator() {
902
1261
  const records = [];
903
1262
  let inputTokens = 0;
@@ -1052,12 +1411,12 @@ async function executeProgressTarget(execution, path, target, clock, counters, d
1052
1411
  filePath: normalizeCachePath(cacheContext.cwd, path.file.path),
1053
1412
  fingerprint: {
1054
1413
  alintVersion: version,
1055
- configHash: cacheContext.configHash,
1414
+ configHash: target.configHash,
1056
1415
  modelHash: cacheContext.modelHash,
1057
1416
  ruleHash: execution.runtime.ruleHash
1058
1417
  },
1059
1418
  target: {
1060
- hash: hashText(target.text),
1419
+ hash: createTargetHash(target),
1061
1420
  identity: target.identity,
1062
1421
  kind: target.kind,
1063
1422
  loc: target.loc,
@@ -1190,6 +1549,7 @@ function toProgressFilePath(filePlan, filesTotal) {
1190
1549
  function toTargetIdentityInput(cwd, filePath, target) {
1191
1550
  return {
1192
1551
  filePath: target.kind === "file" ? normalizeCachePath(cwd, filePath) : void 0,
1552
+ identity: target.identity,
1193
1553
  kind: target.kind,
1194
1554
  name: target.name,
1195
1555
  range: target.range
@@ -1207,4 +1567,4 @@ function defineRule(rule) {
1207
1567
  return rule;
1208
1568
  }
1209
1569
  //#endregion
1210
- export { AlintRunError, buildRuleRegistry, createSourceFile, createSourceRuntime, defineConfig, definePlugin, defineRule, extractJsSourceUnits, resolveModel, runAlint, sliceLines, sliceRange };
1570
+ export { AlintRunError, buildRuleRegistry, createBuiltInLanguageRegistry, createSourceFile, createSourceRuntime, defineConfig, definePlugin, defineRule, extractJsSourceTargets, hasDiscoveryFilePatterns, matchesDiscoveryFile, normalizeConfig, registerLanguage, resolveConfigForFile, resolveLanguage, resolveModel, runAlint, sliceLines, sliceRange };