@alint-js/core 0.0.4
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 +365 -0
- package/dist/index.mjs +1210 -0
- package/package.json +33 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1210 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import process, { cwd } from "node:process";
|
|
3
|
+
import { errorCauseFrom, errorMessageFrom } from "@moeru/std/error";
|
|
4
|
+
import { resolve } from "pathe";
|
|
5
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
+
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
|
+
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.4";
|
|
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
|
+
});
|
|
31
|
+
}
|
|
32
|
+
for (const id of Object.keys(config.rules ?? {})) if (!rules.has(id)) throw new Error(`Unknown rule "${id}".`);
|
|
33
|
+
return {
|
|
34
|
+
enabledRules,
|
|
35
|
+
rules
|
|
36
|
+
};
|
|
37
|
+
}
|
|
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;
|
|
79
|
+
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
|
|
96
|
+
};
|
|
97
|
+
}
|
|
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);
|
|
151
|
+
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
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
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);
|
|
179
|
+
}
|
|
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
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function hashText(text) {
|
|
188
|
+
return createHash("sha256").update(text).digest("hex");
|
|
189
|
+
}
|
|
190
|
+
function normalizeCachePath(cwd, filePath) {
|
|
191
|
+
return relative(cwd, isAbsolute(filePath) ? resolve$1(filePath) : resolve$1(cwd, filePath)).split(sep).join("/");
|
|
192
|
+
}
|
|
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
|
+
};
|
|
206
|
+
}
|
|
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;
|
|
215
|
+
}
|
|
216
|
+
function stableHash(value) {
|
|
217
|
+
return hashText(stableStringify(value));
|
|
218
|
+
}
|
|
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;
|
|
224
|
+
}
|
|
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
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function createNoopCacheStore(location) {
|
|
236
|
+
return {
|
|
237
|
+
get: () => void 0,
|
|
238
|
+
location,
|
|
239
|
+
markFile: () => {},
|
|
240
|
+
reconcile: async () => {},
|
|
241
|
+
set: () => {}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function isCacheFile(value) {
|
|
245
|
+
return is(CacheFileSchema, value);
|
|
246
|
+
}
|
|
247
|
+
function isRecord(value) {
|
|
248
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
249
|
+
}
|
|
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();
|
|
256
|
+
}
|
|
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);
|
|
265
|
+
}
|
|
266
|
+
function statSyncIsDirectory(path) {
|
|
267
|
+
return statSync(path).isDirectory();
|
|
268
|
+
}
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region src/core/source/runtime.ts
|
|
271
|
+
function createSourceFile(path, text) {
|
|
272
|
+
return {
|
|
273
|
+
language: inferLanguage(path),
|
|
274
|
+
lines: text.split(/\r?\n/),
|
|
275
|
+
path,
|
|
276
|
+
text
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function createSourceRuntime() {
|
|
280
|
+
return {
|
|
281
|
+
getText: (target) => target.text,
|
|
282
|
+
readFile: async (filePath) => createSourceFile(filePath, await readFile(filePath, "utf8")),
|
|
283
|
+
sliceLines,
|
|
284
|
+
sliceRange
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function sliceLines(file, range) {
|
|
288
|
+
const startLine = clampLine(range.startLine, file.lines.length);
|
|
289
|
+
const endLine = clampLine(range.endLine, file.lines.length);
|
|
290
|
+
const orderedStartLine = Math.min(startLine, endLine);
|
|
291
|
+
const orderedEndLine = Math.max(startLine, endLine);
|
|
292
|
+
const text = file.lines.slice(orderedStartLine - 1, orderedEndLine).join("\n");
|
|
293
|
+
return {
|
|
294
|
+
filePath: file.path,
|
|
295
|
+
loc: {
|
|
296
|
+
end: {
|
|
297
|
+
column: file.lines[orderedEndLine - 1]?.length ?? 0,
|
|
298
|
+
line: orderedEndLine
|
|
299
|
+
},
|
|
300
|
+
start: {
|
|
301
|
+
column: 0,
|
|
302
|
+
line: orderedStartLine
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
text
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function sliceRange(file, range) {
|
|
309
|
+
const start = clampOffset(range.start, file.text.length);
|
|
310
|
+
const end = clampOffset(range.end, file.text.length);
|
|
311
|
+
const orderedStart = Math.min(start, end);
|
|
312
|
+
const orderedEnd = Math.max(start, end);
|
|
313
|
+
return {
|
|
314
|
+
filePath: file.path,
|
|
315
|
+
loc: {
|
|
316
|
+
end: getPosition(file.text, orderedEnd),
|
|
317
|
+
start: getPosition(file.text, orderedStart)
|
|
318
|
+
},
|
|
319
|
+
text: file.text.slice(orderedStart, orderedEnd)
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function clampLine(line, lineCount) {
|
|
323
|
+
if (lineCount === 0) return 1;
|
|
324
|
+
return clamp(Math.trunc(line), 1, lineCount);
|
|
325
|
+
}
|
|
326
|
+
function clampOffset(offset, textLength) {
|
|
327
|
+
return clamp(Math.trunc(offset), 0, textLength);
|
|
328
|
+
}
|
|
329
|
+
function getPosition(text, offset) {
|
|
330
|
+
let line = 1;
|
|
331
|
+
let column = 0;
|
|
332
|
+
let index = 0;
|
|
333
|
+
while (index < offset) {
|
|
334
|
+
const character = text[index];
|
|
335
|
+
if (character === "\r") {
|
|
336
|
+
if (text[index + 1] === "\n" && index + 1 < offset) index += 1;
|
|
337
|
+
line += 1;
|
|
338
|
+
column = 0;
|
|
339
|
+
} else if (character === "\n") {
|
|
340
|
+
line += 1;
|
|
341
|
+
column = 0;
|
|
342
|
+
} else column += 1;
|
|
343
|
+
index += 1;
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
column,
|
|
347
|
+
line
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function inferLanguage(path) {
|
|
351
|
+
switch (extname(path)) {
|
|
352
|
+
case ".cjs":
|
|
353
|
+
case ".js":
|
|
354
|
+
case ".jsx":
|
|
355
|
+
case ".mjs": return "javascript";
|
|
356
|
+
case ".cts":
|
|
357
|
+
case ".mts":
|
|
358
|
+
case ".ts":
|
|
359
|
+
case ".tsx": return "typescript";
|
|
360
|
+
default: return "unknown";
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
//#endregion
|
|
364
|
+
//#region src/core/source/js.ts
|
|
365
|
+
function extractJsSourceUnits(file) {
|
|
366
|
+
const result = parseSync(file.path, file.text, { sourceType: "module" });
|
|
367
|
+
const state = {
|
|
368
|
+
bindingNodes: /* @__PURE__ */ new Map(),
|
|
369
|
+
classes: [],
|
|
370
|
+
exportedNodes: /* @__PURE__ */ new Set(),
|
|
371
|
+
file,
|
|
372
|
+
functions: [],
|
|
373
|
+
inferredNames: /* @__PURE__ */ new Map(),
|
|
374
|
+
seenClasses: /* @__PURE__ */ new Set(),
|
|
375
|
+
seenFunctions: /* @__PURE__ */ new Set(),
|
|
376
|
+
visited: /* @__PURE__ */ new Set()
|
|
377
|
+
};
|
|
378
|
+
collectModuleBindings(result.program, state);
|
|
379
|
+
collectExportedBindings(result.program, state);
|
|
380
|
+
visit(result.program, state);
|
|
381
|
+
return {
|
|
382
|
+
classes: state.classes,
|
|
383
|
+
functions: state.functions
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function addClassUnit(node, state) {
|
|
387
|
+
if (state.seenClasses.has(node)) return;
|
|
388
|
+
const unit = createClassUnit(node, state);
|
|
389
|
+
if (!unit) return;
|
|
390
|
+
state.seenClasses.add(node);
|
|
391
|
+
state.classes.push(unit);
|
|
392
|
+
}
|
|
393
|
+
function addFunctionUnit(node, state) {
|
|
394
|
+
if (state.seenFunctions.has(node)) return;
|
|
395
|
+
const unit = createFunctionUnit(node, state);
|
|
396
|
+
if (!unit) return;
|
|
397
|
+
state.seenFunctions.add(node);
|
|
398
|
+
state.functions.push(unit);
|
|
399
|
+
}
|
|
400
|
+
function asAstNode(value) {
|
|
401
|
+
return isAstNode(value) ? value : void 0;
|
|
402
|
+
}
|
|
403
|
+
function collectDeclarationBindings(node, state) {
|
|
404
|
+
const name = getNodeName(node);
|
|
405
|
+
if (name && (isClassNode(node) || isFunctionNode(node))) {
|
|
406
|
+
state.bindingNodes.set(name, node);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (node.type !== "VariableDeclaration" || !Array.isArray(node.declarations)) return;
|
|
410
|
+
for (const declarator of node.declarations) {
|
|
411
|
+
if (!isAstNode(declarator)) continue;
|
|
412
|
+
const id = asAstNode(declarator.id);
|
|
413
|
+
const initializer = asAstNode(declarator.init);
|
|
414
|
+
if (typeof id?.name === "string" && initializer && (isClassNode(initializer) || isFunctionNode(initializer))) state.bindingNodes.set(id.name, initializer);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function collectExportedBindings(node, state) {
|
|
418
|
+
if (!node) return;
|
|
419
|
+
if (Array.isArray(node)) {
|
|
420
|
+
for (const child of node) collectExportedBindings(child, state);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (node.type === "ExportNamedDeclaration" && !node.source && Array.isArray(node.specifiers)) for (const specifier of node.specifiers) {
|
|
424
|
+
if (!isAstNode(specifier) || specifier.type !== "ExportSpecifier") continue;
|
|
425
|
+
const local = asAstNode(specifier.local);
|
|
426
|
+
if (typeof local?.name === "string") markExportedBinding(local.name, state);
|
|
427
|
+
}
|
|
428
|
+
else if (node.type === "ExportDefaultDeclaration") {
|
|
429
|
+
const declaration = asAstNode(node.declaration);
|
|
430
|
+
if (typeof declaration?.name === "string") markExportedBinding(declaration.name, state);
|
|
431
|
+
}
|
|
432
|
+
for (const value of Object.values(node)) if (Array.isArray(value)) collectExportedBindings(value.filter(isAstNode), state);
|
|
433
|
+
else if (isAstNode(value)) collectExportedBindings(value, state);
|
|
434
|
+
}
|
|
435
|
+
function collectModuleBindings(program, state) {
|
|
436
|
+
if (!Array.isArray(program.body)) return;
|
|
437
|
+
for (const statement of program.body) {
|
|
438
|
+
if (!isAstNode(statement)) continue;
|
|
439
|
+
const declaration = isExportDeclaration(statement) ? asAstNode(statement.declaration) : statement;
|
|
440
|
+
if (declaration) collectDeclarationBindings(declaration, state);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function createClassUnit(node, state) {
|
|
444
|
+
const range = getRange(node);
|
|
445
|
+
if (!range) return;
|
|
446
|
+
const source = sliceRange(state.file, range);
|
|
447
|
+
return {
|
|
448
|
+
exported: isExportedUnit(node, state),
|
|
449
|
+
file: state.file,
|
|
450
|
+
kind: "class",
|
|
451
|
+
loc: source.loc,
|
|
452
|
+
name: getNodeName(node) ?? state.inferredNames.get(node),
|
|
453
|
+
range,
|
|
454
|
+
text: source.text
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function createFunctionUnit(node, state) {
|
|
458
|
+
const range = getRange(node);
|
|
459
|
+
if (!range) return;
|
|
460
|
+
const source = sliceRange(state.file, range);
|
|
461
|
+
const functionNode = node.type === "MethodDefinition" ? asAstNode(node.value) : node;
|
|
462
|
+
return {
|
|
463
|
+
async: functionNode?.async === true,
|
|
464
|
+
exported: isExportedUnit(node, state),
|
|
465
|
+
file: state.file,
|
|
466
|
+
kind: "function",
|
|
467
|
+
loc: source.loc,
|
|
468
|
+
name: getNodeName(node) ?? getNodeName(functionNode) ?? state.inferredNames.get(node),
|
|
469
|
+
range,
|
|
470
|
+
text: source.text
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function getNodeName(node) {
|
|
474
|
+
if (!node) return;
|
|
475
|
+
if (typeof node.name === "string") return node.name;
|
|
476
|
+
const id = asAstNode(node.id);
|
|
477
|
+
if (typeof id?.name === "string") return id.name;
|
|
478
|
+
const key = asAstNode(node.key);
|
|
479
|
+
if (typeof key?.name === "string") return key.name;
|
|
480
|
+
if (typeof key?.value === "string" || typeof key?.value === "number") return String(key.value);
|
|
481
|
+
}
|
|
482
|
+
function getRange(node) {
|
|
483
|
+
if (typeof node.start === "number" && typeof node.end === "number") return {
|
|
484
|
+
end: node.end,
|
|
485
|
+
start: node.start
|
|
486
|
+
};
|
|
487
|
+
if (Array.isArray(node.range) && node.range.length === 2) return {
|
|
488
|
+
end: node.range[1],
|
|
489
|
+
start: node.range[0]
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
function inferChildName(parent, key, child, state) {
|
|
493
|
+
if (state.inferredNames.has(child)) return;
|
|
494
|
+
if (parent.type === "VariableDeclarator" && key === "init") {
|
|
495
|
+
const id = asAstNode(parent.id);
|
|
496
|
+
if (typeof id?.name === "string") state.inferredNames.set(child, id.name);
|
|
497
|
+
}
|
|
498
|
+
if (parent.type === "Property" && key === "value") {
|
|
499
|
+
const name = getNodeName(parent);
|
|
500
|
+
if (name) state.inferredNames.set(child, name);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function isAstNode(value) {
|
|
504
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
505
|
+
}
|
|
506
|
+
function isClassNode(node) {
|
|
507
|
+
return node.type === "ClassDeclaration" || node.type === "ClassExpression";
|
|
508
|
+
}
|
|
509
|
+
function isExportDeclaration(node) {
|
|
510
|
+
return node.type === "ExportDefaultDeclaration" || node.type === "ExportNamedDeclaration";
|
|
511
|
+
}
|
|
512
|
+
function isExportedUnit(node, state) {
|
|
513
|
+
return state.exportedNodes.has(node);
|
|
514
|
+
}
|
|
515
|
+
function isFunctionNode(node) {
|
|
516
|
+
return node.type === "ArrowFunctionExpression" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression";
|
|
517
|
+
}
|
|
518
|
+
function markExportedBinding(name, state) {
|
|
519
|
+
const bindingNode = state.bindingNodes.get(name);
|
|
520
|
+
if (bindingNode) state.exportedNodes.add(bindingNode);
|
|
521
|
+
}
|
|
522
|
+
function markExportedDeclaration(node, state) {
|
|
523
|
+
state.exportedNodes.add(node);
|
|
524
|
+
if (node.type !== "VariableDeclaration" || !Array.isArray(node.declarations)) return;
|
|
525
|
+
for (const declarator of node.declarations) {
|
|
526
|
+
if (!isAstNode(declarator)) continue;
|
|
527
|
+
const initializer = asAstNode(declarator.init);
|
|
528
|
+
if (initializer && (isClassNode(initializer) || isFunctionNode(initializer))) state.exportedNodes.add(initializer);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function visit(node, state) {
|
|
532
|
+
if (!node) return;
|
|
533
|
+
if (Array.isArray(node)) {
|
|
534
|
+
for (const child of node) visit(child, state);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (state.visited.has(node)) return;
|
|
538
|
+
state.visited.add(node);
|
|
539
|
+
if (node.type === "ExportDefaultDeclaration" || node.type === "ExportNamedDeclaration") {
|
|
540
|
+
if (node.declaration) {
|
|
541
|
+
markExportedDeclaration(node.declaration, state);
|
|
542
|
+
visit(node.declaration, state);
|
|
543
|
+
}
|
|
544
|
+
visitChildren(node, state, /* @__PURE__ */ new Set(["declaration"]));
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (isClassNode(node)) addClassUnit(node, state);
|
|
548
|
+
else if (isFunctionNode(node) || node.type === "MethodDefinition") {
|
|
549
|
+
addFunctionUnit(node, state);
|
|
550
|
+
const methodValue = node.type === "MethodDefinition" ? asAstNode(node.value) : void 0;
|
|
551
|
+
if (methodValue) state.seenFunctions.add(methodValue);
|
|
552
|
+
}
|
|
553
|
+
visitChildren(node, state);
|
|
554
|
+
}
|
|
555
|
+
function visitChildren(node, state, skippedKeys = /* @__PURE__ */ new Set()) {
|
|
556
|
+
for (const [key, value] of Object.entries(node)) {
|
|
557
|
+
if (skippedKeys.has(key) || key === "type" || key === "start" || key === "end" || key === "range") continue;
|
|
558
|
+
if (Array.isArray(value)) {
|
|
559
|
+
for (const child of value) if (isAstNode(child)) {
|
|
560
|
+
inferChildName(node, key, child, state);
|
|
561
|
+
visit(child, state);
|
|
562
|
+
}
|
|
563
|
+
} else if (isAstNode(value)) {
|
|
564
|
+
inferChildName(node, key, value, state);
|
|
565
|
+
visit(value, state);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
//#endregion
|
|
570
|
+
//#region src/core/run.ts
|
|
571
|
+
var AlintRuleExecutionError = class extends Error {
|
|
572
|
+
failure;
|
|
573
|
+
constructor(error, path) {
|
|
574
|
+
const message = errorMessageFrom(error) ?? String(error);
|
|
575
|
+
super(message, { cause: error });
|
|
576
|
+
this.name = "AlintRuleExecutionError";
|
|
577
|
+
this.failure = {
|
|
578
|
+
filePath: path.file.path,
|
|
579
|
+
message,
|
|
580
|
+
ruleId: path.rule.id,
|
|
581
|
+
target: {
|
|
582
|
+
kind: path.target.kind,
|
|
583
|
+
name: path.target.name
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
var AlintRunError = class extends Error {
|
|
589
|
+
failure;
|
|
590
|
+
result;
|
|
591
|
+
constructor(message, result, options = {}) {
|
|
592
|
+
super(message, { cause: options.cause });
|
|
593
|
+
this.name = "AlintRunError";
|
|
594
|
+
this.failure = options.failure;
|
|
595
|
+
this.result = result;
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
async function runAlint(options = {}) {
|
|
599
|
+
const cwd$1 = options.cwd ?? cwd();
|
|
600
|
+
const config = options.config ?? {};
|
|
601
|
+
const setupConfig = options.setupConfig ?? {
|
|
602
|
+
providers: [],
|
|
603
|
+
version: 1
|
|
604
|
+
};
|
|
605
|
+
const clock = options.runner?.clock ?? Date.now;
|
|
606
|
+
const diagnostics = [];
|
|
607
|
+
const usage = createUsageAccumulator();
|
|
608
|
+
const registry = buildRuleRegistry(config);
|
|
609
|
+
const src = createSourceRuntime();
|
|
610
|
+
const normalizedCacheConfig = normalizeRunnerCacheConfig(options.runner?.cache, cwd$1);
|
|
611
|
+
const cacheStore = await createCacheStore({
|
|
612
|
+
cwd: cwd$1,
|
|
613
|
+
enabled: normalizedCacheConfig.enabled,
|
|
614
|
+
location: normalizedCacheConfig.location
|
|
615
|
+
});
|
|
616
|
+
const cacheContext = {
|
|
617
|
+
configHash: stableHash({ rules: config.rules ?? {} }),
|
|
618
|
+
cwd: cwd$1,
|
|
619
|
+
enabled: normalizedCacheConfig.enabled,
|
|
620
|
+
fileEntryKeys: /* @__PURE__ */ new Map(),
|
|
621
|
+
modelHash: stableHash({
|
|
622
|
+
modelOverride: options.modelOverride,
|
|
623
|
+
setupConfig
|
|
624
|
+
}),
|
|
625
|
+
store: cacheStore
|
|
626
|
+
};
|
|
627
|
+
const files = await Promise.all((options.files ?? []).map(async (filePath) => {
|
|
628
|
+
const file = await src.readFile(resolve(cwd$1, filePath));
|
|
629
|
+
return {
|
|
630
|
+
file,
|
|
631
|
+
units: file.language === "javascript" || file.language === "typescript" ? extractJsSourceUnits(file) : void 0
|
|
632
|
+
};
|
|
633
|
+
}));
|
|
634
|
+
let planned = 0;
|
|
635
|
+
const counters = createRuleEndCounters();
|
|
636
|
+
const primaryError = createPrimaryErrorState();
|
|
637
|
+
let filePlans = [];
|
|
638
|
+
let runStartedAt;
|
|
639
|
+
let runError;
|
|
640
|
+
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);
|
|
714
|
+
planned = calculatePlannedExecutions(filePlans);
|
|
715
|
+
runStartedAt = clock();
|
|
716
|
+
options.progress?.onRunStart?.({
|
|
717
|
+
files: filePlans.map((filePlan) => toProgressFilePath(filePlan, files.length)),
|
|
718
|
+
filesTotal: files.length,
|
|
719
|
+
planned,
|
|
720
|
+
rulesTotal: registry.enabledRules.length,
|
|
721
|
+
startedAt: runStartedAt
|
|
722
|
+
});
|
|
723
|
+
await runConcurrently(filePlans.filter((filePlan) => filePlan.targets.length > 0), resolveFileConcurrency(options.runner?.fileConcurrency), (filePlan) => executeFilePlan(filePlan, files.length, clock, counters, diagnostics, usage, cacheContext, options));
|
|
724
|
+
} catch (error) {
|
|
725
|
+
primaryError.set();
|
|
726
|
+
runError = error;
|
|
727
|
+
} finally {
|
|
728
|
+
await reconcileCache(filePlans, cacheContext);
|
|
729
|
+
emitCleanupProgress(() => options.progress?.onRunEnd?.({
|
|
730
|
+
...counters.snapshot(planned),
|
|
731
|
+
diagnostics,
|
|
732
|
+
endedAt: clock(),
|
|
733
|
+
startedAt: runStartedAt ?? clock(),
|
|
734
|
+
usage: usage.toJSON()
|
|
735
|
+
}), primaryError);
|
|
736
|
+
}
|
|
737
|
+
const result = {
|
|
738
|
+
diagnostics,
|
|
739
|
+
usage: usage.toJSON()
|
|
740
|
+
};
|
|
741
|
+
if (runError) throw createAlintRunError(runError, result);
|
|
742
|
+
return result;
|
|
743
|
+
}
|
|
744
|
+
function addTokenCount(base, value) {
|
|
745
|
+
return typeof value === "number" && Number.isFinite(value) ? base + value : base;
|
|
746
|
+
}
|
|
747
|
+
function calculateFilePlanExecutions(filePlan) {
|
|
748
|
+
return filePlan.targets.reduce((total, target) => total + target.executions.length, 0);
|
|
749
|
+
}
|
|
750
|
+
function calculatePlannedExecutions(filePlans) {
|
|
751
|
+
return filePlans.reduce((total, filePlan) => total + filePlan.planned, 0);
|
|
752
|
+
}
|
|
753
|
+
function collectExecutionTargets(ruleRuntimes, preparedFile) {
|
|
754
|
+
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
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return targets;
|
|
807
|
+
}
|
|
808
|
+
function createAlintRunError(error, result) {
|
|
809
|
+
if (error instanceof AlintRunError) return error;
|
|
810
|
+
if (error instanceof AlintRuleExecutionError) return new AlintRunError(error.message, result, {
|
|
811
|
+
cause: errorCauseFrom(error),
|
|
812
|
+
failure: error.failure
|
|
813
|
+
});
|
|
814
|
+
return new AlintRunError(errorMessageFrom(error) ?? String(error), result, { cause: error });
|
|
815
|
+
}
|
|
816
|
+
function createExecutionCacheKey(runtime, target, path, cacheContext) {
|
|
817
|
+
if (!cacheContext.enabled || !runtime.cacheable) return;
|
|
818
|
+
return createCacheKey({
|
|
819
|
+
alintVersion: version,
|
|
820
|
+
configHash: cacheContext.configHash,
|
|
821
|
+
filePath: normalizeCachePath(cacheContext.cwd, path.file.path),
|
|
822
|
+
modelHash: cacheContext.modelHash,
|
|
823
|
+
ruleHash: runtime.ruleHash,
|
|
824
|
+
schemaVersion: 1,
|
|
825
|
+
targetHash: hashText(target.text),
|
|
826
|
+
targetIdentity: target.identity,
|
|
827
|
+
targetKind: target.kind
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
function createPreparedFileExecutionPlans(ruleRuntimes, files, cwd) {
|
|
831
|
+
return files.map((preparedFile, fileOffset) => {
|
|
832
|
+
const targets = collectExecutionTargets(ruleRuntimes, preparedFile);
|
|
833
|
+
const resolveTargetIdentity = createTargetIdentityResolver(targets.map((target) => toTargetIdentityInput(cwd, preparedFile.file.path, target)));
|
|
834
|
+
for (const target of targets) target.identity = resolveTargetIdentity(toTargetIdentityInput(cwd, preparedFile.file.path, target));
|
|
835
|
+
const filePlan = {
|
|
836
|
+
fileIndex: fileOffset + 1,
|
|
837
|
+
planned: 0,
|
|
838
|
+
preparedFile,
|
|
839
|
+
targets
|
|
840
|
+
};
|
|
841
|
+
filePlan.planned = calculateFilePlanExecutions(filePlan);
|
|
842
|
+
return filePlan;
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
function createPrimaryErrorState() {
|
|
846
|
+
let hasError = false;
|
|
847
|
+
return {
|
|
848
|
+
get hasError() {
|
|
849
|
+
return hasError;
|
|
850
|
+
},
|
|
851
|
+
set() {
|
|
852
|
+
hasError = true;
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
function createProgressPath(filePath, ruleId, entry) {
|
|
857
|
+
return {
|
|
858
|
+
file: {
|
|
859
|
+
index: entry.fileIndex,
|
|
860
|
+
path: filePath,
|
|
861
|
+
planned: entry.filePlanned,
|
|
862
|
+
total: entry.fileTotal
|
|
863
|
+
},
|
|
864
|
+
rule: {
|
|
865
|
+
id: ruleId,
|
|
866
|
+
index: entry.ruleIndex,
|
|
867
|
+
total: entry.ruleTotal
|
|
868
|
+
},
|
|
869
|
+
target: {
|
|
870
|
+
index: entry.targetIndex,
|
|
871
|
+
kind: entry.targetKind,
|
|
872
|
+
name: entry.targetName,
|
|
873
|
+
total: entry.targetTotal
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
function createRuleEndCounters() {
|
|
878
|
+
let cached = 0;
|
|
879
|
+
let completed = 0;
|
|
880
|
+
let errored = 0;
|
|
881
|
+
return {
|
|
882
|
+
cache() {
|
|
883
|
+
cached += 1;
|
|
884
|
+
},
|
|
885
|
+
complete() {
|
|
886
|
+
completed += 1;
|
|
887
|
+
},
|
|
888
|
+
error() {
|
|
889
|
+
errored += 1;
|
|
890
|
+
},
|
|
891
|
+
snapshot(planned) {
|
|
892
|
+
return {
|
|
893
|
+
cached,
|
|
894
|
+
completed,
|
|
895
|
+
errored,
|
|
896
|
+
planned
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
function createUsageAccumulator() {
|
|
902
|
+
const records = [];
|
|
903
|
+
let inputTokens = 0;
|
|
904
|
+
let outputTokens = 0;
|
|
905
|
+
let totalTokens = 0;
|
|
906
|
+
return {
|
|
907
|
+
record(record) {
|
|
908
|
+
records.push(record);
|
|
909
|
+
inputTokens = addTokenCount(inputTokens, record.inputTokens);
|
|
910
|
+
outputTokens = addTokenCount(outputTokens, record.outputTokens);
|
|
911
|
+
totalTokens = addTokenCount(totalTokens, record.totalTokens);
|
|
912
|
+
return record;
|
|
913
|
+
},
|
|
914
|
+
toJSON() {
|
|
915
|
+
return {
|
|
916
|
+
inputTokens,
|
|
917
|
+
outputTokens,
|
|
918
|
+
records,
|
|
919
|
+
totalTokens
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
function emitCleanupProgress(callback, primaryError) {
|
|
925
|
+
try {
|
|
926
|
+
callback();
|
|
927
|
+
} catch (error) {
|
|
928
|
+
if (!primaryError.hasError) throw error;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
async function executeFilePlan(filePlan, filesTotal, clock, counters, diagnostics, usage, cacheContext, options) {
|
|
932
|
+
const preparedFile = filePlan.preparedFile;
|
|
933
|
+
const fileStartedAt = clock();
|
|
934
|
+
const fileProgress = {
|
|
935
|
+
index: filePlan.fileIndex,
|
|
936
|
+
path: preparedFile.file.path,
|
|
937
|
+
planned: filePlan.planned,
|
|
938
|
+
total: filesTotal
|
|
939
|
+
};
|
|
940
|
+
options.progress?.onFileStart?.({
|
|
941
|
+
file: fileProgress,
|
|
942
|
+
startedAt: fileStartedAt
|
|
943
|
+
});
|
|
944
|
+
const fileError = createPrimaryErrorState();
|
|
945
|
+
try {
|
|
946
|
+
for (const [targetOffset, target] of filePlan.targets.entries()) {
|
|
947
|
+
const targetPath = createProgressPath(preparedFile.file.path, target.executions[0]?.runtime.enabledRule.id ?? "", {
|
|
948
|
+
fileIndex: filePlan.fileIndex,
|
|
949
|
+
filePlanned: filePlan.planned,
|
|
950
|
+
fileTotal: filesTotal,
|
|
951
|
+
ruleIndex: 1,
|
|
952
|
+
ruleTotal: target.executions.length,
|
|
953
|
+
targetIndex: targetOffset + 1,
|
|
954
|
+
targetKind: target.kind,
|
|
955
|
+
targetName: target.name,
|
|
956
|
+
targetTotal: filePlan.targets.length
|
|
957
|
+
});
|
|
958
|
+
const targetError = createPrimaryErrorState();
|
|
959
|
+
const targetStartedAt = clock();
|
|
960
|
+
options.progress?.onTargetStart?.({
|
|
961
|
+
path: targetPath,
|
|
962
|
+
startedAt: targetStartedAt
|
|
963
|
+
});
|
|
964
|
+
try {
|
|
965
|
+
for (const [executionOffset, execution] of target.executions.entries()) await executeProgressTarget(execution, createProgressPath(preparedFile.file.path, execution.runtime.enabledRule.id, {
|
|
966
|
+
fileIndex: filePlan.fileIndex,
|
|
967
|
+
filePlanned: filePlan.planned,
|
|
968
|
+
fileTotal: filesTotal,
|
|
969
|
+
ruleIndex: executionOffset + 1,
|
|
970
|
+
ruleTotal: target.executions.length,
|
|
971
|
+
targetIndex: targetOffset + 1,
|
|
972
|
+
targetKind: target.kind,
|
|
973
|
+
targetName: target.name,
|
|
974
|
+
targetTotal: filePlan.targets.length
|
|
975
|
+
}), target, clock, counters, diagnostics, usage, cacheContext, options);
|
|
976
|
+
} catch (error) {
|
|
977
|
+
targetError.set();
|
|
978
|
+
throw error;
|
|
979
|
+
} finally {
|
|
980
|
+
emitCleanupProgress(() => options.progress?.onTargetEnd?.({
|
|
981
|
+
endedAt: clock(),
|
|
982
|
+
path: targetPath,
|
|
983
|
+
startedAt: targetStartedAt
|
|
984
|
+
}), targetError);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
} catch (error) {
|
|
988
|
+
fileError.set();
|
|
989
|
+
throw error;
|
|
990
|
+
} finally {
|
|
991
|
+
emitCleanupProgress(() => options.progress?.onFileEnd?.({
|
|
992
|
+
endedAt: clock(),
|
|
993
|
+
file: fileProgress,
|
|
994
|
+
startedAt: fileStartedAt
|
|
995
|
+
}), fileError);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
async function executeProgressTarget(execution, path, target, clock, counters, diagnostics, usage, cacheContext, options) {
|
|
999
|
+
const startedAt = clock();
|
|
1000
|
+
const cacheKey = createExecutionCacheKey(execution.runtime, target, path, cacheContext);
|
|
1001
|
+
const cachedEntry = cacheKey && cacheContext.enabled && execution.runtime.cacheable ? cacheContext.store.get(cacheKey) : void 0;
|
|
1002
|
+
options.progress?.onRuleStart?.({
|
|
1003
|
+
path,
|
|
1004
|
+
startedAt
|
|
1005
|
+
});
|
|
1006
|
+
if (cacheKey && cachedEntry) {
|
|
1007
|
+
rememberFileCacheEntry(cacheContext, path.file.path, cacheKey);
|
|
1008
|
+
try {
|
|
1009
|
+
replayCachedEntry(cachedEntry, path, diagnostics, usage, options);
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
counters.error();
|
|
1012
|
+
try {
|
|
1013
|
+
options.progress?.onRuleEnd?.({
|
|
1014
|
+
cache: "hit",
|
|
1015
|
+
endedAt: clock(),
|
|
1016
|
+
path,
|
|
1017
|
+
startedAt,
|
|
1018
|
+
state: "errored"
|
|
1019
|
+
});
|
|
1020
|
+
} catch {}
|
|
1021
|
+
throw new AlintRuleExecutionError(error, path);
|
|
1022
|
+
}
|
|
1023
|
+
counters.cache();
|
|
1024
|
+
options.progress?.onRuleEnd?.({
|
|
1025
|
+
cache: "hit",
|
|
1026
|
+
endedAt: clock(),
|
|
1027
|
+
path,
|
|
1028
|
+
startedAt,
|
|
1029
|
+
state: "completed"
|
|
1030
|
+
});
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
let handlerError;
|
|
1034
|
+
let handlerSucceeded = false;
|
|
1035
|
+
const cacheDiagnostics = cacheKey ? [] : void 0;
|
|
1036
|
+
const cacheUsage = cacheKey ? [] : void 0;
|
|
1037
|
+
try {
|
|
1038
|
+
await execution.runtime.executionState.run({
|
|
1039
|
+
activeFilePath: path.file.path,
|
|
1040
|
+
cacheDiagnostics,
|
|
1041
|
+
cacheUsage,
|
|
1042
|
+
progressPath: path
|
|
1043
|
+
}, execution.run);
|
|
1044
|
+
handlerSucceeded = true;
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
handlerError = error;
|
|
1047
|
+
}
|
|
1048
|
+
if (handlerSucceeded) {
|
|
1049
|
+
if (cacheKey && cacheContext.enabled && execution.runtime.cacheable) {
|
|
1050
|
+
cacheContext.store.set(cacheKey, {
|
|
1051
|
+
diagnostics: cacheDiagnostics ?? [],
|
|
1052
|
+
filePath: normalizeCachePath(cacheContext.cwd, path.file.path),
|
|
1053
|
+
fingerprint: {
|
|
1054
|
+
alintVersion: version,
|
|
1055
|
+
configHash: cacheContext.configHash,
|
|
1056
|
+
modelHash: cacheContext.modelHash,
|
|
1057
|
+
ruleHash: execution.runtime.ruleHash
|
|
1058
|
+
},
|
|
1059
|
+
target: {
|
|
1060
|
+
hash: hashText(target.text),
|
|
1061
|
+
identity: target.identity,
|
|
1062
|
+
kind: target.kind,
|
|
1063
|
+
loc: target.loc,
|
|
1064
|
+
name: target.name,
|
|
1065
|
+
range: target.range
|
|
1066
|
+
},
|
|
1067
|
+
usage: cacheUsage ?? []
|
|
1068
|
+
});
|
|
1069
|
+
rememberFileCacheEntry(cacheContext, path.file.path, cacheKey);
|
|
1070
|
+
}
|
|
1071
|
+
counters.complete();
|
|
1072
|
+
options.progress?.onRuleEnd?.({
|
|
1073
|
+
cache: "miss",
|
|
1074
|
+
endedAt: clock(),
|
|
1075
|
+
path,
|
|
1076
|
+
startedAt,
|
|
1077
|
+
state: "completed"
|
|
1078
|
+
});
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
counters.error();
|
|
1082
|
+
try {
|
|
1083
|
+
options.progress?.onRuleEnd?.({
|
|
1084
|
+
cache: "miss",
|
|
1085
|
+
endedAt: clock(),
|
|
1086
|
+
path,
|
|
1087
|
+
startedAt,
|
|
1088
|
+
state: "errored"
|
|
1089
|
+
});
|
|
1090
|
+
} catch {}
|
|
1091
|
+
throw new AlintRuleExecutionError(handlerError, path);
|
|
1092
|
+
}
|
|
1093
|
+
function mergeCapabilities(base, extra) {
|
|
1094
|
+
if (!base && !extra) return;
|
|
1095
|
+
return [.../* @__PURE__ */ new Set([...base ?? [], ...extra ?? []])];
|
|
1096
|
+
}
|
|
1097
|
+
function mergeMinContextWindow(base, extra) {
|
|
1098
|
+
if (base === void 0) return extra;
|
|
1099
|
+
if (extra === void 0) return base;
|
|
1100
|
+
return Math.max(base, extra);
|
|
1101
|
+
}
|
|
1102
|
+
function mergeModelRequirement(base, extra) {
|
|
1103
|
+
if (!base && !extra) return;
|
|
1104
|
+
const capabilities = mergeCapabilities(base?.capabilities, extra?.capabilities);
|
|
1105
|
+
const params = {
|
|
1106
|
+
...base?.params ?? {},
|
|
1107
|
+
...extra?.params ?? {}
|
|
1108
|
+
};
|
|
1109
|
+
return {
|
|
1110
|
+
capabilities,
|
|
1111
|
+
minContextWindow: mergeMinContextWindow(base?.minContextWindow, extra?.minContextWindow),
|
|
1112
|
+
params: Object.keys(params).length > 0 ? params : void 0,
|
|
1113
|
+
size: extra?.size ?? base?.size
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
async function reconcileCache(filePlans, cacheContext) {
|
|
1117
|
+
try {
|
|
1118
|
+
for (const filePlan of filePlans) {
|
|
1119
|
+
const file = filePlan.preparedFile.file;
|
|
1120
|
+
const normalizedPath = normalizeCachePath(cacheContext.cwd, file.path);
|
|
1121
|
+
const entries = [...cacheContext.fileEntryKeys.get(normalizedPath) ?? []];
|
|
1122
|
+
cacheContext.store.markFile(file.path, hashText(file.text), entries);
|
|
1123
|
+
}
|
|
1124
|
+
await cacheContext.store.reconcile();
|
|
1125
|
+
} catch {}
|
|
1126
|
+
}
|
|
1127
|
+
function rememberFileCacheEntry(cacheContext, filePath, cacheKey) {
|
|
1128
|
+
const normalizedPath = normalizeCachePath(cacheContext.cwd, filePath);
|
|
1129
|
+
const entries = cacheContext.fileEntryKeys.get(normalizedPath) ?? /* @__PURE__ */ new Set();
|
|
1130
|
+
entries.add(cacheKey);
|
|
1131
|
+
cacheContext.fileEntryKeys.set(normalizedPath, entries);
|
|
1132
|
+
}
|
|
1133
|
+
function replayCachedEntry(entry, path, diagnostics, usage, options) {
|
|
1134
|
+
for (const cachedDiagnostic of entry.diagnostics) {
|
|
1135
|
+
const diagnostic = { ...cachedDiagnostic };
|
|
1136
|
+
diagnostics.push(diagnostic);
|
|
1137
|
+
options.progress?.onDiagnostic?.({
|
|
1138
|
+
diagnostic,
|
|
1139
|
+
diagnostics: [...diagnostics],
|
|
1140
|
+
path
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
for (const cachedUsage of entry.usage) {
|
|
1144
|
+
const record = usage.record({ ...cachedUsage });
|
|
1145
|
+
options.progress?.onUsage?.({
|
|
1146
|
+
path,
|
|
1147
|
+
record,
|
|
1148
|
+
total: usage.toJSON()
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
function resolveFileConcurrency(fileConcurrency) {
|
|
1153
|
+
return fileConcurrency ?? 1;
|
|
1154
|
+
}
|
|
1155
|
+
async function runConcurrently(items, concurrency, runItem) {
|
|
1156
|
+
let firstError;
|
|
1157
|
+
let nextIndex = 0;
|
|
1158
|
+
const workerCount = Math.min(Math.max(concurrency, 1), items.length);
|
|
1159
|
+
async function runWorker() {
|
|
1160
|
+
while (firstError === void 0) {
|
|
1161
|
+
const currentIndex = nextIndex;
|
|
1162
|
+
nextIndex += 1;
|
|
1163
|
+
if (currentIndex >= items.length) return;
|
|
1164
|
+
try {
|
|
1165
|
+
await runItem(items[currentIndex]);
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
firstError ??= error;
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
await Promise.all(Array.from({ length: workerCount }, () => runWorker()));
|
|
1173
|
+
if (firstError !== void 0) throw firstError;
|
|
1174
|
+
}
|
|
1175
|
+
function toDiagnosticModel(model, request) {
|
|
1176
|
+
return {
|
|
1177
|
+
providerId: model.provider.id,
|
|
1178
|
+
requested: request,
|
|
1179
|
+
resolvedId: model.id
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
function toProgressFilePath(filePlan, filesTotal) {
|
|
1183
|
+
return {
|
|
1184
|
+
index: filePlan.fileIndex,
|
|
1185
|
+
path: filePlan.preparedFile.file.path,
|
|
1186
|
+
planned: filePlan.planned,
|
|
1187
|
+
total: filesTotal
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
function toTargetIdentityInput(cwd, filePath, target) {
|
|
1191
|
+
return {
|
|
1192
|
+
filePath: target.kind === "file" ? normalizeCachePath(cwd, filePath) : void 0,
|
|
1193
|
+
kind: target.kind,
|
|
1194
|
+
name: target.name,
|
|
1195
|
+
range: target.range
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
//#endregion
|
|
1199
|
+
//#region src/dsl/define.ts
|
|
1200
|
+
function defineConfig(config) {
|
|
1201
|
+
return config;
|
|
1202
|
+
}
|
|
1203
|
+
function definePlugin(plugin) {
|
|
1204
|
+
return plugin;
|
|
1205
|
+
}
|
|
1206
|
+
function defineRule(rule) {
|
|
1207
|
+
return rule;
|
|
1208
|
+
}
|
|
1209
|
+
//#endregion
|
|
1210
|
+
export { AlintRunError, buildRuleRegistry, createSourceFile, createSourceRuntime, defineConfig, definePlugin, defineRule, extractJsSourceUnits, resolveModel, runAlint, sliceLines, sliceRange };
|