Skip to main content

sinter_extract/
language.rs

1use tree_sitter::Language;
2
3/// A language is data: a grammar, a capture query, and comment node kinds.
4/// The engine consumes only this struct — adding a language adds a row here
5/// and a `.scm` file, never engine code.
6///
7/// Query capture contract (the universal primitives):
8/// - `@def.<kind>`   whole definition node; `<kind>` must parse via
9///   `SymbolKind::from_str_opt`. A definition also scopes what it contains.
10/// - `@name`         the definition's (or scope's) name node, same match.
11/// - `@qualifier`    optional extra scope prefix from the same match
12///   (e.g. a Go method receiver type).
13/// - `@scope`        a node that scopes names but is not itself a symbol
14///   (e.g. a Rust `impl` block); pairs with `@name`.
15/// - `@ref.<rel>`    a reference site; `<rel>` in {call, use, read, write,
16///   create, alter, drop} maps to the
17///   relation an eventual binding would carry.
18/// - `@import`       an imported path; quotes are stripped.
19/// - `@import.module` + `@import.name` — from-style imports
20///   (`from util import helper`, `import { helper } from "./util"`): the
21///   engine joins them with the language's first path separator so the
22///   import binds the item, not just the module.
23/// - `@import.alias`   local rebinding (`as` clauses, Go dot imports).
24/// - `@import.star`    glob semantics: with `@import.module` (Python `*`)
25///   or alongside a plain `@import` (bash `source`), every top-level name
26///   of the module binds.
27/// - `@local` (+ `@local.type`) — shadowing bindings, optionally typed.
28/// - `@embed`          embedded/promoted type members (Go).
29/// - `@trait` + `@trait.impl` — an impl block (`@trait.impl`) naming the
30///   trait it implements (`@trait`): dynamic-dispatch pairing input.
31/// - `@doc`            a node whose text IS the definition's doc (Python
32///   docstrings): attached to the smallest containing definition,
33///   overriding any sibling-comment doc.
34///
35/// Standard tree-sitter text predicates (`#eq?`, `#any-of?`, ...) are
36/// evaluated by the tree-sitter crate itself and may be used freely
37/// (bash isolates `source` from ordinary commands this way).
38/// A package manifest declares the mapping between a package's *name*
39/// (what imports say) and its *directory* (what module paths say) — a
40/// naming root the file tree alone cannot reveal. Reading it is
41/// evidence, exactly like reading an import statement. Pure data; the
42/// engine never branches on language.
43pub struct ManifestSpec {
44    /// Manifest file basename ("Cargo.toml", "go.mod").
45    pub filename: &'static str,
46    /// Key whose value is the package name ("name" for `name = "x"`,
47    /// "module" for `module x`).
48    pub name_key: &'static str,
49    /// Path-head aliases meaning "this package's root" ("crate").
50    pub self_names: &'static [&'static str],
51    /// Normalizes the declared name to reference form (dashes to
52    /// underscores for Rust).
53    pub normalize: fn(&str) -> String,
54}
55
56/// A discovered package root: files under `dir` belong to package `name`
57/// for language `language`.
58#[derive(Debug, Clone)]
59pub struct ModuleRoot {
60    pub name: String,
61    /// Repo-relative directory of the manifest ("" for repo root).
62    pub dir: String,
63    pub language: &'static str,
64}
65
66/// Parse one candidate file into a module root, if its basename matches
67/// a language's manifest spec. `rel_path` is repo-relative.
68pub fn manifest_root(rel_path: &str, content: &str) -> Option<ModuleRoot> {
69    let base = rel_path.rsplit('/').next()?;
70    let spec = LANGUAGES
71        .iter()
72        .find(|l| l.manifest.is_some_and(|m| m.filename == base))?;
73    let m = spec.manifest?;
74    let name = content.lines().find_map(|line| {
75        let rest = line.trim().strip_prefix(m.name_key)?;
76        let rest = rest.trim_start();
77        let rest = rest.strip_prefix('=').unwrap_or(rest).trim();
78        let name = rest.trim_matches('"').trim();
79        (!name.is_empty() && !name.contains(' ')).then(|| name.to_string())
80    })?;
81    let dir = rel_path
82        .rsplit_once('/')
83        .map(|(d, _)| d.to_string())
84        .unwrap_or_default();
85    Some(ModuleRoot {
86        name: (m.normalize)(&name),
87        dir,
88        language: spec.name,
89    })
90}
91
92/// A secondary grammar for languages whose spec splits container and
93/// content parses (tree-sitter markdown's block/inline split). The engine
94/// re-parses the byte ranges of the named container nodes with this
95/// grammar and runs its query through the same capture contract; spans
96/// stay file-absolute via included-range parsing. Pure data.
97pub struct InlineSpec {
98    pub grammar: fn() -> Language,
99    pub query_source: &'static str,
100    /// Node kinds in the primary tree whose ranges the inline grammar
101    /// parses.
102    pub container_kinds: &'static [&'static str],
103}
104
105pub struct LanguageSpec {
106    pub name: &'static str,
107    pub extensions: &'static [&'static str],
108    pub grammar: fn() -> Language,
109    pub query_source: &'static str,
110    pub comment_kinds: &'static [&'static str],
111    /// Maps a repo-relative file path to its module/package path segments —
112    /// what import statements are matched against. Pure data transform;
113    /// the resolver stays language-blind.
114    pub module_path: fn(&str) -> Vec<String>,
115    /// Splits an import/reference path into segments (`::` vs `/` vs `.`).
116    pub path_separators: &'static [&'static str],
117    /// Turns a raw import/reference path into absolute module segments,
118    /// resolving language-relative forms (`super::`, leading dots, `./`)
119    /// against the path's own file. Data transform; resolver stays blind.
120    pub absolutize: fn(path: &str, file: &str) -> Vec<String>,
121    /// Receiver keywords (`self`, `this`): a qualified reference through one
122    /// binds within the reference's enclosing type.
123    pub receivers: &'static [&'static str],
124    /// Node kinds the doc-comment walk may step over (max 2) between a
125    /// definition and its doc — e.g. Unreal's `UCLASS(...)` line, which
126    /// parses as an expression_statement between comment and class.
127    pub doc_skip_kinds: &'static [&'static str],
128    /// Package manifest shape, when the language has one that names
129    /// module roots (see ManifestSpec).
130    pub manifest: Option<&'static ManifestSpec>,
131    /// Secondary inline grammar applied to designated container-node
132    /// ranges, whose captures merge into the file's facts (markdown's
133    /// block/inline split).
134    pub inline: Option<&'static InlineSpec>,
135    /// References in this language are document paths naming corpus
136    /// files (`[text](docs/guide.md#setup)`), not symbol paths: the
137    /// resolver binds them by exact file path — with or without the
138    /// language's extensions, `#fragment` to the target file's unique
139    /// def whose name slugifies to the fragment — and never through the
140    /// symbol tiers. Evidence or nothing: a dead link stays unresolved.
141    pub file_refs: bool,
142    /// Interface satisfaction is implicit (Go): no syntax names the
143    /// interface at the implementing type, so the resolver runs a
144    /// structural method-set pass instead of `@trait`/`@trait.impl`
145    /// pairing. Pure data; the engine never branches on language name.
146    pub implicit_interfaces: bool,
147}
148
149fn rust_normalize(name: &str) -> String {
150    name.replace('-', "_")
151}
152
153static RUST_MANIFEST: ManifestSpec = ManifestSpec {
154    filename: "Cargo.toml",
155    name_key: "name",
156    self_names: &["crate"],
157    normalize: rust_normalize,
158};
159
160fn identity_normalize(name: &str) -> String {
161    name.to_string()
162}
163
164/// `module example.com/proj` — the declared module path anchors full-path
165/// imports to the repo directory holding go.mod. No self-alias: Go import
166/// paths are always absolute.
167static GO_MANIFEST: ManifestSpec = ManifestSpec {
168    filename: "go.mod",
169    name_key: "module",
170    self_names: &[],
171    normalize: identity_normalize,
172};
173
174fn split_all(path: &str, separators: &[&str]) -> Vec<String> {
175    let mut segments = vec![path.to_string()];
176    for sep in separators {
177        segments = segments
178            .iter()
179            .flat_map(|s| s.split(sep).map(str::to_string))
180            .collect();
181    }
182    segments.into_iter().filter(|s| !s.is_empty()).collect()
183}
184
185fn dirname_segments(file: &str) -> Vec<String> {
186    let mut segments: Vec<String> = file.split('/').map(str::to_string).collect();
187    segments.pop();
188    segments
189}
190
191/// `crate::x` stays absolute; `super::x`/`self::x` resolve against the
192/// file's own module path.
193fn rust_absolutize(path: &str, file: &str) -> Vec<String> {
194    let mut module = rust_module_path(file);
195    let mut rest = path;
196    if let Some(r) = rest.strip_prefix("self::") {
197        rest = r;
198    } else {
199        while let Some(r) = rest.strip_prefix("super::") {
200            module.pop();
201            rest = r;
202        }
203        if rest.len() == path.len() {
204            if path.starts_with("crate::") {
205                return split_all(path, &["::", "."]);
206            }
207            // Bare paths are relative to the file's own module
208            // (`internal::helper` inside lib.rs means crate::internal::...).
209            module.extend(split_all(path, &["::", "."]));
210            return module;
211        }
212    }
213    module.extend(split_all(rest, &["::", "."]));
214    module
215}
216
217fn go_absolutize(path: &str, _file: &str) -> Vec<String> {
218    split_all(path, &["/", "."])
219}
220
221/// `acme/core/v1/action_event.proto` -> ["acme","core","v1","action_event"]:
222/// the file extension must not become a module segment, or the import key
223/// never suffix-matches the file's own module path and same-package bare
224/// references stay unresolved.
225fn proto_absolutize(path: &str, _file: &str) -> Vec<String> {
226    split_all(path.strip_suffix(".proto").unwrap_or(path), &["/", "."])
227}
228
229/// Leading dots are package-relative: one dot is the file's own package,
230/// each further dot one package up.
231fn python_absolutize(path: &str, file: &str) -> Vec<String> {
232    let dots = path.len() - path.trim_start_matches('.').len();
233    if dots == 0 {
234        return split_all(path, &["."]);
235    }
236    let mut base = dirname_segments(file);
237    for _ in 1..dots {
238        base.pop();
239    }
240    base.extend(split_all(&path[dots..], &["."]));
241    base
242}
243
244/// `./x` and `../x` resolve against the file's directory.
245fn typescript_absolutize(path: &str, file: &str) -> Vec<String> {
246    if !path.starts_with('.') {
247        return split_all(path, &["/", "."]);
248    }
249    let mut base = dirname_segments(file);
250    let mut rest = path;
251    // Leading `/` is the repo-root convention (GitHub-style), not a
252    // relative segment: resolve against the root, not the linking file.
253    if let Some(r) = rest.strip_prefix('/') {
254        base.clear();
255        rest = r;
256    }
257    loop {
258        if let Some(r) = rest.strip_prefix("./") {
259            rest = r;
260        } else if let Some(r) = rest.strip_prefix("../") {
261            base.pop();
262            rest = r;
263        } else {
264            break;
265        }
266    }
267    base.extend(split_all(rest, &["/"]));
268    base
269}
270
271fn rust_grammar() -> Language {
272    tree_sitter_rust::LANGUAGE.into()
273}
274
275fn go_grammar() -> Language {
276    tree_sitter_go::LANGUAGE.into()
277}
278
279/// `src/util.rs` -> ["crate", "util"]; `src/foo/mod.rs` -> ["crate", "foo"].
280/// Single-crate view by design: the resolver's manifest-aware `key_of`
281/// replaces the "crate" head with the declared package name in workspaces.
282fn rust_module_path(file: &str) -> Vec<String> {
283    let trimmed = file.strip_suffix(".rs").unwrap_or(file);
284    let after_src = trimmed.rsplit_once("src/").map_or(trimmed, |(_, r)| r);
285    let mut segments = vec!["crate".to_string()];
286    for seg in after_src.split('/') {
287        if !matches!(seg, "lib" | "main" | "mod" | "") {
288            segments.push(seg.to_string());
289        }
290    }
291    segments
292}
293
294/// `pkg/util/util.go` -> ["pkg", "util"]; root files -> [].
295fn go_module_path(file: &str) -> Vec<String> {
296    let mut segments: Vec<String> = file.split('/').map(str::to_string).collect();
297    segments.pop(); // file name; Go packages are directories
298    segments
299}
300
301fn python_grammar() -> Language {
302    tree_sitter_python::LANGUAGE.into()
303}
304
305fn bash_grammar() -> Language {
306    tree_sitter_bash::LANGUAGE.into()
307}
308
309/// Bash has no module system: a file is its path. `lib/util.sh` ->
310/// ["lib", "util"].
311fn bash_module_path(file: &str) -> Vec<String> {
312    let trimmed = file
313        .strip_suffix(".sh")
314        .or_else(|| file.strip_suffix(".bash"))
315        .unwrap_or(file);
316    trimmed
317        .split('/')
318        .filter(|s| !s.is_empty())
319        .map(str::to_string)
320        .collect()
321}
322
323/// `source` paths: strip `$(dirname "$0")/` and `./` style prefixes and
324/// resolve against the sourcing file's directory; bare paths pass through.
325fn bash_absolutize(path: &str, file: &str) -> Vec<String> {
326    let trimmed = path.trim();
327    let dir_relative = [
328        "$(dirname \"$0\")/",
329        "$(dirname $0)/",
330        "${BASH_SOURCE%/*}/",
331        "./",
332    ]
333    .iter()
334    .find_map(|p| trimmed.strip_prefix(p));
335    let stripped = |s: &str| {
336        s.strip_suffix(".sh")
337            .or_else(|| s.strip_suffix(".bash"))
338            .unwrap_or(s)
339            .to_string()
340    };
341    match dir_relative {
342        Some(rest) => {
343            let mut base = dirname_segments(file);
344            base.extend(rest.split('/').filter(|s| !s.is_empty()).map(stripped));
345            base
346        }
347        None => trimmed
348            .split('/')
349            .filter(|s| !s.is_empty() && *s != ".")
350            .map(stripped)
351            .collect(),
352    }
353}
354
355fn cpp_grammar() -> Language {
356    tree_sitter_cpp::LANGUAGE.into()
357}
358
359/// `player/character.h` and `player/character.cpp` share the module
360/// ["player", "character"] — header/impl pairs resolve into one another.
361fn cpp_module_path(file: &str) -> Vec<String> {
362    let trimmed = file.rsplit_once('.').map_or(file, |(stem, _)| stem);
363    trimmed
364        .split('/')
365        .filter(|s| !s.is_empty())
366        .map(str::to_string)
367        .collect()
368}
369
370/// Quoted include paths, extension-stripped, `./` resolved against the
371/// including file's directory; `<system>` includes pass through (and stay
372/// external unless a matching module exists in the corpus).
373fn cpp_absolutize(path: &str, file: &str) -> Vec<String> {
374    let trimmed = path.trim().trim_matches(['<', '>']);
375    let no_ext = trimmed.rsplit_once('.').map_or(trimmed, |(stem, ext)| {
376        if matches!(
377            ext,
378            "h" | "hh" | "hpp" | "hxx" | "cpp" | "cc" | "cxx" | "inl"
379        ) {
380            stem
381        } else {
382            trimmed
383        }
384    });
385    if let Some(rest) = no_ext.strip_prefix("./") {
386        let mut base = dirname_segments(file);
387        base.extend(
388            rest.split('/')
389                .filter(|s| !s.is_empty())
390                .map(str::to_string),
391        );
392        return base;
393    }
394    // Member access (`c.jump`, `this->jump`) must split so the resolver's
395    // receiver/typed-local tiers see a prefix (fixture: cpp-header-impl).
396    no_ext
397        .replace("->", ".")
398        .split(['/', ':', '.'])
399        .filter(|s| !s.is_empty())
400        .map(str::to_string)
401        .collect()
402}
403
404fn proto_grammar() -> Language {
405    tree_sitter_proto::LANGUAGE.into()
406}
407
408/// `contracts/payments.proto` -> ["contracts", "payments"]. Generated-stub
409/// imports name proto symbols through package paths; module keys are the
410/// file path, packages resolve via the same suffix matching as Go.
411fn proto_module_path(file: &str) -> Vec<String> {
412    let trimmed = file.strip_suffix(".proto").unwrap_or(file);
413    trimmed
414        .split('/')
415        .filter(|s| !s.is_empty())
416        .map(str::to_string)
417        .collect()
418}
419
420fn typescript_grammar() -> Language {
421    tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()
422}
423
424/// `pkg/mod.py` -> ["pkg", "mod"]; `pkg/__init__.py` -> ["pkg"].
425fn python_module_path(file: &str) -> Vec<String> {
426    let trimmed = file.strip_suffix(".py").unwrap_or(file);
427    trimmed
428        .split('/')
429        .filter(|s| !matches!(*s, "__init__" | ""))
430        .map(str::to_string)
431        .collect()
432}
433
434/// `src/util.ts` -> ["src", "util"]; `src/index.ts` -> ["src"].
435fn typescript_module_path(file: &str) -> Vec<String> {
436    let trimmed = file
437        .strip_suffix(".tsx")
438        .or_else(|| file.strip_suffix(".ts"))
439        .unwrap_or(file);
440    trimmed
441        .split('/')
442        .filter(|s| !matches!(*s, "index" | ""))
443        .map(str::to_string)
444        .collect()
445}
446
447fn javascript_grammar() -> Language {
448    tree_sitter_javascript::LANGUAGE.into()
449}
450
451fn c_grammar() -> Language {
452    tree_sitter_c::LANGUAGE.into()
453}
454
455fn java_grammar() -> Language {
456    tree_sitter_java::LANGUAGE.into()
457}
458
459fn csharp_grammar() -> Language {
460    tree_sitter_c_sharp::LANGUAGE.into()
461}
462
463/// C# module identity is the file's directory, Go-style: `using Acme.Util;`
464/// imports a namespace (never a type), and namespaces conventionally mirror
465/// directories. `Acme/Util/TextHelper.cs` -> ["Acme", "Util"], so the using
466/// directive's segments suffix-match the directory key. Path-derived only:
467/// namespace declarations that diverge from layout are out of scope
468/// (stated boundary of the csharp pack, see queries/csharp.scm).
469fn csharp_module_path(file: &str) -> Vec<String> {
470    let mut segments: Vec<String> = file.split('/').map(str::to_string).collect();
471    segments.pop(); // file name; namespaces are directories
472    segments
473}
474
475fn markdown_grammar() -> Language {
476    tree_sitter_md::LANGUAGE.into()
477}
478
479fn markdown_inline_grammar() -> Language {
480    tree_sitter_md::INLINE_LANGUAGE.into()
481}
482
483/// Inline content (`[text](target)`) is a second grammar in tree-sitter's
484/// markdown split; the engine parses the block tree's `inline` ranges
485/// with it (see InlineSpec).
486static MARKDOWN_INLINE: InlineSpec = InlineSpec {
487    grammar: markdown_inline_grammar,
488    query_source: include_str!("../queries/markdown-inline.scm"),
489    container_kinds: &["inline"],
490};
491
492/// Link destinations resolve against the linking file's directory —
493/// `./x`, `../x`, and bare `x` alike (doc-link convention); the `.md`
494/// extension is not a module segment.
495fn markdown_absolutize(path: &str, file: &str) -> Vec<String> {
496    let mut base = dirname_segments(file);
497    let mut rest = path;
498    // Leading `/` is the repo-root convention (GitHub-style), not a
499    // relative segment: resolve against the root, not the linking file.
500    if let Some(r) = rest.strip_prefix('/') {
501        base.clear();
502        rest = r;
503    }
504    loop {
505        if let Some(r) = rest.strip_prefix("./") {
506            rest = r;
507        } else if let Some(r) = rest.strip_prefix("../") {
508            base.pop();
509            rest = r;
510        } else {
511            break;
512        }
513    }
514    let rest = rest
515        .strip_suffix(".md")
516        .or_else(|| rest.strip_suffix(".markdown"))
517        .unwrap_or(rest);
518    base.extend(
519        rest.split('/')
520            .filter(|s| !s.is_empty())
521            .map(str::to_string),
522    );
523    base
524}
525
526/// A document is its path: `docs/team.md` -> ["docs", "team"].
527fn markdown_module_path(file: &str) -> Vec<String> {
528    let trimmed = file
529        .strip_suffix(".md")
530        .or_else(|| file.strip_suffix(".markdown"))
531        .unwrap_or(file);
532    trimmed
533        .split('/')
534        .filter(|s| !s.is_empty())
535        .map(str::to_string)
536        .collect()
537}
538
539fn sql_grammar() -> Language {
540    tree_sitter_sequel::LANGUAGE.into()
541}
542
543fn javascript_module_path(file: &str) -> Vec<String> {
544    let trimmed = file
545        .strip_suffix(".jsx")
546        .or_else(|| file.strip_suffix(".mjs"))
547        .or_else(|| file.strip_suffix(".cjs"))
548        .or_else(|| file.strip_suffix(".js"))
549        .unwrap_or(file);
550    trimmed
551        .split('/')
552        .filter(|s| !matches!(*s, "index" | ""))
553        .map(str::to_string)
554        .collect()
555}
556
557fn javascript_absolutize(path: &str, file: &str) -> Vec<String> {
558    typescript_absolutize(path, file)
559}
560
561fn c_module_path(file: &str) -> Vec<String> {
562    cpp_module_path(file)
563}
564
565fn c_absolutize(path: &str, file: &str) -> Vec<String> {
566    cpp_absolutize(path, file)
567}
568
569/// Java packages are directories: `com/acme/util/Text.java` ->
570/// ["com", "acme", "util"], matching a path-aligned `package com.acme.util;`.
571/// The class contributes its own segment as a definition, so the FQN
572/// `com.acme.util.Text` resolves as module + top-level def.
573// ponytail: assumes package declarations align with directories; parse the
574// package_declaration into module identity if misaligned repos matter.
575fn java_module_path(file: &str) -> Vec<String> {
576    dirname_segments(file)
577        .into_iter()
578        .filter(|s| !s.is_empty())
579        .collect()
580}
581
582/// Java qualified references arrive as full invocation text
583/// (`Text.trim(s)`): everything from the first `(` on is arguments, not
584/// path. Imports are already-absolute dotted FQNs.
585fn java_absolutize(path: &str, _file: &str) -> Vec<String> {
586    let head = path.split('(').next().unwrap_or(path);
587    head.split('.')
588        .map(str::trim)
589        .filter(|s| !s.is_empty())
590        .map(str::to_string)
591        .collect()
592}
593
594/// All .sql files in a directory share one namespace (a database schema
595/// has no per-file scoping), so the module key is the directory alone:
596/// `db/schema.sql` and `db/queries.sql` both map to ["db"].
597fn sql_module_path(file: &str) -> Vec<String> {
598    let dir = file.rsplit_once('/').map_or("", |(dir, _)| dir);
599    dir.split('/')
600        .filter(|s| !s.is_empty())
601        .map(str::to_string)
602        .collect()
603}
604
605/// Dotted absolute imports (C#/SQL style): already-absolute FQNs split
606/// on dots; no relative forms exist.
607fn dotted_absolutize(path: &str, _file: &str) -> Vec<String> {
608    path.trim()
609        .split('.')
610        .filter(|s| !s.is_empty())
611        .map(str::to_string)
612        .collect()
613}
614
615pub static LANGUAGES: &[LanguageSpec] = &[
616    LanguageSpec {
617        name: "rust",
618        extensions: &["rs"],
619        grammar: rust_grammar,
620        query_source: include_str!("../queries/rust.scm"),
621        comment_kinds: &["line_comment", "block_comment"],
622        module_path: rust_module_path,
623        path_separators: &["::", "."],
624        absolutize: rust_absolutize,
625        receivers: &["self", "Self"],
626        doc_skip_kinds: &[],
627        manifest: Some(&RUST_MANIFEST),
628        inline: None,
629        file_refs: false,
630        implicit_interfaces: false,
631    },
632    LanguageSpec {
633        name: "go",
634        extensions: &["go"],
635        grammar: go_grammar,
636        query_source: include_str!("../queries/go.scm"),
637        comment_kinds: &["comment"],
638        module_path: go_module_path,
639        path_separators: &["/", "."],
640        absolutize: go_absolutize,
641        receivers: &[],
642        doc_skip_kinds: &[],
643        manifest: Some(&GO_MANIFEST),
644        inline: None,
645        file_refs: false,
646        implicit_interfaces: true,
647    },
648    LanguageSpec {
649        name: "python",
650        extensions: &["py"],
651        grammar: python_grammar,
652        query_source: include_str!("../queries/python.scm"),
653        comment_kinds: &["comment"],
654        module_path: python_module_path,
655        path_separators: &["."],
656        absolutize: python_absolutize,
657        receivers: &["self", "cls"],
658        doc_skip_kinds: &[],
659        manifest: None,
660        inline: None,
661        file_refs: false,
662        implicit_interfaces: false,
663    },
664    LanguageSpec {
665        name: "typescript",
666        extensions: &["ts", "tsx"],
667        grammar: typescript_grammar,
668        query_source: include_str!("../queries/typescript.scm"),
669        comment_kinds: &["comment"],
670        module_path: typescript_module_path,
671        path_separators: &["/", "."],
672        absolutize: typescript_absolutize,
673        receivers: &["this"],
674        doc_skip_kinds: &[],
675        manifest: None,
676        inline: None,
677        file_refs: false,
678        implicit_interfaces: false,
679    },
680    LanguageSpec {
681        name: "bash",
682        extensions: &["sh", "bash"],
683        grammar: bash_grammar,
684        query_source: include_str!("../queries/bash.scm"),
685        comment_kinds: &["comment"],
686        module_path: bash_module_path,
687        path_separators: &["/"],
688        absolutize: bash_absolutize,
689        receivers: &[],
690        doc_skip_kinds: &[],
691        manifest: None,
692        inline: None,
693        file_refs: false,
694        implicit_interfaces: false,
695    },
696    LanguageSpec {
697        name: "proto",
698        extensions: &["proto"],
699        grammar: proto_grammar,
700        query_source: include_str!("../queries/proto.scm"),
701        comment_kinds: &["comment"],
702        module_path: proto_module_path,
703        path_separators: &["/", "."],
704        absolutize: proto_absolutize,
705        receivers: &[],
706        doc_skip_kinds: &[],
707        manifest: None,
708        inline: None,
709        file_refs: false,
710        implicit_interfaces: false,
711    },
712    LanguageSpec {
713        name: "cpp",
714        extensions: &["cpp", "cc", "cxx", "hpp", "hh", "hxx", "h"],
715        grammar: cpp_grammar,
716        query_source: include_str!("../queries/cpp.scm"),
717        comment_kinds: &["comment"],
718        module_path: cpp_module_path,
719        path_separators: &["/", "::"],
720        absolutize: cpp_absolutize,
721        receivers: &["this"],
722        doc_skip_kinds: &["expression_statement"],
723        manifest: None,
724        inline: None,
725        file_refs: false,
726        implicit_interfaces: false,
727    },
728    LanguageSpec {
729        name: "javascript",
730        extensions: &["js", "jsx", "mjs", "cjs"],
731        grammar: javascript_grammar,
732        query_source: include_str!("../queries/javascript.scm"),
733        comment_kinds: &["comment"],
734        module_path: javascript_module_path,
735        path_separators: &["/", "."],
736        absolutize: javascript_absolutize,
737        receivers: &["this"],
738        doc_skip_kinds: &[],
739        manifest: None,
740        inline: None,
741        file_refs: false,
742        implicit_interfaces: false,
743    },
744    LanguageSpec {
745        name: "c",
746        extensions: &["c"],
747        grammar: c_grammar,
748        query_source: include_str!("../queries/c.scm"),
749        comment_kinds: &["comment"],
750        module_path: c_module_path,
751        path_separators: &["/"],
752        absolutize: c_absolutize,
753        receivers: &[],
754        doc_skip_kinds: &[],
755        manifest: None,
756        inline: None,
757        file_refs: false,
758        implicit_interfaces: false,
759    },
760    LanguageSpec {
761        name: "java",
762        extensions: &["java"],
763        grammar: java_grammar,
764        query_source: include_str!("../queries/java.scm"),
765        comment_kinds: &["line_comment", "block_comment"],
766        module_path: java_module_path,
767        path_separators: &["."],
768        absolutize: java_absolutize,
769        receivers: &["this"],
770        doc_skip_kinds: &[],
771        manifest: None,
772        inline: None,
773        file_refs: false,
774        implicit_interfaces: false,
775    },
776    LanguageSpec {
777        name: "csharp",
778        extensions: &["cs"],
779        grammar: csharp_grammar,
780        query_source: include_str!("../queries/csharp.scm"),
781        comment_kinds: &["comment"],
782        module_path: csharp_module_path,
783        path_separators: &["."],
784        absolutize: dotted_absolutize,
785        receivers: &["this", "base"],
786        doc_skip_kinds: &[],
787        manifest: None,
788        inline: None,
789        file_refs: false,
790        implicit_interfaces: false,
791    },
792    LanguageSpec {
793        name: "sql",
794        extensions: &["sql"],
795        grammar: sql_grammar,
796        query_source: include_str!("../queries/sql.scm"),
797        comment_kinds: &["comment", "marginalia"],
798        module_path: sql_module_path,
799        path_separators: &["."],
800        absolutize: dotted_absolutize,
801        receivers: &[],
802        doc_skip_kinds: &[],
803        manifest: None,
804        inline: None,
805        file_refs: false,
806        implicit_interfaces: false,
807    },
808    LanguageSpec {
809        name: "markdown",
810        extensions: &["md", "markdown"],
811        grammar: markdown_grammar,
812        query_source: include_str!("../queries/markdown.scm"),
813        comment_kinds: &[],
814        module_path: markdown_module_path,
815        path_separators: &["/"],
816        absolutize: markdown_absolutize,
817        receivers: &[],
818        doc_skip_kinds: &[],
819        manifest: None,
820        inline: Some(&MARKDOWN_INLINE),
821        file_refs: true,
822        implicit_interfaces: false,
823    },
824];
825
826/// Spec for a file, by extension.
827pub fn spec_for_path(path: &str) -> Option<&'static LanguageSpec> {
828    let ext = path.rsplit('.').next()?;
829    LANGUAGES.iter().find(|spec| spec.extensions.contains(&ext))
830}