grammars.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  1. /**
  2. * Grammar Loading and Caching
  3. *
  4. * Uses web-tree-sitter (WASM) for universal cross-platform support.
  5. * Grammars are loaded lazily — only languages actually present in the project
  6. * are compiled, keeping V8 WASM memory pressure low on large codebases.
  7. */
  8. import * as path from 'path';
  9. import * as fsp from 'fs/promises';
  10. import { Parser, Language as WasmLanguage } from 'web-tree-sitter';
  11. import { Language } from '../types';
  12. export type GrammarLanguage = Exclude<Language, 'svelte' | 'vue' | 'astro' | 'liquid' | 'razor' | 'yaml' | 'twig' | 'xml' | 'properties' | 'unknown'>;
  13. /**
  14. * WASM filename map — maps each language to its .wasm grammar file
  15. * in the tree-sitter-wasms package.
  16. */
  17. const WASM_GRAMMAR_FILES: Record<GrammarLanguage, string> = {
  18. typescript: 'tree-sitter-typescript.wasm',
  19. tsx: 'tree-sitter-tsx.wasm',
  20. javascript: 'tree-sitter-javascript.wasm',
  21. jsx: 'tree-sitter-javascript.wasm',
  22. python: 'tree-sitter-python.wasm',
  23. go: 'tree-sitter-go.wasm',
  24. rust: 'tree-sitter-rust.wasm',
  25. java: 'tree-sitter-java.wasm',
  26. c: 'tree-sitter-c.wasm',
  27. cpp: 'tree-sitter-cpp.wasm',
  28. csharp: 'tree-sitter-c_sharp.wasm',
  29. php: 'tree-sitter-php.wasm',
  30. ruby: 'tree-sitter-ruby.wasm',
  31. swift: 'tree-sitter-swift.wasm',
  32. kotlin: 'tree-sitter-kotlin.wasm',
  33. dart: 'tree-sitter-dart.wasm',
  34. pascal: 'tree-sitter-pascal.wasm',
  35. scala: 'tree-sitter-scala.wasm',
  36. lua: 'tree-sitter-lua.wasm',
  37. r: 'tree-sitter-r.wasm',
  38. luau: 'tree-sitter-luau.wasm',
  39. objc: 'tree-sitter-objc.wasm',
  40. cfml: 'tree-sitter-cfml.wasm',
  41. cfscript: 'tree-sitter-cfscript.wasm',
  42. cfquery: 'tree-sitter-cfquery.wasm',
  43. cobol: 'tree-sitter-cobol.wasm',
  44. vbnet: 'tree-sitter-vbnet.wasm',
  45. erlang: 'tree-sitter-erlang.wasm',
  46. solidity: 'tree-sitter-solidity.wasm',
  47. terraform: 'tree-sitter-terraform.wasm',
  48. arkts: 'tree-sitter-arkts.wasm',
  49. nix: 'tree-sitter-nix.wasm',
  50. };
  51. /**
  52. * File extension to Language mapping
  53. */
  54. export const EXTENSION_MAP: Record<string, Language> = {
  55. '.ts': 'typescript',
  56. '.tsx': 'tsx',
  57. // ESM/CJS TypeScript module extensions — parsed as TS (no JSX). (#366)
  58. '.mts': 'typescript',
  59. '.cts': 'typescript',
  60. // ArkTS (HarmonyOS / OpenHarmony) — a TypeScript superset with declarative
  61. // UI (`@Component struct` + `build()`). Own grammar (a tree-sitter-typescript
  62. // -style fork); plain `.ts` in an ArkTS project stays TypeScript. (#648)
  63. '.ets': 'arkts',
  64. '.js': 'javascript',
  65. '.mjs': 'javascript',
  66. '.cjs': 'javascript',
  67. // SAP HANA XS Classic server-side JavaScript. (#556)
  68. '.xsjs': 'javascript',
  69. '.xsjslib': 'javascript',
  70. '.jsx': 'jsx',
  71. '.py': 'python',
  72. '.pyw': 'python',
  73. '.go': 'go',
  74. '.rs': 'rust',
  75. '.java': 'java',
  76. '.c': 'c',
  77. '.h': 'c', // Could also be C++, defaulting to C
  78. '.cpp': 'cpp',
  79. '.cc': 'cpp',
  80. '.cxx': 'cpp',
  81. '.hpp': 'cpp',
  82. '.hxx': 'cpp',
  83. '.cs': 'csharp',
  84. // ASP.NET Razor / Blazor markup — custom RazorExtractor (links @model/@inject/
  85. // component tags to their C# types; markup isn't a tree-sitter grammar).
  86. '.cshtml': 'razor',
  87. '.razor': 'razor',
  88. '.php': 'php',
  89. // Drupal-specific PHP file extensions
  90. '.module': 'php',
  91. '.install': 'php',
  92. '.theme': 'php',
  93. '.inc': 'php',
  94. // YAML (used for Drupal routing files; no symbol extraction, file-level tracking only)
  95. '.yml': 'yaml',
  96. '.yaml': 'yaml',
  97. // Twig templates (file-level tracking only, no symbol extraction)
  98. '.twig': 'twig',
  99. '.rb': 'ruby',
  100. '.rake': 'ruby',
  101. '.swift': 'swift',
  102. '.kt': 'kotlin',
  103. '.kts': 'kotlin',
  104. '.dart': 'dart',
  105. '.liquid': 'liquid',
  106. '.svelte': 'svelte',
  107. '.vue': 'vue',
  108. '.astro': 'astro',
  109. '.r': 'r',
  110. '.pas': 'pascal',
  111. '.dpr': 'pascal',
  112. '.dpk': 'pascal',
  113. '.lpr': 'pascal',
  114. '.dfm': 'pascal',
  115. '.fmx': 'pascal',
  116. '.scala': 'scala',
  117. '.sc': 'scala',
  118. '.lua': 'lua',
  119. '.luau': 'luau',
  120. '.m': 'objc',
  121. '.mm': 'objc',
  122. '.sol': 'solidity',
  123. // CFML: .cfc/.cfm parse with the tag-aware `cfml` grammar (custom CfmlExtractor
  124. // dialect-switches to cfscript for bare-script content); .cfs is pure CFScript.
  125. '.cfc': 'cfml',
  126. '.cfm': 'cfml',
  127. '.cfs': 'cfscript',
  128. // Metal Shading Language ≈ C++14: the C++ grammar extracts its functions,
  129. // structs, and calls. MSL-specific `[[attribute]]` annotations are blanked
  130. // pre-parse for `.metal` files (see blankMetalAttributes in c-cpp.ts). (#1121)
  131. '.metal': 'cpp',
  132. // CUDA ≈ C++ plus execution-space specifiers (`__global__` …) and
  133. // `<<<grid, block>>>` kernel-launch syntax: the C++ grammar extracts its
  134. // functions/structs/classes/calls once blankCudaConstructs (pre-parse; gated
  135. // by these extensions OR by content for CUDA living in `.h`/`.hpp` headers —
  136. // see c-cpp.ts) blanks the CUDA-only tokens. (#387)
  137. '.cu': 'cpp',
  138. '.cuh': 'cpp',
  139. '.nix': 'nix',
  140. // XML: file-level tracking; the MyBatis extractor matches `<mapper namespace="...">`
  141. // shape and emits SQL-statement nodes (other XML returns empty).
  142. '.xml': 'xml',
  143. // COBOL: programs (.cbl/.cob) and copybooks (.cpy). Vendored grammar
  144. // (patched yutaro-sakamoto/tree-sitter-cobol) handles fixed-format column
  145. // rules, EXEC CICS/SQL blocks, and standalone copybook fragments.
  146. '.cbl': 'cobol',
  147. '.cob': 'cobol',
  148. '.cobol': 'cobol',
  149. '.cpy': 'cobol',
  150. // VB.NET: vendored grammar (patched govindbanura/tree-sitter-vbnet) — classes,
  151. // modules, interfaces, structures, properties, events, Handles clauses, LINQ.
  152. '.vb': 'vbnet',
  153. // Erlang: modules (.erl) and header files (.hrl). Vendored WhatsApp/
  154. // tree-sitter-erlang grammar (the ELP grammar).
  155. '.erl': 'erlang',
  156. '.hrl': 'erlang',
  157. // escripts parse natively — the grammar has a first-class `shebang` node.
  158. // (`.app`/`.app.src` resource files route via isErlangAppFile below: their
  159. // last-dot extension is too generic for this map.)
  160. '.escript': 'erlang',
  161. // Spring config: `application.properties` / `application-*.properties`. Same
  162. // shape as the `.yml` variants — the YAML/properties extractor emits one node
  163. // per leaf key, and the Spring resolver links `@Value("${k}")` references.
  164. '.properties': 'properties',
  165. // Terraform / OpenTofu / HCL config — tree-sitter-terraform dialect of HCL.
  166. '.tf': 'terraform',
  167. '.tfvars': 'terraform',
  168. '.tofu': 'terraform',
  169. };
  170. /**
  171. * Whether a file is one CodeGraph can parse, based purely on its extension.
  172. * This is the single source of truth for "should we index this file" — derived
  173. * from EXTENSION_MAP so parser support and indexing selection never drift.
  174. *
  175. * `overrides` is the project's validated custom extension → language map (from
  176. * `codegraph.json`); when present its extensions count as indexable in addition
  177. * to the built-ins. Omitting it is byte-identical to the zero-config behavior.
  178. */
  179. export function isSourceFile(filePath: string, overrides?: Record<string, Language>): boolean {
  180. if (isPlayRoutesFile(filePath)) return true; // Play `conf/routes` is extensionless
  181. if (isShopifyLiquidJson(filePath)) return true; // Shopify OS 2.0 JSON templates / section groups
  182. if (isErlangAppFile(filePath)) return true; // OTP `.app`/`.app.src` resource files
  183. const dot = filePath.lastIndexOf('.');
  184. if (dot < 0) return false;
  185. const ext = filePath.slice(dot).toLowerCase();
  186. return ext in EXTENSION_MAP || (!!overrides && ext in overrides);
  187. }
  188. /**
  189. * Shopify OS 2.0 JSON template (`templates/*.json`) or section group
  190. * (`sections/*.json`) — these reference sections by `"type"`, so the Liquid
  191. * extractor links them. (config/ + locales/ JSON have no section refs.)
  192. */
  193. export function isShopifyLiquidJson(filePath: string): boolean {
  194. // Allow nested template dirs (`templates/customers/login.json`), not just
  195. // top-level (`templates/product.json`).
  196. return /(^|\/)(templates|sections)\/.+\.json$/i.test(filePath);
  197. }
  198. /**
  199. * OTP application resource file: `<app>.app.src` (checked into every rebar3/
  200. * erlang.mk app) or its compiled `<app>.app`. Erlang TERMS, not forms — the
  201. * grammar parses them as top-level expressions, and the Erlang extractor's
  202. * application-tuple handler turns `{mod, {Mod, _}}` and `{applications, […]}`
  203. * into entry-module and dependency edges. Routed by full suffix because the
  204. * last-dot extension (`.src`) is far too generic for EXTENSION_MAP.
  205. */
  206. export function isErlangAppFile(filePath: string): boolean {
  207. return /\.app(?:\.src)?$/i.test(filePath);
  208. }
  209. /**
  210. * Play Framework routes file: the extensionless `conf/routes` (and included
  211. * `conf/*.routes`). No grammar — route extraction is done by the Play framework
  212. * resolver, so it's processed through the no-grammar (`yaml`-style) path.
  213. */
  214. export function isPlayRoutesFile(filePath: string): boolean {
  215. return (
  216. filePath === 'conf/routes' ||
  217. filePath.endsWith('/conf/routes') ||
  218. filePath.endsWith('.routes')
  219. );
  220. }
  221. /**
  222. * Caches for loaded grammars and parsers
  223. */
  224. const parserCache = new Map<Language, Parser>();
  225. const languageCache = new Map<Language, WasmLanguage>();
  226. const unavailableGrammarErrors = new Map<Language, string>();
  227. let parserInitialized = false;
  228. /**
  229. * Initialize the tree-sitter WASM runtime. Must be called before loading grammars.
  230. * Does NOT load any grammar WASM files — use loadGrammarsForLanguages() for that.
  231. * Idempotent — safe to call multiple times.
  232. */
  233. export async function initGrammars(): Promise<void> {
  234. if (parserInitialized) return;
  235. await Parser.init();
  236. parserInitialized = true;
  237. }
  238. /**
  239. * Grammars that ship their own vendored WASMs under `dist/extraction/wasm/`
  240. * (not in tree-sitter-wasms, or the tree-sitter-wasms build is too old).
  241. * Lua: tree-sitter-wasms ships an ABI-13 build that corrupts the shared WASM
  242. * heap under web-tree-sitter 0.25 (drops nested calls/imports on every file
  243. * after the first); we vendor the upstream ABI-15 wasm instead. C#: the
  244. * tree-sitter-wasms build (ABI 13) has no primary-constructor support and
  245. * parses `class Foo(...)` as an ERROR that swallows the whole class (#237); we
  246. * vendor the upstream ABI-15 tree-sitter-c-sharp 0.23.5 wasm, which parses
  247. * primary constructors natively. Terraform: tree-sitter-wasms does not ship
  248. * HCL/Terraform at all, so we vendor the prebuilt tree-sitter-terraform.wasm
  249. * from @tree-sitter-grammars/tree-sitter-hcl 1.2.0 (Apache-2.0) —
  250. * byte-identical to the npm package's artifact. ArkTS: tree-sitter-wasms
  251. * doesn't ship it either; we vendor the prebuilt tree-sitter-arkts.wasm from
  252. * the tree-sitter-arkts 0.2.0 npm package (harmony-contrib/tree-sitter-arkts,
  253. * MIT) — byte-identical to the npm tarball's artifact. It extends the
  254. * tree-sitter-javascript grammar the same way tree-sitter-typescript does,
  255. * adding `struct_declaration` and the `arkui_component_expression` build()
  256. * DSL. Nix: tree-sitter-wasms doesn't ship it; we vendor a wasm built from
  257. * nix-community/tree-sitter-nix @ 3d0173d (MIT) with tree-sitter-cli 0.25.10
  258. * (`generate` + `build --wasm`, ABI 15 — upstream's checked-in parser.c is
  259. * still ABI 13; all 54 upstream corpus tests pass on the regenerated parser).
  260. *
  261. * TypeScript/TSX/JavaScript (+jsx, which shares the javascript grammar): the
  262. * tree-sitter-wasms builds are 2023-era (^0.20.x); we vendor wasm built from
  263. * the SAME grammar revisions the native extraction kernel compiles
  264. * (codegraph-kernel/Cargo.toml), so the kernel path and the wasm fallback
  265. * parse identically and per-language routing stays graph-neutral:
  266. * - tree-sitter/tree-sitter-typescript v0.23.2 (f975a62) → typescript + tsx
  267. * - tree-sitter/tree-sitter-javascript v0.25.0 (44c892e) → javascript + jsx
  268. * - tree-sitter/tree-sitter-java v0.23.5 (94703d5) → java
  269. * - tree-sitter/tree-sitter-python v0.23.6 (bffb65a) → python
  270. * - tree-sitter/tree-sitter-go v0.23.4 (3c3775f) → go
  271. * Built from each repo's CHECKED-IN parser.c (no `generate`) with
  272. * tree-sitter-cli 0.25.10 `build --wasm` — the same tables crates.io compiles
  273. * (parser.c sha-matched against the crates.io tarball).
  274. * The kernel-grammar-parity test asserts this alignment; bump the crate and
  275. * the vendored wasm together.
  276. */
  277. const VENDORED_WASM_LANGS: ReadonlySet<GrammarLanguage> = new Set([
  278. 'pascal', 'scala', 'lua', 'luau', 'csharp', 'r', 'cfml', 'cfscript', 'cfquery',
  279. 'cobol', 'vbnet', 'erlang', 'terraform', 'arkts', 'nix',
  280. 'typescript', 'tsx', 'javascript', 'jsx', 'java', 'python', 'go',
  281. // R7a (C/C++ kernel port prep): tree-sitter-c v0.24.2 (b780e47) +
  282. // tree-sitter-cpp v0.23.4 (f41e1a0), parser.c/scanner.c sha-matched against
  283. // the crates.io tarballs. `.metal`/`.cu` map to language 'cpp', so the
  284. // dialects ride the same (single, coherent) upgraded grammar.
  285. 'c', 'cpp',
  286. // R7b (Rust kernel port prep): tree-sitter-rust v0.24.2 (77a3747),
  287. // parser.c/scanner.c sha-matched against the crates.io tarball. Replaces the
  288. // 2023-era tree-sitter-wasms build (ABI 14 → 15).
  289. 'rust',
  290. // R7b (Ruby kernel port prep): tree-sitter-ruby v0.23.1 (71bd32f),
  291. // parser.c/scanner.c sha-matched against the crates.io tarball. Replaces the
  292. // ^0.20.1 tree-sitter-wasms build. Content bump only — the tag's checked-in
  293. // parser.c is still ABI 14 (predates the ABI-15 generator).
  294. 'ruby',
  295. ]);
  296. /** Absolute path of a language's grammar WASM (vendored or tree-sitter-wasms). */
  297. function resolveWasmPath(lang: GrammarLanguage): string {
  298. const wasmFile = WASM_GRAMMAR_FILES[lang];
  299. return VENDORED_WASM_LANGS.has(lang)
  300. ? path.join(__dirname, 'wasm', wasmFile)
  301. : require.resolve(`tree-sitter-wasms/out/${wasmFile}`);
  302. }
  303. /**
  304. * Expand an index set's languages to the grammars actually needed to parse it.
  305. * SFC languages (svelte/vue/astro) have no grammar of their own — their
  306. * extractors delegate <script>/frontmatter content to the TS/JS extractor, so
  307. * those grammars must be loaded even when no plain .ts/.js file is in the index
  308. * set (e.g. a pure-.astro content site). CFML (.cfc/.cfm) likewise delegates
  309. * bare-script content, <cfscript> tag bodies, and <cfquery> SQL bodies to the
  310. * cfscript/cfquery grammars (see injections.scm in tree-sitter-cfml).
  311. */
  312. function expandGrammarLanguages(languages: Language[]): Language[] {
  313. if (languages.some((l) => l === 'svelte' || l === 'vue' || l === 'astro')) {
  314. languages = [...languages, 'typescript', 'javascript'];
  315. }
  316. if (languages.some((l) => l === 'cfml')) {
  317. languages = [...languages, 'cfscript', 'cfquery'];
  318. }
  319. return languages;
  320. }
  321. /**
  322. * Pre-read the grammar WASM bytes for an index set, keyed by language. The
  323. * orchestrator reads each grammar ONCE and hands the bytes to every parse
  324. * worker via its `load-grammars` message, so worker spawns/respawns load
  325. * grammars from memory instead of re-reading them from disk — on slow storage
  326. * (HDD, issue #1231) each respawn's grammar re-read otherwise amplifies the
  327. * I/O contention that caused the respawn. Best-effort: a language whose WASM
  328. * can't be read here is simply omitted, and the worker falls back to its own
  329. * disk load (which surfaces the real error/warning path).
  330. */
  331. export async function readGrammarWasmBytes(languages: Language[]): Promise<Record<string, Uint8Array>> {
  332. const out: Record<string, Uint8Array> = {};
  333. const toRead = [...new Set(expandGrammarLanguages(languages))].filter(
  334. (lang): lang is GrammarLanguage => lang in WASM_GRAMMAR_FILES
  335. );
  336. for (const lang of toRead) {
  337. try {
  338. out[lang] = await fsp.readFile(resolveWasmPath(lang));
  339. } catch {
  340. // fall through — the worker's own load reports the failure
  341. }
  342. }
  343. return out;
  344. }
  345. /**
  346. * Load grammar WASM files for specific languages only.
  347. * Skips languages that are already loaded or have no WASM grammar.
  348. * Must be called after initGrammars().
  349. *
  350. * `wasmBytes` (optional) holds pre-read grammar bytes keyed by language (from
  351. * {@link readGrammarWasmBytes}, forwarded through the parse pool); when a
  352. * language's bytes are present they're loaded from memory instead of disk.
  353. */
  354. export async function loadGrammarsForLanguages(languages: Language[], wasmBytes?: Record<string, Uint8Array>): Promise<void> {
  355. if (!parserInitialized) {
  356. await initGrammars();
  357. }
  358. languages = expandGrammarLanguages(languages);
  359. // Deduplicate and filter to languages that have WASM grammars and aren't already loaded
  360. const toLoad = [...new Set(languages)].filter(
  361. (lang): lang is GrammarLanguage =>
  362. lang in WASM_GRAMMAR_FILES &&
  363. !languageCache.has(lang) &&
  364. !unavailableGrammarErrors.has(lang)
  365. );
  366. // Load grammars sequentially to avoid web-tree-sitter WASM race condition on Node 20+
  367. // See: https://github.com/tree-sitter/tree-sitter/issues/2338
  368. for (const lang of toLoad) {
  369. try {
  370. const bytes = wasmBytes?.[lang];
  371. const language = await WasmLanguage.load(bytes ?? resolveWasmPath(lang));
  372. languageCache.set(lang, language);
  373. } catch (error) {
  374. const message = error instanceof Error ? error.message : String(error);
  375. console.warn(`[CodeGraph] Failed to load ${lang} grammar — parsing will be unavailable: ${message}`);
  376. unavailableGrammarErrors.set(lang, message);
  377. }
  378. }
  379. }
  380. /**
  381. * Load ALL grammar WASM files. Convenience function for tests and
  382. * backward compatibility. Prefer loadGrammarsForLanguages() in production.
  383. */
  384. export async function loadAllGrammars(): Promise<void> {
  385. const allLanguages = Object.keys(WASM_GRAMMAR_FILES) as GrammarLanguage[];
  386. await loadGrammarsForLanguages(allLanguages);
  387. }
  388. /**
  389. * Check if grammars have been initialized
  390. */
  391. export function isGrammarsInitialized(): boolean {
  392. return parserInitialized;
  393. }
  394. /**
  395. * Get a parser for the specified language.
  396. * Returns synchronously from pre-loaded cache.
  397. */
  398. export function getParser(language: Language): Parser | null {
  399. if (parserCache.has(language)) {
  400. return parserCache.get(language)!;
  401. }
  402. const lang = languageCache.get(language);
  403. if (!lang) {
  404. return null;
  405. }
  406. const parser = new Parser();
  407. parser.setLanguage(lang);
  408. parserCache.set(language, parser);
  409. return parser;
  410. }
  411. /**
  412. * Detect language from file extension.
  413. *
  414. * `overrides` is the project's validated custom extension → language map (from
  415. * `codegraph.json`); when present its mappings take precedence over the built-in
  416. * `EXTENSION_MAP`. Omitting it is byte-identical to the zero-config behavior.
  417. */
  418. export function detectLanguage(filePath: string, source?: string, overrides?: Record<string, Language>): Language {
  419. // Play `conf/routes` has no grammar — route through the no-symbol path; the
  420. // Play framework resolver extracts route nodes from it.
  421. if (isPlayRoutesFile(filePath)) return 'yaml';
  422. const ext = filePath.substring(filePath.lastIndexOf('.')).toLowerCase();
  423. // Shopify OS 2.0 JSON templates / section groups → the Liquid extractor (it
  424. // links each section `"type"` to its `sections/<type>.liquid`).
  425. if (isShopifyLiquidJson(filePath)) return 'liquid';
  426. // OTP `.app`/`.app.src` resource files — Erlang terms the grammar parses as
  427. // top-level expressions (last-dot ext `.src` is too generic for the map).
  428. if (isErlangAppFile(filePath)) return 'erlang';
  429. const lang = (overrides && overrides[ext]) || EXTENSION_MAP[ext] || 'unknown';
  430. // .h files could be C, C++, or Objective-C — check source content
  431. if (lang === 'c' && ext === '.h' && source) {
  432. if (looksLikeCpp(source)) return 'cpp';
  433. if (looksLikeObjc(source)) return 'objc';
  434. }
  435. return lang;
  436. }
  437. /**
  438. * Heuristic: does a .h file contain C++ constructs?
  439. * Checks the first ~8KB for patterns that are unique to C++ and never valid C.
  440. */
  441. function looksLikeCpp(source: string): boolean {
  442. const sample = source.substring(0, 8192);
  443. // The `class MACRO Name : Base` / `class MACRO Name { … }` branch mirrors what
  444. // `blankCppExportMacros` recovers: an ALL-CAPS export/visibility macro
  445. // (`ENGINE_API`, `MYMODULE_API`, `*_EXPORT`, …) sitting between `class`/`struct`
  446. // and the type name. Without it, a header whose ONLY C++ signal is such a
  447. // macro-annotated class — common for lean Unreal-Engine types that carry just
  448. // `GENERATED_BODY()` and no explicit `public:`/`virtual` — is misdetected as C,
  449. // routed through the C extractor (which extracts no classes), and its class
  450. // definition silently vanishes. The two-token shape (`<KW> <MACRO> <Name>`
  451. // before a `[:{]`) never occurs in valid C, so this can't misclassify C headers.
  452. return /\bnamespace\b|\bclass\s+\w+\s*[:{]|\b(?:class|struct)\s+[A-Z][A-Z0-9_]+\s+\w+\s*(?:final\s*)?[:{]|\btemplate\s*<|\b(?:public|private|protected)\s*:|\bvirtual\b|\busing\s+(?:namespace\b|\w+\s*=)/.test(sample);
  453. }
  454. /**
  455. * Heuristic: does a .h file contain Objective-C constructs?
  456. */
  457. function looksLikeObjc(source: string): boolean {
  458. const sample = source.substring(0, 8192);
  459. return /@(?:interface|implementation|protocol|synthesize)\b/.test(sample);
  460. }
  461. /**
  462. * Check if a language is supported (has a grammar defined).
  463. * Returns true if the grammar exists, even if not yet loaded.
  464. */
  465. export function isLanguageSupported(language: Language): boolean {
  466. if (language === 'svelte') return true; // custom extractor (script block delegation)
  467. if (language === 'vue') return true; // custom extractor (script block delegation)
  468. if (language === 'astro') return true; // custom extractor (frontmatter/script block delegation)
  469. if (language === 'liquid') return true; // custom regex extractor
  470. if (language === 'razor') return true; // custom RazorExtractor (.cshtml/.razor markup)
  471. if (language === 'yaml') return true; // file-level tracking only; Drupal routing extraction via framework resolver
  472. if (language === 'twig') return true; // file-level tracking only
  473. if (language === 'xml') return true; // MyBatis mapper extractor
  474. if (language === 'properties') return true; // Spring config keys
  475. if (language === 'unknown') return false;
  476. return language in WASM_GRAMMAR_FILES;
  477. }
  478. /**
  479. * Check if a grammar has been loaded and is ready for parsing.
  480. */
  481. export function isGrammarLoaded(language: Language): boolean {
  482. if (language === 'svelte' || language === 'vue' || language === 'astro' || language === 'liquid' || language === 'razor') return true;
  483. if (language === 'yaml' || language === 'twig') return true; // no WASM grammar needed
  484. if (language === 'xml' || language === 'properties') return true; // no WASM grammar needed
  485. return languageCache.has(language);
  486. }
  487. /**
  488. * Languages tracked at the file-record level only: parsing emits zero symbol
  489. * nodes, but the file is still stored (and framework resolvers may add per-file
  490. * references later, e.g. Drupal routing yml, Spring `@Value` against
  491. * application.properties). This is the canonical set behind the no-symbol
  492. * branch in `tree-sitter.ts`; `xml` is intentionally excluded because its
  493. * MyBatis extractor emits a file node. Callers use this to count such files as
  494. * indexed rather than skipped, so it must stay in sync with that branch.
  495. */
  496. export function isFileLevelOnlyLanguage(language: Language): boolean {
  497. return language === 'yaml' || language === 'twig' || language === 'properties';
  498. }
  499. /**
  500. * Get all supported languages (those with grammar definitions).
  501. */
  502. export function getSupportedLanguages(): Language[] {
  503. return [...(Object.keys(WASM_GRAMMAR_FILES) as GrammarLanguage[]), 'svelte', 'vue', 'astro', 'liquid'];
  504. }
  505. /**
  506. * Reset the cached parser for a language to reclaim WASM heap memory.
  507. * The tree-sitter WASM runtime accumulates fragmented memory over thousands
  508. * of parses. Deleting and recreating the Parser instance forces the WASM
  509. * heap to reset, preventing "memory access out of bounds" crashes in
  510. * large repos.
  511. */
  512. export function resetParser(language: Language): void {
  513. const old = parserCache.get(language);
  514. if (old) {
  515. old.delete();
  516. parserCache.delete(language);
  517. }
  518. }
  519. /**
  520. * Clear parser/grammar caches (useful for testing)
  521. */
  522. export function clearParserCache(): void {
  523. for (const parser of parserCache.values()) {
  524. parser.delete();
  525. }
  526. parserCache.clear();
  527. // Note: languageCache is NOT cleared — WASM languages persist.
  528. // To fully re-init, set parserInitialized = false and call initGrammars() again.
  529. unavailableGrammarErrors.clear();
  530. }
  531. /**
  532. * Report grammars that failed to load.
  533. */
  534. export function getUnavailableGrammarErrors(): Partial<Record<Language, string>> {
  535. const out: Partial<Record<Language, string>> = {};
  536. for (const [language, message] of unavailableGrammarErrors.entries()) {
  537. out[language] = message;
  538. }
  539. return out;
  540. }
  541. /**
  542. * Get language display name
  543. */
  544. export function getLanguageDisplayName(language: Language): string {
  545. const names: Record<Language, string> = {
  546. typescript: 'TypeScript',
  547. javascript: 'JavaScript',
  548. tsx: 'TypeScript (TSX)',
  549. jsx: 'JavaScript (JSX)',
  550. python: 'Python',
  551. go: 'Go',
  552. rust: 'Rust',
  553. r: 'R',
  554. java: 'Java',
  555. c: 'C',
  556. cpp: 'C++',
  557. csharp: 'C#',
  558. razor: 'Razor/Blazor',
  559. php: 'PHP',
  560. ruby: 'Ruby',
  561. swift: 'Swift',
  562. kotlin: 'Kotlin',
  563. dart: 'Dart',
  564. svelte: 'Svelte',
  565. vue: 'Vue',
  566. astro: 'Astro',
  567. liquid: 'Liquid',
  568. pascal: 'Pascal / Delphi',
  569. scala: 'Scala',
  570. lua: 'Lua',
  571. luau: 'Luau',
  572. objc: 'Objective-C',
  573. solidity: 'Solidity',
  574. nix: 'Nix',
  575. yaml: 'YAML',
  576. twig: 'Twig',
  577. xml: 'XML',
  578. properties: 'Java properties',
  579. cfml: 'CFML',
  580. cfscript: 'CFScript',
  581. cfquery: 'CFQuery (SQL)',
  582. cobol: 'COBOL',
  583. vbnet: 'Visual Basic .NET',
  584. erlang: 'Erlang',
  585. terraform: 'Terraform',
  586. arkts: 'ArkTS',
  587. unknown: 'Unknown',
  588. };
  589. return names[language] || language;
  590. }