use std::cell::Cell;
use std::fmt;
use std::sync::Arc;
use lanekeep_core::FilePath;
use lanekeep_lang::Language;
use lanekeep_lang::binding::{Binding, BindingResolver, ImportedName};
use tree_sitter::{Node, Tree};
use crate::declarations::ExportTarget;
use crate::table;
use crate::types::{Primitive, Symbol, Type};
pub trait ImportResolution {
fn imported_value_type(
&self,
from: &FilePath,
module: &str,
name: &ImportedName,
depth: u32,
) -> Option<Type>;
fn imported_alias_type(
&self,
from: &FilePath,
module: &str,
name: &ImportedName,
depth: u32,
) -> Followed;
fn imported_return_type(
&self,
from: &FilePath,
module: &str,
name: &ImportedName,
depth: u32,
) -> Option<Type>;
fn imported_export(
&self,
from: &FilePath,
module: &str,
name: &ImportedName,
) -> Option<ExportTarget>;
}
const REQUIRED_KINDS: &[&str] = &[
"predefined_type",
"type_annotation",
"type_identifier",
"union_type",
"literal_type",
"type_alias_declaration",
"type_parameter",
"identifier",
"required_parameter",
"optional_parameter",
"variable_declarator",
"comment",
"string",
"template_string",
"true",
"false",
"null",
"undefined",
"number",
"parenthesized_expression",
"binary_expression",
"unary_expression",
"call_expression",
"export_statement",
"export_clause",
"export_specifier",
"namespace_export",
"ambient_declaration",
"lexical_declaration",
"variable_declaration",
"function_signature",
"function_declaration",
"generator_function_declaration",
"class_declaration",
"abstract_class_declaration",
"interface_declaration",
"enum_declaration",
"module",
"internal_module",
"class_heritage",
"extends_clause",
"extends_type_clause",
"import_statement",
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Followed {
Type(Type),
Exhausted,
NotAnAlias,
}
pub(crate) const MAX_DEPTH: u32 = 16;
pub struct TypeScriptOracle<'t> {
tree: &'t Tree,
source: &'t str,
resolver: Arc<dyn BindingResolver>,
file: Option<&'t FilePath>,
imports: Option<&'t dyn ImportResolution>,
exhausted: Option<&'t Cell<bool>>,
}
impl fmt::Debug for TypeScriptOracle<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TypeScriptOracle")
.field("tree", &self.tree)
.field("source_len", &self.source.len())
.field("has_imports", &self.imports.is_some())
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct TypeScriptSupport {
resolver: Arc<dyn BindingResolver>,
}
impl fmt::Debug for TypeScriptSupport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TypeScriptSupport").finish_non_exhaustive()
}
}
impl TypeScriptSupport {
#[must_use]
pub fn probe(language: &dyn Language) -> Option<Self> {
let grammar = language.grammar();
if !REQUIRED_KINDS
.iter()
.all(|kind| grammar.id_for_node_kind(kind, true) != 0)
{
return None;
}
Some(Self {
resolver: language.resolver()?,
})
}
pub(crate) fn resolver(&self) -> &Arc<dyn BindingResolver> {
&self.resolver
}
}
impl<'t> TypeScriptOracle<'t> {
#[must_use]
pub fn new(support: &TypeScriptSupport, tree: &'t Tree, source: &'t str) -> Self {
Self {
tree,
source,
resolver: Arc::clone(&support.resolver),
file: None,
imports: None,
exhausted: None,
}
}
#[must_use]
pub fn with_imports(mut self, file: &'t FilePath, imports: &'t dyn ImportResolution) -> Self {
self.file = Some(file);
self.imports = Some(imports);
self
}
#[must_use]
pub fn with_exhaustion(mut self, exhausted: &'t Cell<bool>) -> Self {
self.exhausted = Some(exhausted);
self
}
fn exhaust<T>(&self) -> Option<T> {
if let Some(flag) = self.exhausted {
flag.set(true);
}
None
}
#[must_use]
pub fn type_of_from(&self, node: Node<'t>, depth: u32) -> Option<Type> {
self.type_of_at(node, depth)
}
#[must_use]
pub fn declaration_type_from(&self, declaration: Node<'t>, depth: u32) -> Option<Type> {
self.declaration_type(declaration, depth)
}
#[must_use]
pub fn annotation_type_from(&self, annotation: Node<'t>, depth: u32) -> Option<Type> {
self.annotation_type(annotation_child(annotation)?, depth)
}
#[must_use]
pub fn return_type_from(&self, node: Node<'t>, depth: u32) -> Option<Type> {
self.return_type_at(node, depth)
}
#[must_use]
pub fn type_named_by(&self, node: Node<'t>) -> Option<Type> {
self.named_type(node, 0)
}
#[must_use]
pub fn type_of(&self, node: Node<'t>) -> Option<Type> {
self.type_of_at(node, 0)
}
#[must_use]
pub fn symbol_of(&self, node: Node<'t>) -> Option<Symbol> {
self.symbol_at(node)
}
#[must_use]
pub fn return_type_of(&self, node: Node<'t>) -> Option<Type> {
self.return_type_at(node, 0)
}
fn return_type_at(&self, node: Node<'t>, depth: u32) -> Option<Type> {
if depth >= MAX_DEPTH {
return self.exhaust();
}
let next = depth.saturating_add(1);
match node.kind() {
"call_expression" => self.return_type_at(node.child_by_field_name("function")?, next),
"identifier" => {
if let Some(Binding::Import { module, name }) =
self.resolver.resolve(self.tree, self.source, node)
&& let (Some(file), Some(imports)) = (self.file, self.imports)
{
return imports.imported_return_type(file, &module, &name, next);
}
let declaration = self.resolver.declaration_of(self.tree, self.source, node)?;
self.return_type_at(declaration, next)
}
"variable_declarator" => self.return_type_at(node.child_by_field_name("value")?, next),
"function_declaration"
| "generator_function_declaration"
| "function_signature"
| "function_expression"
| "generator_function"
| "arrow_function"
| "method_definition"
| "method_signature"
| "abstract_method_signature" => self.signature_return(node, next),
_ => None,
}
}
fn signature_return(&self, node: Node<'t>, depth: u32) -> Option<Type> {
if let Some(annotation) = node.child_by_field_name("return_type") {
return self.annotation_type(annotation_child(annotation)?, depth);
}
if wraps_its_return(node) {
return None;
}
let body = node.child_by_field_name("body")?;
if body.kind() != "statement_block" {
return self.type_of_at(body, depth);
}
let mut returns = Vec::new();
collect_returns(body, &mut returns);
if returns.is_empty() {
return None;
}
let members: Vec<Type> = returns
.into_iter()
.map(|returned| match returned {
None => Some(Type::Primitive(Primitive::Undefined)),
Some(expression) => self.type_of_at(expression, depth),
})
.collect::<Option<Vec<Type>>>()?;
Type::union(members)
}
fn type_of_at(&self, node: Node<'t>, depth: u32) -> Option<Type> {
if depth >= MAX_DEPTH {
return self.exhaust();
}
match node.kind() {
"string" | "template_string" => Some(Type::Primitive(Primitive::String)),
"true" | "false" => Some(Type::Primitive(Primitive::Boolean)),
"null" => Some(Type::Primitive(Primitive::Null)),
"undefined" => Some(Type::Primitive(Primitive::Undefined)),
"number" => Some(Type::Primitive(if self.text(node).ends_with('n') {
Primitive::BigInt
} else {
Primitive::Number
})),
"parenthesized_expression" => {
self.type_of_at(node.named_child(0)?, depth.saturating_add(1))
}
"binary_expression" => {
let next = depth.saturating_add(1);
let operator = self.operator_of(node)?;
let left_node = node.child_by_field_name("left")?;
let right = self.primitive_of(node.child_by_field_name("right")?, next);
let left = if operator == "??" {
self.non_nullish_primitive_of(left_node, next)
} else {
self.primitive_of(left_node, next)
};
table::binary(operator, left, right).map(Type::Primitive)
}
"unary_expression" => table::unary(self.operator_of(node)?).map(Type::Primitive),
"call_expression" => {
let callee = node.child_by_field_name("function")?;
if callee.kind() != "identifier" {
return None;
}
if self
.resolver
.resolve(self.tree, self.source, callee)
.is_some()
{
return None;
}
table::builtin_call(self.text(callee)).map(Type::Primitive)
}
"type_annotation" => {
self.annotation_type(node.named_child(0)?, depth.saturating_add(1))
}
"predefined_type" | "union_type" | "literal_type" | "type_identifier" => {
self.annotation_type(node, depth)
}
"identifier" => {
if let Some(Binding::Import { module, name }) =
self.resolver.resolve(self.tree, self.source, node)
&& let (Some(file), Some(imports)) = (self.file, self.imports)
{
return imports.imported_value_type(
file,
&module,
&name,
depth.saturating_add(1),
);
}
let declaration = self.resolver.declaration_of(self.tree, self.source, node)?;
self.declaration_type(declaration, depth.saturating_add(1))
}
"member_expression" | "subscript_expression" => self.member_access(node, depth),
_ => None,
}
}
fn member_access(&self, node: Node<'t>, depth: u32) -> Option<Type> {
let (member_inner, nullish) = self.member_site(node, depth)?;
with_optional(
self.annotation_type(member_inner, depth.saturating_add(1))?,
nullish,
)
}
fn member_site(&self, node: Node<'t>, depth: u32) -> Option<(Node<'t>, bool)> {
if depth >= MAX_DEPTH {
return self.exhaust();
}
let object = node.child_by_field_name("object")?;
let (receiver, path_nullish) = self.receiver_type_node(object, depth.saturating_add(1))?;
let receiver_nullish = type_contains_nullish(receiver);
let container = self.resolve_to_container(receiver, depth.saturating_add(1))?;
let member = member_name(self.source, node)?;
let (annotation, member_optional) = member_annotation(self.source, container, &member)?;
Some((
annotation_child(annotation)?,
path_nullish || receiver_nullish || optional_access(node) || member_optional,
))
}
fn receiver_type_node(&self, expr: Node<'t>, depth: u32) -> Option<(Node<'t>, bool)> {
match expr.kind() {
"member_expression" | "subscript_expression" => self.member_site(expr, depth),
"parenthesized_expression" => {
self.receiver_type_node(expr.named_child(0)?, depth.saturating_add(1))
}
_ => self.annotated_type_node(expr),
}
}
#[must_use]
pub fn annotated_type_node(&self, expr: Node<'t>) -> Option<(Node<'t>, bool)> {
match expr.kind() {
"parenthesized_expression" => self.annotated_type_node(expr.named_child(0)?),
"identifier" => {
let declaration = self.resolver.declaration_of(self.tree, self.source, expr)?;
let nullish = declaration.kind() == "optional_parameter";
Some((binding_annotation(declaration)?, nullish))
}
_ => None,
}
}
fn resolve_to_container(&self, type_node: Node<'t>, depth: u32) -> Option<Node<'t>> {
if depth >= MAX_DEPTH {
return self.exhaust();
}
match type_node.kind() {
"object_type" => Some(type_node),
"parenthesized_type" => {
self.resolve_to_container(type_node.named_child(0)?, depth.saturating_add(1))
}
"union_type" => {
self.resolve_to_container(sole_non_nullish_arm(type_node)?, depth.saturating_add(1))
}
"type_identifier" | "generic_type" => {
let name = type_name_node(type_node)?;
if matches!(
self.resolver.resolve(self.tree, self.source, name),
Some(Binding::Import { .. })
) {
return None;
}
let declaration = self.resolver.declaration_of(self.tree, self.source, name)?;
if declaration.has_error() {
return None;
}
match declaration.kind() {
"type_alias_declaration" => self.resolve_to_container(
declaration.child_by_field_name("value")?,
depth.saturating_add(1),
),
_ => declaration_body(declaration),
}
}
_ => None,
}
}
fn declaration_type(&self, declaration: Node<'t>, depth: u32) -> Option<Type> {
if depth >= MAX_DEPTH {
return self.exhaust();
}
let next = depth.saturating_add(1);
match declaration.kind() {
"required_parameter" | "optional_parameter" => {
if !binds_one_name(declaration, "pattern") {
return None;
}
let annotation = declaration.child_by_field_name("type")?;
self.annotation_type(annotation_child(annotation)?, next)
}
"variable_declarator" => {
if !binds_one_name(declaration, "name") {
return None;
}
if let Some(annotation) = declaration.child_by_field_name("type") {
return self.annotation_type(annotation_child(annotation)?, next);
}
self.type_of_at(declaration.child_by_field_name("value")?, next)
}
_ => None,
}
}
fn primitive_of(&self, node: Node<'t>, depth: u32) -> Option<Primitive> {
match self.type_of_at(node, depth)? {
Type::Primitive(primitive) => Some(primitive),
Type::Nominal { .. } | Type::Union(_) => None,
}
}
fn non_nullish_primitive_of(&self, node: Node<'t>, depth: u32) -> Option<Primitive> {
fn is_nullish(primitive: Primitive) -> bool {
matches!(primitive, Primitive::Null | Primitive::Undefined)
}
match self.type_of_at(node, depth)? {
Type::Primitive(primitive) if !is_nullish(primitive) => Some(primitive),
Type::Union(members) => {
let mut sole = None;
for member in members {
match member {
Type::Primitive(primitive) if is_nullish(primitive) => {}
Type::Primitive(primitive) => {
if sole.is_some() {
return None;
}
sole = Some(primitive);
}
_ => return None,
}
}
sole
}
_ => None,
}
}
fn annotation_type(&self, node: Node<'t>, depth: u32) -> Option<Type> {
if depth >= MAX_DEPTH {
return self.exhaust();
}
match node.kind() {
"predefined_type" => match self.text(node) {
"number" => Some(Type::Primitive(Primitive::Number)),
"string" => Some(Type::Primitive(Primitive::String)),
"boolean" => Some(Type::Primitive(Primitive::Boolean)),
"symbol" => Some(Type::Primitive(Primitive::Symbol)),
_ => None,
},
"union_type" => {
let next = depth.saturating_add(1);
let mut cursor = node.walk();
let members: Vec<Type> = node
.children(&mut cursor)
.filter(|child| child.is_named() && child.kind() != "comment")
.map(|member| self.annotation_type(member, next))
.collect::<Option<Vec<Type>>>()?;
Type::union(members)
}
"literal_type" => self.type_of_at(node.named_child(0)?, depth.saturating_add(1)),
"type_identifier" => {
if self.text(node) == "bigint"
&& self
.resolver
.resolve(self.tree, self.source, node)
.is_none()
{
return Some(Type::Primitive(Primitive::BigInt));
}
self.named_type(node, depth)
}
_ => None,
}
}
fn named_type(&self, node: Node<'t>, depth: u32) -> Option<Type> {
let name = self.text(node);
if name.is_empty() {
return None;
}
if let Some(declaration) = self.resolver.declaration_of(self.tree, self.source, node) {
if declaration.kind() == "type_parameter" {
return None;
}
if declaration.kind() == "type_alias_declaration"
&& let Some(value) = declaration.child_by_field_name("value")
{
return self.annotation_type(value, depth.saturating_add(1));
}
}
if let Some(Binding::Import {
module,
name: imported,
}) = self.resolver.resolve(self.tree, self.source, node)
&& let (Some(file), Some(imports)) = (self.file, self.imports)
{
match imports.imported_alias_type(file, &module, &imported, depth.saturating_add(1)) {
Followed::Type(aliased) => return Some(aliased),
Followed::Exhausted => return self.exhaust(),
Followed::NotAnAlias => {}
}
}
Some(Type::Nominal {
name: name.to_owned(),
symbol: self.symbol_at(node),
})
}
fn symbol_at(&self, node: Node<'t>) -> Option<Symbol> {
let name = self.text(node);
if name.is_empty() {
return None;
}
let (module, exported) = match self.resolver.resolve(self.tree, self.source, node)? {
Binding::Import {
module,
name: imported,
} => {
let declared = self
.file
.zip(self.imports)
.and_then(|(file, imports)| imports.imported_export(file, &module, &imported))
.map(|target| target.name);
let exported = declared.or(match &imported {
ImportedName::Named(exported) => Some(exported.clone()),
ImportedName::Default => Some("default".to_owned()),
ImportedName::Namespace => None,
});
(Some(module), exported)
}
Binding::Local(_) => (None, None),
};
Some(Symbol {
name: name.to_owned(),
module,
exported,
})
}
fn operator_of(&self, node: Node<'t>) -> Option<&'t str> {
node.child_by_field_name("operator")
.map(|child| self.text(child))
}
fn text(&self, node: Node<'t>) -> &'t str {
self.source.get(node.byte_range()).unwrap_or("")
}
}
fn binds_one_name(declaration: Node<'_>, field: &str) -> bool {
declaration
.child_by_field_name(field)
.is_some_and(|bound| bound.kind() == "identifier")
}
fn wraps_its_return(node: Node<'_>) -> bool {
let mut cursor = node.walk();
node.children(&mut cursor)
.any(|child| !child.is_named() && matches!(child.kind(), "async" | "*"))
}
fn collect_returns<'t>(node: Node<'t>, out: &mut Vec<Option<Node<'t>>>) {
let mut stack = vec![node];
while let Some(current) = stack.pop() {
if current.kind() == "return_statement" {
out.push(current.named_child(0));
continue;
}
if current.id() != node.id() && is_function_like(current) {
continue;
}
let mut cursor = current.walk();
let children: Vec<Node<'t>> = current.children(&mut cursor).collect();
stack.extend(children.into_iter().rev());
}
}
fn is_function_like(node: Node<'_>) -> bool {
matches!(
node.kind(),
"function_declaration"
| "generator_function_declaration"
| "function_signature"
| "function_expression"
| "generator_function"
| "arrow_function"
| "method_definition"
| "method_signature"
| "abstract_method_signature"
)
}
pub(crate) fn annotation_child(node: Node<'_>) -> Option<Node<'_>> {
if node.kind() == "type_annotation" {
node.named_child(0)
} else {
Some(node)
}
}
pub(crate) fn member_name(source: &str, node: Node<'_>) -> Option<String> {
match node.kind() {
"member_expression" => {
let property = node.child_by_field_name("property")?;
(property.kind() == "property_identifier")
.then(|| source.get(property.byte_range()).map(str::to_owned))
.flatten()
}
"subscript_expression" => {
let index = node.child_by_field_name("index")?;
if index.kind() != "string" {
return None;
}
string_literal_value(source.get(index.byte_range())?)
}
_ => None,
}
}
fn string_literal_value(text: &str) -> Option<String> {
let inner = text
.strip_prefix('"')
.and_then(|rest| rest.strip_suffix('"'))
.or_else(|| {
text.strip_prefix('\'')
.and_then(|rest| rest.strip_suffix('\''))
})?;
(!inner.contains('\\')).then(|| inner.to_owned())
}
pub(crate) fn optional_access(node: Node<'_>) -> bool {
node.child_by_field_name("optional_chain").is_some()
}
pub(crate) fn binding_annotation(declaration: Node<'_>) -> Option<Node<'_>> {
let field = match declaration.kind() {
"required_parameter" | "optional_parameter" => "pattern",
"variable_declarator" => "name",
_ => return None,
};
if !binds_one_name(declaration, field) {
return None;
}
annotation_child(declaration.child_by_field_name("type")?)
}
fn is_nullish_type(type_node: Node<'_>) -> bool {
match type_node.kind() {
"null" | "undefined" => true,
"literal_type" => type_node
.named_child(0)
.is_some_and(|inner| matches!(inner.kind(), "null" | "undefined")),
_ => false,
}
}
pub(crate) fn type_contains_nullish(type_node: Node<'_>) -> bool {
if is_nullish_type(type_node) {
return true;
}
if type_node.kind() == "union_type" {
let mut cursor = type_node.walk();
return type_node.named_children(&mut cursor).any(is_nullish_type);
}
false
}
pub(crate) fn sole_non_nullish_arm(union_type: Node<'_>) -> Option<Node<'_>> {
let mut cursor = union_type.walk();
let mut arm = None;
for child in union_type.named_children(&mut cursor) {
if is_nullish_type(child) || child.kind() == "comment" {
continue;
}
if arm.is_some() {
return None;
}
arm = Some(child);
}
arm
}
pub(crate) fn type_name_node(type_node: Node<'_>) -> Option<Node<'_>> {
match type_node.kind() {
"type_identifier" => Some(type_node),
"generic_type" => type_node.child_by_field_name("name"),
_ => None,
}
}
pub(crate) fn declaration_body(declaration: Node<'_>) -> Option<Node<'_>> {
matches!(
declaration.kind(),
"interface_declaration" | "class_declaration" | "abstract_class_declaration"
)
.then(|| declaration.child_by_field_name("body"))
.flatten()
}
pub(crate) fn member_annotation<'t>(
source: &str,
container: Node<'t>,
member: &str,
) -> Option<(Node<'t>, bool)> {
let mut cursor = container.walk();
for child in container.named_children(&mut cursor) {
if !matches!(
child.kind(),
"property_signature" | "public_field_definition"
) {
continue;
}
let Some(name) = child.child_by_field_name("name") else {
continue;
};
if name.kind() != "property_identifier" || source.get(name.byte_range()) != Some(member) {
continue;
}
let annotation = child.child_by_field_name("type")?;
return Some((annotation, has_optional_token(child)));
}
None
}
fn has_optional_token(node: Node<'_>) -> bool {
let mut cursor = node.walk();
node.children(&mut cursor).any(|child| child.kind() == "?")
}
pub(crate) fn with_optional(ty: Type, optional: bool) -> Option<Type> {
if optional {
Type::union(vec![ty, Type::Primitive(Primitive::Undefined)])
} else {
Some(ty)
}
}