Skip to main content

bonsai_lang_api/
lib.rs

1//! Extension surface for language adapters (spec §5).
2//!
3//! A new language is added by implementing [`LanguageAdapter`] in a
4//! `lang_<name>` crate and registering it on a [`LanguageRegistry`]. Core
5//! crates must depend only on `lang_api`, never on concrete adapters.
6
7pub mod capabilities;
8pub mod kit;
9mod parse_recovery;
10pub mod registry;
11mod storage;
12pub mod taxonomy;
13pub mod types;
14
15pub use capabilities::{
16    callable_reference_variants, CallTextPrefilter, CallableDeclarationFamily, CallableReferenceSyntax,
17    CapabilityLevel, LanguageCapabilities, ModulePathSyntax, ReceiverTypeSyntax, NO_CONSTRUCTOR_METHOD_NAMES,
18};
19pub use kit::{
20    alias_map_from_import_specs, alias_map_from_imports, apply_assign_call_result_types,
21    apply_call_receiver_types, apply_call_receiver_types_with_language_syntax,
22    apply_call_receiver_types_with_super_tokens, apply_class_field_type_aliases,
23    apply_constructor_result_type_aliases, apply_expression_value_kinds, apply_file_stem_semantic_identity,
24    apply_local_closure_captures, apply_module_path_semantic_identity, assignment_trace_message,
25    c_family_preproc_imports, collect_assign_targets, collect_constructor_result_type_aliases,
26    collect_modifier_visibility, collect_param_type_aliases, collect_receiver_field_initializers,
27    collect_return_spans, decl_index_from_tree_with_handler, decl_index_with_handler,
28    extend_alias_map_with_flow_events, extract_assignment_value_facts, extract_branch_condition_facts,
29    extract_call_argument_value_facts, extract_call_receiver_facts, extract_imports_via,
30    extract_runtime_type_narrowing_facts, for_each_flow_event, mark_namespace_call_receivers,
31    module_local_binding, normalize_call_result_assignment_sources, populate_decl_return_types,
32    qualify_implicit_member_assign_targets, qualify_implicit_member_reads_in_index,
33    qualify_receiver_field_expression_flows, rewrite_implicit_member_reads, tuple_result_projection_index,
34    AliasTarget, AssignmentNodeSemantics, CallTargetExtraction, ExpressionPlaceExtraction,
35    FunctionDefinitionExtraction, GrammarHandler, ImplicitMemberReadCall, ModifierVocabulary,
36    PatternBindingSite, PatternSourceProjection, ProjectedPatternBindingSite, SyntaxSpecialForm,
37    TypeAliasVocabulary, EMPTY_HANDLER, MODULE_DECL_NAME, WILDCARD_IMPORT_ALIAS_PREFIX,
38};
39pub use parse_recovery::{
40    branch_free_conditional_recovery_edits, c_family_declaration_macro_recovery_edits, syntax_damage_score,
41    ConditionalDirectiveSyntax, ParseRecoveryEdit,
42};
43pub use registry::{AdapterArc, LanguageRegistry};
44pub use taxonomy::{flow_edge_spec, FlowEdgeKind, FlowEdgeSpec, FlowEdgeSupport, FLOW_EDGE_TAXONOMY};
45pub use types::{
46    assignment_value_fact_for_span, assignment_value_rendering, branch_condition_fact_for_span,
47    call_argument_value_fact, call_receiver_fact_for_span, character_constraints_from_substitutions,
48    finite_literal_selection_for_assignment, operations_from_flow_events, AggregateLayout,
49    ArgumentPassingMode, AssignValueKind, AssignmentValueFact, AssignmentValueIndex, BranchConditionFact,
50    BranchConditionPolarity, CallArg, CallArgumentValueFact, CallKind, CallReceiverFact, CallReceiverRole,
51    CharacterClass, CharacterConstraintDomain, CharacterConstraintFact, CharacterConstraintOutput,
52    CharacterSubstitutionDomain, CharacterSubstitutionFact, Comment, CommentKind, CompilerAssignmentAlias,
53    CompilerAttribution, CompilerBrowseHeader, CompilerBrowseTermGroup, CompilerCallArgumentAttribution,
54    CompilerCallAttribution, CompilerCallHeader, CompilerFactoryCallAssignment, CompilerFunctionAttribution,
55    CompilerGuardFact, CompilerReturnHeader, CompilerSyntaxHeader, CompilerWriteAttribution,
56    ConditionEquality, ConditionExpressionFact, ConditionOperandFact, Decl, DeclIndex, DeclKind,
57    DynamicKeyFilterFact, ExpressionField, ExpressionFlow, ExpressionProjection, FieldWrite,
58    FiniteLiteralSelectionFact, FlowEvent, GuardedValueFilterFact, ImportIndex, ImportScope, ImportSpec,
59    LanguageId, LoopKind, MembershipConditionFact, ModulePath, Operation, OperationKind, OperationOperand,
60    OperationOperandRole, ReceiverFieldInitializer, Ref, RefKind, RuntimeTypeNarrowingFact,
61    SameOriginPathConstraintFact, StaticAggregateFieldValue, StaticScalarValue, StaticStringMapEntry,
62    StaticStringMapFact, StringCategory, StringCompositionFact, StringCompositionPart, StringLiteral,
63    TypeAliasBinding, UnsupportedConstruct, Visibility, WorkspaceRoot,
64    COMPILER_GUARD_RELATIVE_PATH_BOUNDARY_REJECTION,
65};
66
67use bonsai_common::FileId;
68use bonsai_diagnostics::DiagnosticSink;
69pub use bonsai_vfs::{FileSnapshot, Vfs};
70use parking_lot::RwLock;
71use std::sync::Arc;
72pub use tree_sitter::Tree as SyntaxTree;
73
74/// Canonical tree-sitter tree provider used by adapters.
75///
76/// The analyzer database implements this with its versioned parser cache, so
77/// every adapter pass over one file shares the same tree instead of creating
78/// another parser and reparsing identical source. Standalone adapter tests may
79/// omit the provider and use the direct fallback in `kit::parse_with`.
80pub trait TreeProvider: Send + Sync {
81    /// Return the tree for this exact immutable snapshot and grammar.
82    ///
83    /// `pack_name` is the adapter-selected grammar variant, not necessarily
84    /// the adapter's public language id. Taking the snapshot rather than only a [`FileId`] is part of the
85    /// correctness contract: a concurrent VFS write must never pair an older
86    /// source snapshot with a newer syntax tree.
87    fn tree_for_snapshot(&self, pack_name: &str, snapshot: &FileSnapshot) -> Option<Arc<SyntaxTree>>;
88}
89
90/// Read-only view of the pieces of the analyzer database that adapters need.
91///
92/// Adapters must not see query internals or other adapters; this struct is
93/// the full surface area. Keep it minimal — if a new adapter needs something
94/// it isn't here, add it here rather than giving adapters a `Db` handle.
95pub struct AdapterContext<'a> {
96    pub vfs: &'a Vfs,
97    pub diagnostics: &'a RwLock<DiagnosticSink>,
98    /// Versioned parser/tree cache supplied by the analyzer database.
99    pub tree_provider: Option<&'a dyn TreeProvider>,
100    /// Absolute path of the workspace root the adapter is running
101    /// against. `None` for adapter unit tests that synthesize a Vfs
102    /// without a workspace. Adapters use this to compute
103    /// workspace-relative module paths for `Decl.qualified_name` and
104    /// `Decl.module_path` — see
105    /// `docs/contributing/design-patterns.mdx::Semantic Resolution Always`.
106    pub workspace_root: Option<&'a std::path::Path>,
107}
108
109impl<'a> AdapterContext<'a> {
110    pub fn emit(&self, diag: bonsai_diagnostics::Diagnostic) {
111        self.diagnostics.write().push(diag);
112    }
113
114    /// Workspace-relative path for `file`, or `None` when no
115    /// workspace root is set or the file isn't under the root.
116    /// Adapters use this in lieu of raw `vfs.path(file)` to derive
117    /// stable module paths that match between CLI and SDK callers.
118    #[must_use]
119    pub fn workspace_relative_path(&self, file: bonsai_common::FileId) -> Option<std::path::PathBuf> {
120        let path = self.vfs.path(file).ok()?;
121        let root = self.workspace_root?;
122        path.strip_prefix(root).ok().map(std::path::Path::to_path_buf)
123    }
124}
125
126/// Grammar-owned wrapper for parsing a source fragment outside its original
127/// file context. Most grammars accept fragments directly; adapters override
128/// this only when their root grammar has a distinct host-language mode.
129#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
130pub struct FragmentParseContext {
131    /// Bytes inserted before the fragment solely for parsing.
132    pub prefix: &'static str,
133    /// Bytes inserted after the fragment solely for parsing.
134    pub suffix: &'static str,
135}
136
137/// Adapter-owned proof for selecting among grammars that share a file
138/// extension.
139#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
140pub enum LanguageOwnershipEvidence {
141    /// This specialized grammar's distinguishing syntax is absent. A generic
142    /// compatible grammar should own the ambiguous-extension file instead.
143    Excluded,
144    /// The grammar exposed no syntax unique to this language. Selection falls
145    /// back to concrete syntax damage.
146    #[default]
147    Unproven,
148    /// The concrete tree contains grammar-owned syntax that proves this
149    /// language owns the file.
150    Proven,
151}
152
153impl LanguageOwnershipEvidence {
154    #[must_use]
155    pub const fn selection_rank(self) -> u8 {
156        match self {
157            Self::Excluded => 0,
158            Self::Unproven => 1,
159            Self::Proven => 2,
160        }
161    }
162}
163
164/// The full contract an adapter must implement. See spec §5.5.
165///
166/// The trait is intentionally object-safe so a registry can store
167/// `Arc<dyn LanguageAdapter>` values.
168pub trait LanguageAdapter: Send + Sync + 'static {
169    /// Short machine identifier (e.g. `"rust"`, `"python"`). Must match the
170    /// key used by `tree-sitter-language-pack` where possible.
171    fn language_id(&self) -> LanguageId;
172
173    /// Human-readable display name.
174    fn display_name(&self) -> &'static str;
175
176    /// File extensions this adapter claims (lowercase, no leading dot).
177    fn file_extensions(&self) -> &'static [&'static str];
178
179    /// The Tree-sitter `Language` used for parsing. Most adapters fetch
180    /// this from `tree_sitter_language_pack::get_language`.
181    fn tree_sitter_language(&self) -> Result<tree_sitter::Language, AdapterError>;
182
183    /// Prove that an ambiguous-extension file belongs to this adapter from
184    /// grammar-owned syntax in its concrete tree.
185    ///
186    /// This is a proof hook, not a filename or token-scoring heuristic. The
187    /// database considers the adapter's evidence before comparing parse
188    /// damage. A specialized superset grammar may return
189    /// [`LanguageOwnershipEvidence::Excluded`] when its distinguishing syntax
190    /// is absent and a generic compatible grammar should own the file. Most
191    /// languages have unambiguous extensions and inherit
192    /// [`LanguageOwnershipEvidence::Unproven`].
193    fn source_syntax_proves_language(
194        &self,
195        _snapshot: &FileSnapshot,
196        _tree: &SyntaxTree,
197    ) -> LanguageOwnershipEvidence {
198        LanguageOwnershipEvidence::Unproven
199    }
200
201    /// Select the exact Tree-sitter grammar pack for one source path.
202    /// Most languages have one grammar and inherit their language id. An
203    /// adapter that owns multiple grammar variants (TypeScript/TSX) selects
204    /// here from file syntax metadata rather than asking the parser or shared
205    /// analyzer to recognize an extension.
206    fn grammar_name_for_path(&self, _path: &std::path::Path) -> &'static str {
207        self.language_id().as_str()
208    }
209
210    /// Load the exact grammar selected for one source path. The default keeps
211    /// existing single-grammar adapters on their normal constructor and loads
212    /// a named variant only when `grammar_name_for_path` differs.
213    fn tree_sitter_language_for_path(
214        &self,
215        path: &std::path::Path,
216    ) -> Result<tree_sitter::Language, AdapterError> {
217        let grammar = self.grammar_name_for_path(path);
218        if grammar == self.language_id().as_str() {
219            self.tree_sitter_language()
220        } else {
221            tree_sitter_language_pack::get_language(grammar)
222                .map_err(|error| AdapterError::GrammarUnavailable(format!("{grammar}: {error}")))
223        }
224    }
225
226    /// Return same-width parser-buffer normalizations for a second,
227    /// grammar-recovery parse.
228    ///
229    /// This hook is deliberately narrower than arbitrary source rewriting:
230    /// adapters can only hide syntax whose role they have independently
231    /// established from compiler facts and the raw CST (for example, a
232    /// declaration macro reached through a C/C++ include, or qualification
233    /// unsupported by an otherwise capable grammar production). The parser
234    /// accepts the recovered tree only when it contains strictly fewer syntax
235    /// errors, and all byte offsets continue to address the original source
236    /// snapshot.
237    fn parse_recovery_edits(
238        &self,
239        _snapshot: &FileSnapshot,
240        _vfs: &Vfs,
241        _tree: &SyntaxTree,
242    ) -> Vec<ParseRecoveryEdit> {
243        Vec::new()
244    }
245
246    /// Wrapper required to parse a standalone mid-file source fragment.
247    /// Returned bytes are adapter grammar metadata and never appear in output.
248    fn fragment_parse_context(&self) -> FragmentParseContext {
249        FragmentParseContext::default()
250    }
251
252    /// What the adapter claims to support; unsupported constructs are
253    /// surfaced as diagnostics by the pipeline.
254    fn capabilities(&self) -> LanguageCapabilities;
255
256    /// Discover workspace roots (packages, modules) from the set of files.
257    /// Default implementation treats the workspace as a single root.
258    fn discover_workspace_roots(&self, _files: &[FileId], _ctx: &AdapterContext<'_>) -> Vec<WorkspaceRoot> {
259        vec![WorkspaceRoot::default()]
260    }
261
262    /// Extract declarations and references from a single file.
263    fn extract_declarations(&self, file: FileId, ctx: &AdapterContext<'_>) -> DeclIndex;
264
265    /// Extract imports / uses / includes from a single file.
266    fn extract_imports(&self, file: FileId, ctx: &AdapterContext<'_>) -> ImportIndex;
267
268    /// Report constructs the adapter saw but does not fully support.
269    fn unsupported_constructs(&self, _file: FileId, _ctx: &AdapterContext<'_>) -> Vec<UnsupportedConstruct> {
270        Vec::new()
271    }
272}
273
274/// Helper alias for shared adapter references.
275pub type DynAdapter = Arc<dyn LanguageAdapter>;
276
277#[derive(Debug, thiserror::Error)]
278pub enum AdapterError {
279    #[error("grammar not available: {0}")]
280    GrammarUnavailable(String),
281    #[error("parser setup failed: {0}")]
282    ParserSetup(String),
283    #[error("parse error: {0}")]
284    Parse(String),
285}