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