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