@mnemonik/shared 6.50.0 → 7.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ast/astChunker.d.ts +124 -0
- package/dist/ast/astChunker.d.ts.map +1 -0
- package/dist/ast/astChunker.js +559 -0
- package/dist/ast/astChunker.js.map +1 -0
- package/dist/ast/grammars.d.ts +170 -0
- package/dist/ast/grammars.d.ts.map +1 -0
- package/dist/ast/grammars.js +411 -0
- package/dist/ast/grammars.js.map +1 -0
- package/dist/codeScanner.d.ts +59 -0
- package/dist/codeScanner.d.ts.map +1 -1
- package/dist/codeScanner.js +259 -2
- package/dist/codeScanner.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/queries/bash.tags.scm +7 -0
- package/queries/groovy.tags.scm +15 -0
- package/queries/powershell.tags.scm +14 -0
- package/scripts/vendor-grammars.ts +317 -0
- package/src/ast/astChunker.ts +661 -0
- package/src/ast/grammars.ts +490 -0
- package/src/codeScanner.ts +273 -3
- package/src/index.ts +22 -0
- package/wasm/bash.tags.scm +7 -0
- package/wasm/bash.wasm +0 -0
- package/wasm/c-sharp.tags.scm +23 -0
- package/wasm/c-sharp.wasm +0 -0
- package/wasm/c.tags.scm +9 -0
- package/wasm/c.wasm +0 -0
- package/wasm/cpp.tags.scm +15 -0
- package/wasm/cpp.wasm +0 -0
- package/wasm/elixir.tags.scm +54 -0
- package/wasm/elixir.wasm +0 -0
- package/wasm/go.tags.scm +42 -0
- package/wasm/go.wasm +0 -0
- package/wasm/groovy.tags.scm +15 -0
- package/wasm/groovy.wasm +0 -0
- package/wasm/java.tags.scm +20 -0
- package/wasm/java.wasm +0 -0
- package/wasm/javascript.tags.scm +99 -0
- package/wasm/javascript.wasm +0 -0
- package/wasm/php.tags.scm +40 -0
- package/wasm/php.wasm +0 -0
- package/wasm/powershell.tags.scm +14 -0
- package/wasm/powershell.wasm +0 -0
- package/wasm/python.tags.scm +14 -0
- package/wasm/python.wasm +0 -0
- package/wasm/ruby.tags.scm +64 -0
- package/wasm/ruby.wasm +0 -0
- package/wasm/rust.tags.scm +60 -0
- package/wasm/rust.wasm +0 -0
- package/wasm/scala.tags.scm +66 -0
- package/wasm/scala.wasm +0 -0
- package/wasm/solidity.tags.scm +43 -0
- package/wasm/solidity.wasm +0 -0
- package/wasm/tsx.wasm +0 -0
- package/wasm/typescript.tags.scm +23 -0
- package/wasm/typescript.wasm +0 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The grammar registry: one module owns the question "can we AST-parse this
|
|
3
|
+
* language on this machine, and if not, why not".
|
|
4
|
+
*
|
|
5
|
+
* Every failure mode is a named value (`wasm_missing`, `wasm_load_failed`,
|
|
6
|
+
* `tags_query_missing`, `query_compile_failed`, `not_covered`) rather than a
|
|
7
|
+
* null or a throw, because the consequence of a degraded install is not a crash
|
|
8
|
+
* — it is quietly worse chunks, forever, with nothing in the logs. A caller that
|
|
9
|
+
* gets `unavailable` can fall back to the heuristic chunker AND say why.
|
|
10
|
+
*
|
|
11
|
+
* Artifacts are vendored, not installed: `packages/shared/wasm/<id>.wasm` plus
|
|
12
|
+
* the `<id>.tags.scm` tag queries, committed and published inside
|
|
13
|
+
* `@mnemonik/shared` (see `scripts/vendor-grammars.ts` for how they get there
|
|
14
|
+
* and why `npm pack` rather than devDependencies). A user's install therefore
|
|
15
|
+
* pulls no grammar package, runs no postinstall, needs no compiler and fetches
|
|
16
|
+
* nothing at runtime.
|
|
17
|
+
*
|
|
18
|
+
* Two reports answer "what can this install do" and they are NOT
|
|
19
|
+
* interchangeable. `astArtifactReport` stats the vendored files and costs
|
|
20
|
+
* nothing; `astCapabilityReport` instantiates all 18 grammars, which costs ~690
|
|
21
|
+
* ms and ~75 MB of RSS that is never returned because web-tree-sitter exposes no
|
|
22
|
+
* `Language.delete`. Startup and per-file paths use the first; the second is for
|
|
23
|
+
* a diagnostic a human asked for. Grammars themselves load lazily, one per
|
|
24
|
+
* language, on the first file that needs one.
|
|
25
|
+
*
|
|
26
|
+
* Paths resolve relative to THIS MODULE, never the CWD: the scanner daemon runs
|
|
27
|
+
* from whatever directory the user happens to be in, so a CWD-relative path is
|
|
28
|
+
* the kind of bug that passes every test and fails in the field. `src/ast/` and
|
|
29
|
+
* `dist/ast/` sit at the same depth below the package root, so one relative
|
|
30
|
+
* expression is correct for both the TS sources and the shipped build.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
34
|
+
import { dirname, extname, join } from 'node:path';
|
|
35
|
+
import { fileURLToPath } from 'node:url';
|
|
36
|
+
import { Language, Parser, Query } from 'web-tree-sitter';
|
|
37
|
+
import { warn } from '../logger.js';
|
|
38
|
+
|
|
39
|
+
const AST_LANGUAGE_ID_LIST = [
|
|
40
|
+
'typescript',
|
|
41
|
+
'tsx',
|
|
42
|
+
'javascript',
|
|
43
|
+
'python',
|
|
44
|
+
'go',
|
|
45
|
+
'java',
|
|
46
|
+
'c',
|
|
47
|
+
'cpp',
|
|
48
|
+
'csharp',
|
|
49
|
+
'ruby',
|
|
50
|
+
'php',
|
|
51
|
+
'rust',
|
|
52
|
+
'scala',
|
|
53
|
+
'elixir',
|
|
54
|
+
'solidity',
|
|
55
|
+
'bash',
|
|
56
|
+
'groovy',
|
|
57
|
+
'powershell',
|
|
58
|
+
] as const;
|
|
59
|
+
|
|
60
|
+
/** A language this build can AST-parse. Exactly the vendored set, no aspirations. */
|
|
61
|
+
export type AstLanguageId = (typeof AST_LANGUAGE_ID_LIST)[number];
|
|
62
|
+
|
|
63
|
+
/** The vendored set as a runtime value, for capability reporting and tests. */
|
|
64
|
+
export const AST_LANGUAGE_IDS: readonly AstLanguageId[] = AST_LANGUAGE_ID_LIST;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Ids whose vendored artifact basename differs from the id.
|
|
68
|
+
*
|
|
69
|
+
* `tree-sitter-c-sharp` names its file with a hyphen while every consumer of
|
|
70
|
+
* this registry — `languageForExtension`, chunk metadata, the wire — spells the
|
|
71
|
+
* language `csharp`. Mapping the two explicitly is cheaper than renaming a
|
|
72
|
+
* vendored artifact: the vendoring script, the committed files and the
|
|
73
|
+
* `--check` verifier all agree on `c-sharp`, and a rename would make this
|
|
74
|
+
* module the odd one out for no gain.
|
|
75
|
+
*/
|
|
76
|
+
const ARTIFACT_BASENAME: Partial<Record<AstLanguageId, string>> = {
|
|
77
|
+
csharp: 'c-sharp',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** Vendored artifact basename for an id: `csharp` -> `c-sharp`, else identity. */
|
|
81
|
+
export function grammarArtifactBasename(id: AstLanguageId): string {
|
|
82
|
+
return ARTIFACT_BASENAME[id] ?? id;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Ordered tag-query files composing each grammar's EFFECTIVE query. THE source
|
|
87
|
+
* of truth — `scripts/vendor-grammars.ts` imports this rather than keeping its
|
|
88
|
+
* own copy, because two chains that can drift is precisely the defect this
|
|
89
|
+
* layer exists to remove.
|
|
90
|
+
*
|
|
91
|
+
* Tag queries are DELTAS, not complete definitions. tree-sitter-typescript's
|
|
92
|
+
* query holds only the TypeScript-specific patterns (`function_signature`,
|
|
93
|
+
* `interface_declaration`, `abstract_class_declaration`);
|
|
94
|
+
* `function_declaration`, `class_declaration`, `method_definition` and arrow
|
|
95
|
+
* functions all live in javascript's query and are meant to be inherited.
|
|
96
|
+
* Nothing in the query engine resolves that — composing the chain is the
|
|
97
|
+
* consumer's job.
|
|
98
|
+
*
|
|
99
|
+
* Measured cost of not composing it: 948 definitions across 1,037 TypeScript
|
|
100
|
+
* files in this repo, against 6,471 once javascript's query was prepended.
|
|
101
|
+
* `cpp` inherits `c` for the same reason.
|
|
102
|
+
*
|
|
103
|
+
* Listed explicitly rather than inferred, and typed as a complete record, so
|
|
104
|
+
* adding a language without deciding its chain is a compile error instead of a
|
|
105
|
+
* query that compiles and quietly under-matches.
|
|
106
|
+
*/
|
|
107
|
+
export const QUERY_CHAIN: Readonly<Record<AstLanguageId, readonly AstLanguageId[]>> = {
|
|
108
|
+
typescript: ['javascript', 'typescript'],
|
|
109
|
+
tsx: ['javascript', 'typescript'],
|
|
110
|
+
javascript: ['javascript'],
|
|
111
|
+
python: ['python'],
|
|
112
|
+
go: ['go'],
|
|
113
|
+
java: ['java'],
|
|
114
|
+
c: ['c'],
|
|
115
|
+
cpp: ['c', 'cpp'],
|
|
116
|
+
csharp: ['csharp'],
|
|
117
|
+
ruby: ['ruby'],
|
|
118
|
+
php: ['php'],
|
|
119
|
+
rust: ['rust'],
|
|
120
|
+
scala: ['scala'],
|
|
121
|
+
elixir: ['elixir'],
|
|
122
|
+
solidity: ['solidity'],
|
|
123
|
+
bash: ['bash'],
|
|
124
|
+
groovy: ['groovy'],
|
|
125
|
+
powershell: ['powershell'],
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Extensions whose grammar the language string alone gets WRONG.
|
|
130
|
+
*
|
|
131
|
+
* `.ts` and `.tsx` both report language `typescript` and need different
|
|
132
|
+
* grammars out of the same npm package — a `.tsx` file parsed by the typescript
|
|
133
|
+
* grammar errors on the first JSX element. This is the entire reason
|
|
134
|
+
* `resolveAstLanguage` takes an extension at all.
|
|
135
|
+
*/
|
|
136
|
+
const EXTENSION_GRAMMARS: Readonly<Record<string, AstLanguageId>> = {
|
|
137
|
+
'.tsx': 'tsx',
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Scanner language string -> grammar. Keyed on what `languageForExtension`
|
|
142
|
+
* actually returns, so this table and the extension table cannot disagree about
|
|
143
|
+
* what a `.zsh` file is.
|
|
144
|
+
*
|
|
145
|
+
* Absences are deliberate answers, not gaps. `markdown` is missing because
|
|
146
|
+
* headers already are a document's semantic unit and 10,483 production chunks
|
|
147
|
+
* depend on `chunkMarkdown` keeping them; `kotlin`, `swift`, `dart` and friends
|
|
148
|
+
* are missing because no loadable grammar is vendored yet. Both resolve to
|
|
149
|
+
* `null` and the heuristic chunker takes over.
|
|
150
|
+
*/
|
|
151
|
+
const LANGUAGE_GRAMMARS: Readonly<Record<string, AstLanguageId>> = {
|
|
152
|
+
typescript: 'typescript',
|
|
153
|
+
javascript: 'javascript',
|
|
154
|
+
python: 'python',
|
|
155
|
+
go: 'go',
|
|
156
|
+
java: 'java',
|
|
157
|
+
c: 'c',
|
|
158
|
+
cpp: 'cpp',
|
|
159
|
+
csharp: 'csharp',
|
|
160
|
+
ruby: 'ruby',
|
|
161
|
+
php: 'php',
|
|
162
|
+
rust: 'rust',
|
|
163
|
+
scala: 'scala',
|
|
164
|
+
elixir: 'elixir',
|
|
165
|
+
solidity: 'solidity',
|
|
166
|
+
shell: 'bash',
|
|
167
|
+
groovy: 'groovy',
|
|
168
|
+
powershell: 'powershell',
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/** A loaded grammar plus its compiled query. Compiled once, reused per file. */
|
|
172
|
+
export interface GrammarBinding {
|
|
173
|
+
id: AstLanguageId;
|
|
174
|
+
language: Language;
|
|
175
|
+
query: Query;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Why a grammar is not usable, always with a human-readable detail. */
|
|
179
|
+
export type GrammarUnavailableReason =
|
|
180
|
+
| 'not_covered'
|
|
181
|
+
| 'wasm_missing'
|
|
182
|
+
| 'wasm_load_failed'
|
|
183
|
+
| 'tags_query_missing'
|
|
184
|
+
| 'query_compile_failed';
|
|
185
|
+
|
|
186
|
+
export type GrammarLoad =
|
|
187
|
+
{ ok: GrammarBinding } | { unavailable: GrammarUnavailableReason; detail: string };
|
|
188
|
+
|
|
189
|
+
const WASM_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'wasm');
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The vendored artifact directory, resolved from THIS module.
|
|
193
|
+
*
|
|
194
|
+
* Exported because a test of a DEGRADED install has to break artifacts, and
|
|
195
|
+
* breaking the tracked ones is not an option: the unit project runs test files
|
|
196
|
+
* in parallel forks, so renaming `ruby.wasm` aside is visible to every other
|
|
197
|
+
* file for as long as it lasts, and any file that cold-loads Ruby inside that
|
|
198
|
+
* window caches a `wasm_missing` this checkout does not have. Copy from here
|
|
199
|
+
* into a temp directory and break the copy — `loadGrammar` takes the directory.
|
|
200
|
+
*/
|
|
201
|
+
export const VENDORED_ARTIFACT_DIR = WASM_DIR;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Which grammar, if any, should parse a file with this extension and language.
|
|
205
|
+
*
|
|
206
|
+
* `null` means "chunk it heuristically" and is a legitimate answer for most of
|
|
207
|
+
* the allowlist. Callers pass the language string `languageForExtension`
|
|
208
|
+
* produced; the extension only decides cases where the language string is
|
|
209
|
+
* ambiguous (`.ts` vs `.tsx`).
|
|
210
|
+
*/
|
|
211
|
+
export function resolveAstLanguage(extension: string, language: string): AstLanguageId | null {
|
|
212
|
+
const byExtension = EXTENSION_GRAMMARS[normalizeExtension(extension)];
|
|
213
|
+
if (byExtension) return byExtension;
|
|
214
|
+
return LANGUAGE_GRAMMARS[language.trim().toLowerCase()] ?? null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Accepts `.TSX`, `tsx`, or a whole path, and answers the lowercase dotted
|
|
219
|
+
* extension. Mirrors `languageForExtension`'s ordering — `extname` first, so a
|
|
220
|
+
* dotfile carrying a real extension ('.eslintrc.js') is not swallowed whole.
|
|
221
|
+
*/
|
|
222
|
+
function normalizeExtension(raw: string): string {
|
|
223
|
+
const lower = raw.trim().toLowerCase();
|
|
224
|
+
if (!lower) return '';
|
|
225
|
+
const fromPath = extname(lower);
|
|
226
|
+
if (fromPath) return fromPath;
|
|
227
|
+
return lower.startsWith('.') ? lower : `.${lower}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let parserInit: Promise<void> | null = null;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* `Parser.init()` exactly once per process. web-tree-sitter instantiates its
|
|
234
|
+
* WebAssembly runtime here; calling it per file would dominate the scan.
|
|
235
|
+
*/
|
|
236
|
+
function initParser(): Promise<void> {
|
|
237
|
+
parserInit ??= Parser.init();
|
|
238
|
+
return parserInit;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Successes AND failures, keyed by artifact directory + id. Caching the failures
|
|
243
|
+
* is the point: a checkout missing one `.wasm` must not stat-and-fail once per
|
|
244
|
+
* file for the whole repo walk, and must not log the same warning 30,000 times.
|
|
245
|
+
*
|
|
246
|
+
* The cached value is the in-flight promise, so two concurrent callers share
|
|
247
|
+
* one `Language.load` rather than instantiating the grammar twice.
|
|
248
|
+
*/
|
|
249
|
+
const loads = new Map<string, Promise<GrammarLoad>>();
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Ids whose `Language` has actually been instantiated, same keying as `loads`.
|
|
253
|
+
*
|
|
254
|
+
* A ledger rather than a cache index, because the entry can never be removed:
|
|
255
|
+
* a loaded Language cannot be freed (see `attemptLoad`), so this set IS the
|
|
256
|
+
* permanent wasm memory this process is holding. `loadedGrammarIds` reads it,
|
|
257
|
+
* and the startup path is asserted against it.
|
|
258
|
+
*/
|
|
259
|
+
const instantiated = new Set<string>();
|
|
260
|
+
|
|
261
|
+
// NUL separator, escaped rather than literal: the directory half is an
|
|
262
|
+
// arbitrary path, so the separator has to be a byte a path cannot contain.
|
|
263
|
+
const cacheKey = (artifactDir: string, id: string): string => `${artifactDir}\0${id}`;
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Load (or return the cached) grammar binding for `id`. Never throws.
|
|
267
|
+
*
|
|
268
|
+
* `artifactDir` defaults to the vendored directory and every production caller
|
|
269
|
+
* omits it. It exists so a degraded install can be exercised against a COPY of
|
|
270
|
+
* the artifacts (see `VENDORED_ARTIFACT_DIR`); the cache is keyed by directory,
|
|
271
|
+
* so a broken copy cannot poison the real grammar's cache entry.
|
|
272
|
+
*/
|
|
273
|
+
export function loadGrammar(
|
|
274
|
+
id: AstLanguageId,
|
|
275
|
+
artifactDir: string = WASM_DIR
|
|
276
|
+
): Promise<GrammarLoad> {
|
|
277
|
+
const key = cacheKey(artifactDir, id);
|
|
278
|
+
const cached = loads.get(key);
|
|
279
|
+
if (cached) return cached;
|
|
280
|
+
const pending = loadUncached(id, artifactDir);
|
|
281
|
+
loads.set(key, pending);
|
|
282
|
+
return pending;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Which grammars this process has instantiated — what is resident in wasm
|
|
287
|
+
* memory right now, not what is vendored or what was asked for.
|
|
288
|
+
*
|
|
289
|
+
* Exists because the cost is permanent and therefore worth being able to state:
|
|
290
|
+
* `astArtifactReport` reports it, and a test asserts the scanner's startup line
|
|
291
|
+
* leaves it empty.
|
|
292
|
+
*/
|
|
293
|
+
export function loadedGrammarIds(artifactDir: string = WASM_DIR): AstLanguageId[] {
|
|
294
|
+
return AST_LANGUAGE_IDS.filter((id) => instantiated.has(cacheKey(artifactDir, id)));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function loadUncached(id: AstLanguageId, artifactDir: string): Promise<GrammarLoad> {
|
|
298
|
+
const result = await attemptLoad(id, artifactDir);
|
|
299
|
+
if ('unavailable' in result) {
|
|
300
|
+
// Logged once per id thanks to the cache above. A degradation nobody can
|
|
301
|
+
// see in the logs is indistinguishable from working software.
|
|
302
|
+
warn('ast grammar unavailable', { id, reason: result.unavailable, detail: result.detail });
|
|
303
|
+
}
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* The on-disk half of a load: artifacts present, query chain composed. No wasm
|
|
309
|
+
* is instantiated, so this is what the cheap report and the real load SHARE —
|
|
310
|
+
* two separate presence checks that could disagree is how a report starts
|
|
311
|
+
* claiming a grammar the loader rejects.
|
|
312
|
+
*/
|
|
313
|
+
type ArtifactResolution =
|
|
314
|
+
| { wasmPath: string; source: string }
|
|
315
|
+
| { unavailable: 'wasm_missing' | 'tags_query_missing'; detail: string };
|
|
316
|
+
|
|
317
|
+
function resolveArtifacts(id: AstLanguageId, artifactDir: string): ArtifactResolution {
|
|
318
|
+
const wasmPath = join(artifactDir, `${grammarArtifactBasename(id)}.wasm`);
|
|
319
|
+
if (!isNonEmptyFile(wasmPath)) {
|
|
320
|
+
return { unavailable: 'wasm_missing', detail: `missing or empty grammar artifact ${wasmPath}` };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Composed BEFORE the grammar loads: reading three small files is cheaper
|
|
324
|
+
// than instantiating a megabyte of WebAssembly only to discard it.
|
|
325
|
+
let source = '';
|
|
326
|
+
for (const part of QUERY_CHAIN[id]) {
|
|
327
|
+
const tagsPath = join(artifactDir, `${grammarArtifactBasename(part)}.tags.scm`);
|
|
328
|
+
if (!isNonEmptyFile(tagsPath)) {
|
|
329
|
+
return {
|
|
330
|
+
unavailable: 'tags_query_missing',
|
|
331
|
+
detail: `${id} needs the query chain [${QUERY_CHAIN[id].join(', ')}] but ${tagsPath} is missing or empty`,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
source += `${readFileSync(tagsPath, 'utf8')}\n`;
|
|
336
|
+
} catch (err) {
|
|
337
|
+
return {
|
|
338
|
+
unavailable: 'tags_query_missing',
|
|
339
|
+
detail: `${id}: could not read ${tagsPath}: ${describe(err)}`,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return { wasmPath, source };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function attemptLoad(id: AstLanguageId, artifactDir: string): Promise<GrammarLoad> {
|
|
348
|
+
if (!AST_LANGUAGE_IDS.includes(id)) {
|
|
349
|
+
return {
|
|
350
|
+
unavailable: 'not_covered',
|
|
351
|
+
detail: `${String(id)} is not a vendored grammar; expected one of ${AST_LANGUAGE_IDS.join(', ')}`,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const artifacts = resolveArtifacts(id, artifactDir);
|
|
356
|
+
if ('unavailable' in artifacts) return artifacts;
|
|
357
|
+
const { wasmPath, source } = artifacts;
|
|
358
|
+
|
|
359
|
+
let language: Language;
|
|
360
|
+
try {
|
|
361
|
+
await initParser();
|
|
362
|
+
language = await Language.load(wasmPath);
|
|
363
|
+
} catch (err) {
|
|
364
|
+
// Presence is not loadability: tree-sitter-dart ships a 741 KB .wasm that
|
|
365
|
+
// Language.load rejects because the artifact predates the current wasm ABI.
|
|
366
|
+
return {
|
|
367
|
+
unavailable: 'wasm_load_failed',
|
|
368
|
+
detail: `${wasmPath} exists but web-tree-sitter rejected it (ABI mismatch?): ${describe(err)}`,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
instantiated.add(cacheKey(artifactDir, id));
|
|
372
|
+
|
|
373
|
+
// A loaded Language cannot be freed: web-tree-sitter 0.26.11 exposes
|
|
374
|
+
// `delete()` on Tree and Query but NOT on Language. The failure results below
|
|
375
|
+
// are cached, so a grammar whose query is broken leaks its Language exactly
|
|
376
|
+
// once per process rather than once per file — which is why the cache matters
|
|
377
|
+
// for more than latency. Trees are a different story and must be deleted per
|
|
378
|
+
// file by the chunker; the JS heap does not GC wasm memory.
|
|
379
|
+
let query: Query;
|
|
380
|
+
try {
|
|
381
|
+
query = new Query(language, source);
|
|
382
|
+
} catch (err) {
|
|
383
|
+
return {
|
|
384
|
+
unavailable: 'query_compile_failed',
|
|
385
|
+
detail: `${id} tag query [${QUERY_CHAIN[id].join(', ')}] failed to compile: ${describe(err)}`,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!query.captureNames.some((name) => name.startsWith('definition.'))) {
|
|
390
|
+
// Compiles, matches, names nothing: the chunker would emit only raw chunks
|
|
391
|
+
// and look like it was working. Unavailable is the honest answer.
|
|
392
|
+
query.delete();
|
|
393
|
+
return {
|
|
394
|
+
unavailable: 'query_compile_failed',
|
|
395
|
+
detail: `${id} tag query [${QUERY_CHAIN[id].join(', ')}] compiled but captures no definition.* — it would name nothing`,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return { ok: { id, language, query } };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const isNonEmptyFile = (path: string): boolean => existsSync(path) && statSync(path).size > 0;
|
|
403
|
+
|
|
404
|
+
const describe = (err: unknown): string =>
|
|
405
|
+
err instanceof Error ? err.message.slice(0, 200) || err.constructor.name : String(err);
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* What this install has VENDORED, answered without instantiating anything.
|
|
409
|
+
*
|
|
410
|
+
* Every check is a `statSync` plus a few hundred bytes of `.scm`: the grammar's
|
|
411
|
+
* `.wasm` is present and non-empty, and so is every tag-query file in its chain.
|
|
412
|
+
* Microseconds, and zero permanent memory — which is what makes it the right
|
|
413
|
+
* answer for a startup line (see `logAstCapabilityOnce`).
|
|
414
|
+
*
|
|
415
|
+
* It shares `resolveArtifacts` with the real loader, so it cannot report a
|
|
416
|
+
* grammar as vendored that `loadGrammar` would reject for a missing artifact.
|
|
417
|
+
* What it deliberately cannot see is the two failure modes that require the
|
|
418
|
+
* grammar in memory — `wasm_load_failed` and `query_compile_failed`. Those are
|
|
419
|
+
* reported by `astCapabilityReport` when a human asks, and warned once per
|
|
420
|
+
* language by the scan path on the first file that needs them
|
|
421
|
+
* (`grammar_unavailable` in codeScanner); paying ~75 MB up front to pre-answer
|
|
422
|
+
* the question for 18 languages a repo probably does not contain is the wrong
|
|
423
|
+
* trade.
|
|
424
|
+
*
|
|
425
|
+
* `loaded` is what is resident so far, which for a fresh process is nothing:
|
|
426
|
+
* grammars load lazily, per language, on the first file that needs one.
|
|
427
|
+
*/
|
|
428
|
+
export function astArtifactReport(artifactDir: string = WASM_DIR): {
|
|
429
|
+
vendored: AstLanguageId[];
|
|
430
|
+
loaded: AstLanguageId[];
|
|
431
|
+
missing: Array<{
|
|
432
|
+
id: AstLanguageId;
|
|
433
|
+
reason: 'wasm_missing' | 'tags_query_missing';
|
|
434
|
+
detail: string;
|
|
435
|
+
}>;
|
|
436
|
+
} {
|
|
437
|
+
const vendored: AstLanguageId[] = [];
|
|
438
|
+
const missing: Array<{
|
|
439
|
+
id: AstLanguageId;
|
|
440
|
+
reason: 'wasm_missing' | 'tags_query_missing';
|
|
441
|
+
detail: string;
|
|
442
|
+
}> = [];
|
|
443
|
+
|
|
444
|
+
for (const id of AST_LANGUAGE_IDS) {
|
|
445
|
+
const artifacts = resolveArtifacts(id, artifactDir);
|
|
446
|
+
if ('unavailable' in artifacts) {
|
|
447
|
+
missing.push({ id, reason: artifacts.unavailable, detail: artifacts.detail });
|
|
448
|
+
} else {
|
|
449
|
+
vendored.push(id);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return { vendored, loaded: loadedGrammarIds(artifactDir), missing };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* What this install can actually AST-PARSE, and why not for the rest. Loads
|
|
458
|
+
* every vendored grammar and compiles every query, so it catches the two things
|
|
459
|
+
* `astArtifactReport` cannot: an artifact that exists and does not load, and a
|
|
460
|
+
* query that does not compile or names no definitions.
|
|
461
|
+
*
|
|
462
|
+
* NOT free, and the cost is memory rather than only latency: measured on this
|
|
463
|
+
* checkout, ~690 ms and ~75 MB of RSS that is never given back. web-tree-sitter
|
|
464
|
+
* 0.26.11 exposes no `Language.delete` (see `attemptLoad`), so all 18 grammars
|
|
465
|
+
* stay resident for the life of the process — including the fifteen a given repo
|
|
466
|
+
* has no files for. Caching makes the second call free; it does not make the
|
|
467
|
+
* first one cheap.
|
|
468
|
+
*
|
|
469
|
+
* So: fine for a diagnostic a human asked for (`doctor`, a `--check` script,
|
|
470
|
+
* this suite). Wrong for a daemon's startup line — that is `astArtifactReport`.
|
|
471
|
+
*/
|
|
472
|
+
export async function astCapabilityReport(artifactDir: string = WASM_DIR): Promise<{
|
|
473
|
+
available: AstLanguageId[];
|
|
474
|
+
unavailable: Array<{ id: AstLanguageId; reason: GrammarUnavailableReason; detail: string }>;
|
|
475
|
+
}> {
|
|
476
|
+
const available: AstLanguageId[] = [];
|
|
477
|
+
const unavailable: Array<{
|
|
478
|
+
id: AstLanguageId;
|
|
479
|
+
reason: GrammarUnavailableReason;
|
|
480
|
+
detail: string;
|
|
481
|
+
}> = [];
|
|
482
|
+
|
|
483
|
+
for (const id of AST_LANGUAGE_IDS) {
|
|
484
|
+
const loaded = await loadGrammar(id, artifactDir);
|
|
485
|
+
if ('ok' in loaded) available.push(id);
|
|
486
|
+
else unavailable.push({ id, reason: loaded.unavailable, detail: loaded.detail });
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return { available, unavailable };
|
|
490
|
+
}
|