use std::borrow::Cow;
use oxc::ast::ast::{Declaration, Expression, Program, Statement, VariableDeclarationKind};
use crate::ts_syn::abi::SpanIR;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuildtimeKind {
Tier1Const,
Tier2Function,
Tier3Type,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Visibility {
Private,
Export,
}
#[derive(Debug, Clone)]
pub struct BuildtimeDecl {
pub kind: BuildtimeKind,
pub visibility: Visibility,
pub name: String,
pub decl_span: SpanIR,
pub body_source: String,
}
pub fn discover(program: &Program<'_>, source: &str) -> Vec<BuildtimeDecl> {
let mut out = Vec::new();
for stmt in &program.body {
if let Some(decl) = try_extract_decl(stmt, source) {
out.push(decl);
}
}
out
}
fn try_extract_decl(stmt: &Statement<'_>, source: &str) -> Option<BuildtimeDecl> {
let (visibility, inner_kind) = match stmt {
Statement::VariableDeclaration(var_decl) => {
(Visibility::Private, StatementKind::Var(var_decl))
}
Statement::FunctionDeclaration(func) => (Visibility::Private, StatementKind::Func(func)),
Statement::TSTypeAliasDeclaration(ty) => {
(Visibility::Private, StatementKind::TypeAlias(ty))
}
Statement::ExportNamedDeclaration(export) => match &export.declaration {
Some(Declaration::VariableDeclaration(var_decl)) => {
(Visibility::Export, StatementKind::Var(var_decl))
}
Some(Declaration::FunctionDeclaration(func)) => {
(Visibility::Export, StatementKind::Func(func))
}
Some(Declaration::TSTypeAliasDeclaration(ty)) => {
(Visibility::Export, StatementKind::TypeAlias(ty))
}
_ => return None,
},
_ => return None,
};
let stmt_span = stmt_byte_span(stmt);
if !has_buildtime_annotation(source, stmt_span.start.saturating_sub(1)) {
return None;
}
let stmt_span = with_jsdoc_lead(source, stmt_span);
match inner_kind {
StatementKind::Var(var_decl) => {
if var_decl.kind != VariableDeclarationKind::Const {
return None;
}
if var_decl.declarations.len() != 1 {
return None;
}
let declarator = &var_decl.declarations[0];
let name = declarator.id.get_identifier_name()?.to_string();
let init = declarator.init.as_ref()?;
let runtime_expr = unwrap_ts_expression(init);
let expr_span = expression_span(runtime_expr);
let expr_text = &source[expr_span.0..expr_span.1];
let body_source = format!("return ({});", expr_text);
Some(BuildtimeDecl {
kind: BuildtimeKind::Tier1Const,
visibility,
name,
decl_span: stmt_span,
body_source,
})
}
StatementKind::Func(func) => {
let name = func.id.as_ref()?.name.to_string();
let body = func.body.as_ref()?;
let (body_start, body_end) = (body.span.start as usize, body.span.end as usize);
if body_end <= body_start + 1 {
return None;
}
let inner = &source[body_start + 1..body_end - 1];
Some(BuildtimeDecl {
kind: BuildtimeKind::Tier2Function,
visibility,
name,
decl_span: stmt_span,
body_source: inner.to_string(),
})
}
StatementKind::TypeAlias(ty) => {
use oxc::span::GetSpan;
let name = ty.id.name.to_string();
let ann_span = ty.type_annotation.span();
let (ann_start, ann_end) = (ann_span.start as usize, ann_span.end as usize);
let inner = &source[ann_start..ann_end];
Some(BuildtimeDecl {
kind: BuildtimeKind::Tier3Type,
visibility,
name,
decl_span: stmt_span,
body_source: format!("return ({});", inner),
})
}
}
}
enum StatementKind<'a, 'b> {
Var(&'b oxc::ast::ast::VariableDeclaration<'a>),
Func(&'b oxc::ast::ast::Function<'a>),
TypeAlias(&'b oxc::ast::ast::TSTypeAliasDeclaration<'a>),
}
fn stmt_byte_span(stmt: &Statement<'_>) -> SpanIR {
let span = match stmt {
Statement::VariableDeclaration(d) => d.span,
Statement::FunctionDeclaration(d) => d.span,
Statement::TSTypeAliasDeclaration(d) => d.span,
Statement::ExportNamedDeclaration(d) => d.span,
_ => oxc::span::Span::default(),
};
SpanIR {
start: span.start + 1,
end: span.end + 1,
}
}
fn expression_span(expr: &Expression<'_>) -> (usize, usize) {
use oxc::span::GetSpan;
let span = expr.span();
(span.start as usize, span.end as usize)
}
fn with_jsdoc_lead(source: &str, span: SpanIR) -> SpanIR {
let start_0 = (span.start as usize).saturating_sub(1);
if start_0 == 0 || start_0 > source.len() {
return span;
}
let search_area = &source[..start_0];
let Some(comment_start) = search_area.rfind("/**") else {
return span;
};
let rest = &search_area[comment_start..];
let Some(end_rel) = rest.find("*/") else {
return span;
};
let comment_close_abs = comment_start + end_rel + 2;
if !source[comment_close_abs..start_0]
.chars()
.all(char::is_whitespace)
{
return span;
}
let mut new_start = comment_start;
let bytes = source.as_bytes();
while new_start > 0 {
let b = bytes[new_start - 1];
if b == b' ' || b == b'\t' {
new_start -= 1;
} else {
break;
}
}
SpanIR {
start: (new_start + 1) as u32,
end: span.end,
}
}
fn unwrap_ts_expression<'a>(expr: &'a Expression<'a>) -> &'a Expression<'a> {
match expr {
Expression::TSAsExpression(e) => unwrap_ts_expression(&e.expression),
Expression::TSSatisfiesExpression(e) => unwrap_ts_expression(&e.expression),
Expression::TSNonNullExpression(e) => unwrap_ts_expression(&e.expression),
Expression::TSTypeAssertion(e) => unwrap_ts_expression(&e.expression),
Expression::TSInstantiationExpression(e) => unwrap_ts_expression(&e.expression),
other => other,
}
}
fn has_buildtime_annotation(source: &str, decl_start: u32) -> bool {
let start = decl_start.saturating_sub(1) as usize;
if start == 0 || start > source.len() {
return false;
}
let search_area = &source[..start];
let Some(comment_start) = search_area.rfind("/**") else {
return false;
};
let rest = &search_area[comment_start..];
let Some(end_rel) = rest.find("*/") else {
return false;
};
let comment_close_abs = comment_start + end_rel + 2;
let between = &source[comment_close_abs..decl_start as usize];
if !between.chars().all(char::is_whitespace) {
return false;
}
let comment_body = &rest[3..end_rel];
let body_text = normalize_jsdoc(comment_body);
body_text.contains("@buildtime")
}
fn normalize_jsdoc(body: &str) -> Cow<'_, str> {
if !body.contains('*') {
return Cow::Borrowed(body);
}
let mut out = String::with_capacity(body.len());
for line in body.lines() {
let cleaned = line.trim().trim_start_matches('*').trim();
out.push_str(cleaned);
out.push('\n');
}
Cow::Owned(out)
}