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    /// Interface satisfaction is implicit (Go): no syntax names the
142    /// interface at the implementing type, so the resolver runs a
143    /// structural method-set pass instead of `@trait`/`@trait.impl`
144    /// pairing. Pure data; the engine never branches on language name.
145    pub implicit_interfaces: bool,
146}
147
148fn rust_normalize(name: &str) -> String {
149    name.replace('-', "_")
150}
151
152static RUST_MANIFEST: ManifestSpec = ManifestSpec {
153    filename: "Cargo.toml",
154    name_key: "name",
155    self_names: &["crate"],
156    normalize: rust_normalize,
157};
158
159fn identity_normalize(name: &str) -> String {
160    name.to_string()
161}
162
163/// `module example.com/proj` — the declared module path anchors full-path
164/// imports to the repo directory holding go.mod. No self-alias: Go import
165/// paths are always absolute.
166static GO_MANIFEST: ManifestSpec = ManifestSpec {
167    filename: "go.mod",
168    name_key: "module",
169    self_names: &[],
170    normalize: identity_normalize,
171};
172
173fn split_all(path: &str, separators: &[&str]) -> Vec<String> {
174    let mut segments = vec![path.to_string()];
175    for sep in separators {
176        segments = segments
177            .iter()
178            .flat_map(|s| s.split(sep).map(str::to_string))
179            .collect();
180    }
181    segments.into_iter().filter(|s| !s.is_empty()).collect()
182}
183
184fn dirname_segments(file: &str) -> Vec<String> {
185    let mut segments: Vec<String> = file.split('/').map(str::to_string).collect();
186    segments.pop();
187    segments
188}
189
190/// `crate::x` stays absolute; `super::x`/`self::x` resolve against the
191/// file's own module path.
192fn rust_absolutize(path: &str, file: &str) -> Vec<String> {
193    let mut module = rust_module_path(file);
194    let mut rest = path;
195    if let Some(r) = rest.strip_prefix("self::") {
196        rest = r;
197    } else {
198        while let Some(r) = rest.strip_prefix("super::") {
199            module.pop();
200            rest = r;
201        }
202        if rest.len() == path.len() {
203            if path.starts_with("crate::") {
204                return split_all(path, &["::", "."]);
205            }
206            // Bare paths are relative to the file's own module
207            // (`internal::helper` inside lib.rs means crate::internal::...).
208            module.extend(split_all(path, &["::", "."]));
209            return module;
210        }
211    }
212    module.extend(split_all(rest, &["::", "."]));
213    module
214}
215
216fn go_absolutize(path: &str, _file: &str) -> Vec<String> {
217    split_all(path, &["/", "."])
218}
219
220/// `acme/core/v1/action_event.proto` -> ["acme","core","v1","action_event"]:
221/// the file extension must not become a module segment, or the import key
222/// never suffix-matches the file's own module path and same-package bare
223/// references stay unresolved.
224fn proto_absolutize(path: &str, _file: &str) -> Vec<String> {
225    split_all(path.strip_suffix(".proto").unwrap_or(path), &["/", "."])
226}
227
228/// Leading dots are package-relative: one dot is the file's own package,
229/// each further dot one package up.
230fn python_absolutize(path: &str, file: &str) -> Vec<String> {
231    let dots = path.len() - path.trim_start_matches('.').len();
232    if dots == 0 {
233        return split_all(path, &["."]);
234    }
235    let mut base = dirname_segments(file);
236    for _ in 1..dots {
237        base.pop();
238    }
239    base.extend(split_all(&path[dots..], &["."]));
240    base
241}
242
243/// `./x` and `../x` resolve against the file's directory.
244fn typescript_absolutize(path: &str, file: &str) -> Vec<String> {
245    if !path.starts_with('.') {
246        return split_all(path, &["/", "."]);
247    }
248    let mut base = dirname_segments(file);
249    let mut rest = path;
250    // Leading `/` is the repo-root convention (GitHub-style), not a
251    // relative segment: resolve against the root, not the linking file.
252    if let Some(r) = rest.strip_prefix('/') {
253        base.clear();
254        rest = r;
255    }
256    loop {
257        if let Some(r) = rest.strip_prefix("./") {
258            rest = r;
259        } else if let Some(r) = rest.strip_prefix("../") {
260            base.pop();
261            rest = r;
262        } else {
263            break;
264        }
265    }
266    base.extend(split_all(rest, &["/"]));
267    base
268}
269
270fn rust_grammar() -> Language {
271    tree_sitter_rust::LANGUAGE.into()
272}
273
274fn go_grammar() -> Language {
275    tree_sitter_go::LANGUAGE.into()
276}
277
278/// `src/util.rs` -> ["crate", "util"]; `src/foo/mod.rs` -> ["crate", "foo"].
279/// Single-crate view by design: the resolver's manifest-aware `key_of`
280/// replaces the "crate" head with the declared package name in workspaces.
281fn rust_module_path(file: &str) -> Vec<String> {
282    let trimmed = file.strip_suffix(".rs").unwrap_or(file);
283    let after_src = trimmed.rsplit_once("src/").map_or(trimmed, |(_, r)| r);
284    let mut segments = vec!["crate".to_string()];
285    for seg in after_src.split('/') {
286        if !matches!(seg, "lib" | "main" | "mod" | "") {
287            segments.push(seg.to_string());
288        }
289    }
290    segments
291}
292
293/// `pkg/util/util.go` -> ["pkg", "util"]; root files -> [].
294fn go_module_path(file: &str) -> Vec<String> {
295    let mut segments: Vec<String> = file.split('/').map(str::to_string).collect();
296    segments.pop(); // file name; Go packages are directories
297    segments
298}
299
300fn python_grammar() -> Language {
301    tree_sitter_python::LANGUAGE.into()
302}
303
304fn bash_grammar() -> Language {
305    tree_sitter_bash::LANGUAGE.into()
306}
307
308/// Bash has no module system: a file is its path. `lib/util.sh` ->
309/// ["lib", "util"].
310fn bash_module_path(file: &str) -> Vec<String> {
311    let trimmed = file
312        .strip_suffix(".sh")
313        .or_else(|| file.strip_suffix(".bash"))
314        .unwrap_or(file);
315    trimmed
316        .split('/')
317        .filter(|s| !s.is_empty())
318        .map(str::to_string)
319        .collect()
320}
321
322/// `source` paths: strip `$(dirname "$0")/` and `./` style prefixes and
323/// resolve against the sourcing file's directory; bare paths pass through.
324fn bash_absolutize(path: &str, file: &str) -> Vec<String> {
325    let trimmed = path.trim();
326    let dir_relative = [
327        "$(dirname \"$0\")/",
328        "$(dirname $0)/",
329        "${BASH_SOURCE%/*}/",
330        "./",
331    ]
332    .iter()
333    .find_map(|p| trimmed.strip_prefix(p));
334    let stripped = |s: &str| {
335        s.strip_suffix(".sh")
336            .or_else(|| s.strip_suffix(".bash"))
337            .unwrap_or(s)
338            .to_string()
339    };
340    match dir_relative {
341        Some(rest) => {
342            let mut base = dirname_segments(file);
343            base.extend(rest.split('/').filter(|s| !s.is_empty()).map(stripped));
344            base
345        }
346        None => trimmed
347            .split('/')
348            .filter(|s| !s.is_empty() && *s != ".")
349            .map(stripped)
350            .collect(),
351    }
352}
353
354fn cpp_grammar() -> Language {
355    tree_sitter_cpp::LANGUAGE.into()
356}
357
358/// `player/character.h` and `player/character.cpp` share the module
359/// ["player", "character"] — header/impl pairs resolve into one another.
360fn cpp_module_path(file: &str) -> Vec<String> {
361    let trimmed = file.rsplit_once('.').map_or(file, |(stem, _)| stem);
362    trimmed
363        .split('/')
364        .filter(|s| !s.is_empty())
365        .map(str::to_string)
366        .collect()
367}
368
369/// Quoted include paths, extension-stripped, `./` resolved against the
370/// including file's directory; `<system>` includes pass through (and stay
371/// external unless a matching module exists in the corpus).
372fn cpp_absolutize(path: &str, file: &str) -> Vec<String> {
373    let trimmed = path.trim().trim_matches(['<', '>']);
374    let no_ext = trimmed.rsplit_once('.').map_or(trimmed, |(stem, ext)| {
375        if matches!(
376            ext,
377            "h" | "hh" | "hpp" | "hxx" | "cpp" | "cc" | "cxx" | "inl"
378        ) {
379            stem
380        } else {
381            trimmed
382        }
383    });
384    if let Some(rest) = no_ext.strip_prefix("./") {
385        let mut base = dirname_segments(file);
386        base.extend(
387            rest.split('/')
388                .filter(|s| !s.is_empty())
389                .map(str::to_string),
390        );
391        return base;
392    }
393    // Member access (`c.jump`, `this->jump`) must split so the resolver's
394    // receiver/typed-local tiers see a prefix (fixture: cpp-header-impl).
395    no_ext
396        .replace("->", ".")
397        .split(['/', ':', '.'])
398        .filter(|s| !s.is_empty())
399        .map(str::to_string)
400        .collect()
401}
402
403fn proto_grammar() -> Language {
404    tree_sitter_proto::LANGUAGE.into()
405}
406
407/// `contracts/payments.proto` -> ["contracts", "payments"]. Generated-stub
408/// imports name proto symbols through package paths; module keys are the
409/// file path, packages resolve via the same suffix matching as Go.
410fn proto_module_path(file: &str) -> Vec<String> {
411    let trimmed = file.strip_suffix(".proto").unwrap_or(file);
412    trimmed
413        .split('/')
414        .filter(|s| !s.is_empty())
415        .map(str::to_string)
416        .collect()
417}
418
419fn typescript_grammar() -> Language {
420    tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()
421}
422
423/// `pkg/mod.py` -> ["pkg", "mod"]; `pkg/__init__.py` -> ["pkg"].
424fn python_module_path(file: &str) -> Vec<String> {
425    let trimmed = file.strip_suffix(".py").unwrap_or(file);
426    trimmed
427        .split('/')
428        .filter(|s| !matches!(*s, "__init__" | ""))
429        .map(str::to_string)
430        .collect()
431}
432
433/// `src/util.ts` -> ["src", "util"]; `src/index.ts` -> ["src"].
434fn typescript_module_path(file: &str) -> Vec<String> {
435    let trimmed = file
436        .strip_suffix(".tsx")
437        .or_else(|| file.strip_suffix(".ts"))
438        .unwrap_or(file);
439    trimmed
440        .split('/')
441        .filter(|s| !matches!(*s, "index" | ""))
442        .map(str::to_string)
443        .collect()
444}
445
446fn javascript_grammar() -> Language {
447    tree_sitter_javascript::LANGUAGE.into()
448}
449
450fn c_grammar() -> Language {
451    tree_sitter_c::LANGUAGE.into()
452}
453
454fn java_grammar() -> Language {
455    tree_sitter_java::LANGUAGE.into()
456}
457
458fn csharp_grammar() -> Language {
459    tree_sitter_c_sharp::LANGUAGE.into()
460}
461
462/// C# module identity is the file's directory, Go-style: `using Acme.Util;`
463/// imports a namespace (never a type), and namespaces conventionally mirror
464/// directories. `Acme/Util/TextHelper.cs` -> ["Acme", "Util"], so the using
465/// directive's segments suffix-match the directory key. Path-derived only:
466/// namespace declarations that diverge from layout are out of scope
467/// (stated boundary of the csharp pack, see queries/csharp.scm).
468fn csharp_module_path(file: &str) -> Vec<String> {
469    let mut segments: Vec<String> = file.split('/').map(str::to_string).collect();
470    segments.pop(); // file name; namespaces are directories
471    segments
472}
473
474fn markdown_grammar() -> Language {
475    tree_sitter_md::LANGUAGE.into()
476}
477
478fn markdown_inline_grammar() -> Language {
479    tree_sitter_md::INLINE_LANGUAGE.into()
480}
481
482/// Inline content (`[text](target)`) is a second grammar in tree-sitter's
483/// markdown split; the engine parses the block tree's `inline` ranges
484/// with it (see InlineSpec).
485static MARKDOWN_INLINE: InlineSpec = InlineSpec {
486    grammar: markdown_inline_grammar,
487    query_source: include_str!("../queries/markdown-inline.scm"),
488    container_kinds: &["inline"],
489};
490
491/// Link destinations resolve against the linking file's directory —
492/// `./x`, `../x`, and bare `x` alike (doc-link convention); the `.md`
493/// extension is not a module segment.
494fn markdown_absolutize(path: &str, file: &str) -> Vec<String> {
495    let mut base = dirname_segments(file);
496    let mut rest = path;
497    // Leading `/` is the repo-root convention (GitHub-style), not a
498    // relative segment: resolve against the root, not the linking file.
499    if let Some(r) = rest.strip_prefix('/') {
500        base.clear();
501        rest = r;
502    }
503    loop {
504        if let Some(r) = rest.strip_prefix("./") {
505            rest = r;
506        } else if let Some(r) = rest.strip_prefix("../") {
507            base.pop();
508            rest = r;
509        } else {
510            break;
511        }
512    }
513    let rest = rest
514        .strip_suffix(".md")
515        .or_else(|| rest.strip_suffix(".markdown"))
516        .unwrap_or(rest);
517    base.extend(
518        rest.split('/')
519            .filter(|s| !s.is_empty())
520            .map(str::to_string),
521    );
522    base
523}
524
525/// A document is its path: `docs/team.md` -> ["docs", "team"].
526fn markdown_module_path(file: &str) -> Vec<String> {
527    let trimmed = file
528        .strip_suffix(".md")
529        .or_else(|| file.strip_suffix(".markdown"))
530        .unwrap_or(file);
531    trimmed
532        .split('/')
533        .filter(|s| !s.is_empty())
534        .map(str::to_string)
535        .collect()
536}
537
538fn sql_grammar() -> Language {
539    tree_sitter_sequel::LANGUAGE.into()
540}
541
542fn javascript_module_path(file: &str) -> Vec<String> {
543    let trimmed = file
544        .strip_suffix(".jsx")
545        .or_else(|| file.strip_suffix(".mjs"))
546        .or_else(|| file.strip_suffix(".cjs"))
547        .or_else(|| file.strip_suffix(".js"))
548        .unwrap_or(file);
549    trimmed
550        .split('/')
551        .filter(|s| !matches!(*s, "index" | ""))
552        .map(str::to_string)
553        .collect()
554}
555
556fn javascript_absolutize(path: &str, file: &str) -> Vec<String> {
557    typescript_absolutize(path, file)
558}
559
560fn c_module_path(file: &str) -> Vec<String> {
561    cpp_module_path(file)
562}
563
564fn c_absolutize(path: &str, file: &str) -> Vec<String> {
565    cpp_absolutize(path, file)
566}
567
568/// Java packages are directories: `com/acme/util/Text.java` ->
569/// ["com", "acme", "util"], matching a path-aligned `package com.acme.util;`.
570/// The class contributes its own segment as a definition, so the FQN
571/// `com.acme.util.Text` resolves as module + top-level def.
572// ponytail: assumes package declarations align with directories; parse the
573// package_declaration into module identity if misaligned repos matter.
574fn java_module_path(file: &str) -> Vec<String> {
575    dirname_segments(file)
576        .into_iter()
577        .filter(|s| !s.is_empty())
578        .collect()
579}
580
581/// Java qualified references arrive as full invocation text
582/// (`Text.trim(s)`): everything from the first `(` on is arguments, not
583/// path. Imports are already-absolute dotted FQNs.
584fn java_absolutize(path: &str, _file: &str) -> Vec<String> {
585    let head = path.split('(').next().unwrap_or(path);
586    head.split('.')
587        .map(str::trim)
588        .filter(|s| !s.is_empty())
589        .map(str::to_string)
590        .collect()
591}
592
593/// All .sql files in a directory share one namespace (a database schema
594/// has no per-file scoping), so the module key is the directory alone:
595/// `db/schema.sql` and `db/queries.sql` both map to ["db"].
596fn sql_module_path(file: &str) -> Vec<String> {
597    let dir = file.rsplit_once('/').map_or("", |(dir, _)| dir);
598    dir.split('/')
599        .filter(|s| !s.is_empty())
600        .map(str::to_string)
601        .collect()
602}
603
604/// Dotted absolute imports (C#/SQL style): already-absolute FQNs split
605/// on dots; no relative forms exist.
606fn dotted_absolutize(path: &str, _file: &str) -> Vec<String> {
607    path.trim()
608        .split('.')
609        .filter(|s| !s.is_empty())
610        .map(str::to_string)
611        .collect()
612}
613
614pub static LANGUAGES: &[LanguageSpec] = &[
615    LanguageSpec {
616        name: "rust",
617        extensions: &["rs"],
618        grammar: rust_grammar,
619        query_source: include_str!("../queries/rust.scm"),
620        comment_kinds: &["line_comment", "block_comment"],
621        module_path: rust_module_path,
622        path_separators: &["::", "."],
623        absolutize: rust_absolutize,
624        receivers: &["self", "Self"],
625        doc_skip_kinds: &[],
626        manifest: Some(&RUST_MANIFEST),
627        inline: None,
628        file_refs: false,
629        implicit_interfaces: false,
630    },
631    LanguageSpec {
632        name: "go",
633        extensions: &["go"],
634        grammar: go_grammar,
635        query_source: include_str!("../queries/go.scm"),
636        comment_kinds: &["comment"],
637        module_path: go_module_path,
638        path_separators: &["/", "."],
639        absolutize: go_absolutize,
640        receivers: &[],
641        doc_skip_kinds: &[],
642        manifest: Some(&GO_MANIFEST),
643        inline: None,
644        file_refs: false,
645        implicit_interfaces: true,
646    },
647    LanguageSpec {
648        name: "python",
649        extensions: &["py"],
650        grammar: python_grammar,
651        query_source: include_str!("../queries/python.scm"),
652        comment_kinds: &["comment"],
653        module_path: python_module_path,
654        path_separators: &["."],
655        absolutize: python_absolutize,
656        receivers: &["self", "cls"],
657        doc_skip_kinds: &[],
658        manifest: None,
659        inline: None,
660        file_refs: false,
661        implicit_interfaces: false,
662    },
663    LanguageSpec {
664        name: "typescript",
665        extensions: &["ts", "tsx"],
666        grammar: typescript_grammar,
667        query_source: include_str!("../queries/typescript.scm"),
668        comment_kinds: &["comment"],
669        module_path: typescript_module_path,
670        path_separators: &["/", "."],
671        absolutize: typescript_absolutize,
672        receivers: &["this"],
673        doc_skip_kinds: &[],
674        manifest: None,
675        inline: None,
676        file_refs: false,
677        implicit_interfaces: false,
678    },
679    LanguageSpec {
680        name: "bash",
681        extensions: &["sh", "bash"],
682        grammar: bash_grammar,
683        query_source: include_str!("../queries/bash.scm"),
684        comment_kinds: &["comment"],
685        module_path: bash_module_path,
686        path_separators: &["/"],
687        absolutize: bash_absolutize,
688        receivers: &[],
689        doc_skip_kinds: &[],
690        manifest: None,
691        inline: None,
692        file_refs: false,
693        implicit_interfaces: false,
694    },
695    LanguageSpec {
696        name: "proto",
697        extensions: &["proto"],
698        grammar: proto_grammar,
699        query_source: include_str!("../queries/proto.scm"),
700        comment_kinds: &["comment"],
701        module_path: proto_module_path,
702        path_separators: &["/", "."],
703        absolutize: proto_absolutize,
704        receivers: &[],
705        doc_skip_kinds: &[],
706        manifest: None,
707        inline: None,
708        file_refs: false,
709        implicit_interfaces: false,
710    },
711    LanguageSpec {
712        name: "cpp",
713        extensions: &["cpp", "cc", "cxx", "hpp", "hh", "hxx", "h"],
714        grammar: cpp_grammar,
715        query_source: include_str!("../queries/cpp.scm"),
716        comment_kinds: &["comment"],
717        module_path: cpp_module_path,
718        path_separators: &["/", "::"],
719        absolutize: cpp_absolutize,
720        receivers: &["this"],
721        doc_skip_kinds: &["expression_statement"],
722        manifest: None,
723        inline: None,
724        file_refs: false,
725        implicit_interfaces: false,
726    },
727    LanguageSpec {
728        name: "javascript",
729        extensions: &["js", "jsx", "mjs", "cjs"],
730        grammar: javascript_grammar,
731        query_source: include_str!("../queries/javascript.scm"),
732        comment_kinds: &["comment"],
733        module_path: javascript_module_path,
734        path_separators: &["/", "."],
735        absolutize: javascript_absolutize,
736        receivers: &["this"],
737        doc_skip_kinds: &[],
738        manifest: None,
739        inline: None,
740        file_refs: false,
741        implicit_interfaces: false,
742    },
743    LanguageSpec {
744        name: "c",
745        extensions: &["c"],
746        grammar: c_grammar,
747        query_source: include_str!("../queries/c.scm"),
748        comment_kinds: &["comment"],
749        module_path: c_module_path,
750        path_separators: &["/"],
751        absolutize: c_absolutize,
752        receivers: &[],
753        doc_skip_kinds: &[],
754        manifest: None,
755        inline: None,
756        file_refs: false,
757        implicit_interfaces: false,
758    },
759    LanguageSpec {
760        name: "java",
761        extensions: &["java"],
762        grammar: java_grammar,
763        query_source: include_str!("../queries/java.scm"),
764        comment_kinds: &["line_comment", "block_comment"],
765        module_path: java_module_path,
766        path_separators: &["."],
767        absolutize: java_absolutize,
768        receivers: &["this"],
769        doc_skip_kinds: &[],
770        manifest: None,
771        inline: None,
772        file_refs: false,
773        implicit_interfaces: false,
774    },
775    LanguageSpec {
776        name: "csharp",
777        extensions: &["cs"],
778        grammar: csharp_grammar,
779        query_source: include_str!("../queries/csharp.scm"),
780        comment_kinds: &["comment"],
781        module_path: csharp_module_path,
782        path_separators: &["."],
783        absolutize: dotted_absolutize,
784        receivers: &["this", "base"],
785        doc_skip_kinds: &[],
786        manifest: None,
787        inline: None,
788        file_refs: false,
789        implicit_interfaces: false,
790    },
791    LanguageSpec {
792        name: "sql",
793        extensions: &["sql"],
794        grammar: sql_grammar,
795        query_source: include_str!("../queries/sql.scm"),
796        comment_kinds: &["comment", "marginalia"],
797        module_path: sql_module_path,
798        path_separators: &["."],
799        absolutize: dotted_absolutize,
800        receivers: &[],
801        doc_skip_kinds: &[],
802        manifest: None,
803        inline: None,
804        file_refs: false,
805        implicit_interfaces: false,
806    },
807    LanguageSpec {
808        name: "markdown",
809        extensions: &["md", "markdown"],
810        grammar: markdown_grammar,
811        query_source: include_str!("../queries/markdown.scm"),
812        comment_kinds: &[],
813        module_path: markdown_module_path,
814        path_separators: &["/"],
815        absolutize: markdown_absolutize,
816        receivers: &[],
817        doc_skip_kinds: &[],
818        manifest: None,
819        inline: Some(&MARKDOWN_INLINE),
820        file_refs: true,
821        implicit_interfaces: false,
822    },
823];
824
825/// Spec for a file, by extension.
826pub fn spec_for_path(path: &str) -> Option<&'static LanguageSpec> {
827    let ext = path.rsplit('.').next()?;
828    LANGUAGES.iter().find(|spec| spec.extensions.contains(&ext))
829}