use once_cell::sync::Lazy;
use rustc_hash::FxHashMap;
use std::sync::Mutex;
use wasm_bindgen::prelude::{JsValue, wasm_bindgen};
type LibFileCache = FxHashMap<(String, u64), Arc<lib_loader::LibFile>>;
static LIB_FILE_CACHE: Lazy<Mutex<LibFileCache>> = Lazy::new(|| Mutex::new(FxHashMap::default()));
fn hash_lib_content(content: &str) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
content.hash(&mut hasher);
hasher.finish()
}
fn get_or_create_lib_file(file_name: String, source_text: String) -> Arc<lib_loader::LibFile> {
let content_hash = hash_lib_content(&source_text);
let cache_key = (file_name.clone(), content_hash);
{
let cache = LIB_FILE_CACHE.lock().unwrap();
if let Some(cached) = cache.get(&cache_key) {
return Arc::clone(cached);
}
}
let mut lib_parser = ParserState::new(file_name.clone(), source_text);
let source_file_idx = lib_parser.parse_source_file();
let mut lib_binder = BinderState::new();
lib_binder.bind_source_file(lib_parser.get_arena(), source_file_idx);
let arena = Arc::new(lib_parser.into_arena());
let binder = Arc::new(lib_binder);
let lib_file = Arc::new(lib_loader::LibFile::new(file_name, arena, binder));
{
let mut cache = LIB_FILE_CACHE.lock().unwrap();
cache.insert(cache_key, Arc::clone(&lib_file));
}
lib_file
}
#[cfg(test)]
#[path = "../tests/test_fixtures.rs"]
pub mod test_fixtures;
pub use tsz_common::interner;
pub use tsz_common::interner::{Atom, Interner, ShardedInterner};
#[cfg(test)]
#[path = "../tests/interner_tests.rs"]
mod interner_tests;
pub use tsz_common::common;
pub use tsz_common::common::{ModuleKind, NewLineKind, ScriptTarget};
pub use tsz_common::limits;
pub use tsz_scanner as scanner;
pub use tsz_scanner::char_codes;
pub use tsz_scanner::scanner_impl;
pub use tsz_scanner::scanner_impl::{ScannerState, TokenFlags};
pub use tsz_scanner::*;
#[cfg(test)]
#[path = "../tests/scanner_impl_tests.rs"]
mod scanner_impl_tests;
#[cfg(test)]
#[path = "../tests/scanner_tests.rs"]
mod scanner_tests;
pub use tsz_parser::parser;
pub use tsz_parser::syntax;
#[cfg(test)]
#[path = "../tests/parser_state_tests.rs"]
mod parser_state_tests;
#[cfg(test)]
#[path = "../tests/parser_ts1038_tests.rs"]
mod parser_ts1038_tests;
#[cfg(test)]
#[path = "../tests/control_flow_validation_tests.rs"]
mod control_flow_validation_tests;
#[cfg(test)]
#[path = "../tests/regex_flag_tests.rs"]
mod regex_flag_tests;
pub use tsz_binder as binder;
#[cfg(test)]
#[path = "../tests/binder_state_tests.rs"]
mod binder_state_tests;
pub use tsz_binder::module_resolution_debug;
pub use tsz_binder::lib_loader;
pub use tsz_checker as checker;
#[cfg(test)]
#[path = "../tests/checker_state_tests.rs"]
mod checker_state_tests;
#[cfg(test)]
#[path = "../tests/variable_redeclaration_tests.rs"]
mod variable_redeclaration_tests;
#[cfg(test)]
#[path = "../tests/strict_mode_and_module_tests.rs"]
mod strict_mode_and_module_tests;
#[cfg(test)]
#[path = "../tests/overload_compatibility_tests.rs"]
mod overload_compatibility_tests;
#[cfg(test)]
#[path = "../tests/module_resolution_tests.rs"]
mod module_resolution_tests;
pub use checker::state::{CheckerState, MAX_CALL_DEPTH, MAX_INSTANTIATION_DEPTH};
pub use tsz_emitter::emitter;
#[cfg(test)]
#[path = "../tests/transform_api_tests.rs"]
mod transform_api_tests;
pub use tsz_emitter::output::printer;
pub use tsz_emitter::safe_slice;
#[cfg(test)]
#[path = "../tests/printer_tests.rs"]
mod printer_tests;
pub use tsz_common::span;
pub mod source_file;
pub mod diagnostics;
pub use tsz_emitter::enums;
pub mod parallel;
pub use tsz_common::comments;
#[cfg(test)]
#[path = "../tests/comments_tests.rs"]
mod comments_tests;
pub use tsz_common::source_map;
#[cfg(test)]
#[path = "../tests/source_map_test_utils.rs"]
mod source_map_test_utils;
#[cfg(test)]
#[path = "../tests/source_map_tests_1.rs"]
mod source_map_tests_1;
#[cfg(test)]
#[path = "../tests/source_map_tests_2.rs"]
mod source_map_tests_2;
#[cfg(test)]
#[path = "../tests/source_map_tests_3.rs"]
mod source_map_tests_3;
#[cfg(test)]
#[path = "../tests/source_map_tests_4.rs"]
mod source_map_tests_4;
pub use tsz_emitter::output::source_writer;
#[cfg(test)]
#[path = "../tests/source_writer_tests.rs"]
mod source_writer_tests;
pub use tsz_emitter::context;
pub use tsz_emitter::lowering;
pub use tsz_emitter::declaration_emitter;
pub use tsz_emitter::transforms;
pub use tsz_solver;
pub use tsz_lsp as lsp;
#[cfg(test)]
#[path = "../tests/test_harness.rs"]
mod test_harness;
#[cfg(test)]
#[path = "../tests/isolated_test_runner.rs"]
mod isolated_test_runner;
pub mod config;
#[cfg(not(target_arch = "wasm32"))]
pub mod module_resolver;
#[cfg(not(target_arch = "wasm32"))]
mod module_resolver_helpers;
#[cfg(not(target_arch = "wasm32"))]
pub use module_resolver::{ModuleExtension, ModuleResolver, ResolutionFailure, ResolvedModule};
pub mod imports;
pub use imports::{ImportDeclaration, ImportKind, ImportTracker, ImportedBinding};
pub mod exports;
pub use exports::{ExportDeclaration, ExportKind, ExportTracker, ExportedBinding};
#[cfg(not(target_arch = "wasm32"))]
pub mod module_graph;
#[cfg(not(target_arch = "wasm32"))]
pub use module_graph::{CircularDependency, ModuleGraph, ModuleId, ModuleInfo};
#[wasm_bindgen(js_name = createScanner)]
pub fn create_scanner(text: String, skip_trivia: bool) -> ScannerState {
ScannerState::new(text, skip_trivia)
}
use crate::binder::BinderState;
use crate::checker::context::LibContext;
use crate::context::emit::EmitContext;
use crate::context::transform::TransformContext;
use crate::emitter::{Printer, PrinterOptions};
use crate::lib_loader::LibFile;
use crate::lowering::LoweringPass;
use crate::lsp::diagnostics::convert_diagnostic;
use crate::lsp::position::{LineMap, Position, Range};
use crate::lsp::resolver::ScopeCache;
use crate::lsp::{
CodeActionContext, CodeActionKind, CodeActionProvider, Completions, DocumentSymbolProvider,
FindReferences, GoToDefinition, HoverProvider, ImportCandidate, ImportCandidateKind,
RenameProvider, SemanticTokensProvider, SignatureHelpProvider,
};
use crate::parser::ParserState;
use serde::Deserialize;
use std::sync::Arc;
use tsz_solver::TypeInterner;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ImportCandidateInput {
module_specifier: String,
local_name: String,
kind: String,
export_name: Option<String>,
#[serde(default)]
is_type_only: bool,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct CodeActionContextInput {
#[serde(default)]
diagnostics: Vec<tsz_lsp::diagnostics::LspDiagnostic>,
#[serde(default)]
only: Option<Vec<CodeActionKind>>,
#[serde(default)]
import_candidates: Vec<ImportCandidateInput>,
}
#[derive(Deserialize, Clone, Debug, Default)]
#[serde(rename_all = "camelCase")]
struct CompilerOptions {
#[serde(default, deserialize_with = "deserialize_bool_option")]
strict: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_option")]
no_implicit_any: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_option")]
strict_null_checks: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_option")]
strict_function_types: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_option")]
strict_property_initialization: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_option")]
no_implicit_returns: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_option")]
no_implicit_this: Option<bool>,
#[serde(default, deserialize_with = "deserialize_target_or_module")]
target: Option<u32>,
#[serde(default, deserialize_with = "deserialize_bool_option")]
no_lib: Option<bool>,
}
fn deserialize_bool_option<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{self, Visitor};
struct BoolOptionVisitor;
impl<'de> Visitor<'de> for BoolOptionVisitor {
type Value = Option<bool>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a boolean, string, or comma-separated list of booleans")
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(None)
}
fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Some(value))
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
let first_value = value.split(',').next().unwrap_or(value).trim();
let result = match first_value.to_lowercase().as_str() {
"true" | "1" => Some(true),
"false" | "0" => Some(false),
_ => None,
};
Ok(result)
}
}
deserializer.deserialize_any(BoolOptionVisitor)
}
fn deserialize_target_or_module<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{self, Visitor};
struct TargetOrModuleVisitor;
impl<'de> Visitor<'de> for TargetOrModuleVisitor {
type Value = Option<u32>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string or integer representing target/module")
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(None)
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Some(value as u32))
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Some(value as u32))
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
let result = match value.to_uppercase().as_str() {
"ES3" | "NONE" => 0,
"ES5" | "COMMONJS" => 1,
"ES2015" | "ES6" | "AMD" => 2,
"ES2016" | "UMD" => 3,
"ES2017" | "SYSTEM" => 4,
"ES2018" => 5,
"ES2019" => 6,
"ES2020" => 7,
"ES2021" => 8,
"ES2022" => 9,
"ES2023" => 10,
"ESNEXT" => 99,
"NODE16" => 100,
"NODENEXT" => 199,
_ => return Ok(None), };
Ok(Some(result))
}
}
deserializer.deserialize_any(TargetOrModuleVisitor)
}
impl CompilerOptions {
fn resolve_bool(&self, specific: Option<bool>, strict_implies: bool) -> bool {
if let Some(value) = specific {
return value;
}
if strict_implies {
return self.strict.unwrap_or(false);
}
false
}
pub fn get_no_implicit_any(&self) -> bool {
self.no_implicit_any.unwrap_or(self.strict.unwrap_or(true))
}
pub fn get_strict_null_checks(&self) -> bool {
self.resolve_bool(self.strict_null_checks, true)
}
pub fn get_strict_function_types(&self) -> bool {
self.resolve_bool(self.strict_function_types, true)
}
pub fn get_strict_property_initialization(&self) -> bool {
self.resolve_bool(self.strict_property_initialization, true)
}
pub fn get_no_implicit_returns(&self) -> bool {
self.resolve_bool(self.no_implicit_returns, false)
}
pub fn get_no_implicit_this(&self) -> bool {
self.resolve_bool(self.no_implicit_this, true)
}
fn resolve_target(&self) -> crate::checker::context::ScriptTarget {
use crate::checker::context::ScriptTarget;
match self.target {
Some(0) => ScriptTarget::ES3,
Some(1) => ScriptTarget::ES5,
Some(2) => ScriptTarget::ES2015,
Some(3) => ScriptTarget::ES2016,
Some(4) => ScriptTarget::ES2017,
Some(5) => ScriptTarget::ES2018,
Some(6) => ScriptTarget::ES2019,
Some(7) => ScriptTarget::ES2020,
Some(_) => ScriptTarget::ESNext,
None => ScriptTarget::default(),
}
}
pub fn to_checker_options(&self) -> crate::checker::context::CheckerOptions {
let strict = self.strict.unwrap_or(false);
let strict_null_checks = self.get_strict_null_checks();
crate::checker::context::CheckerOptions {
strict,
no_implicit_any: self.get_no_implicit_any(),
no_implicit_returns: self.get_no_implicit_returns(),
strict_null_checks,
strict_function_types: self.get_strict_function_types(),
strict_property_initialization: self.get_strict_property_initialization(),
no_implicit_this: self.get_no_implicit_this(),
use_unknown_in_catch_variables: strict_null_checks,
isolated_modules: false,
no_unchecked_indexed_access: false,
strict_bind_call_apply: false,
exact_optional_property_types: false,
no_lib: self.no_lib.unwrap_or(false),
no_types_and_symbols: false,
target: self.resolve_target(),
module: crate::common::ModuleKind::None,
jsx_factory: "React.createElement".to_string(),
jsx_fragment_factory: "React.Fragment".to_string(),
es_module_interop: false,
allow_synthetic_default_imports: false,
allow_unreachable_code: None,
no_property_access_from_index_signature: false,
sound_mode: false,
experimental_decorators: false,
no_unused_locals: false,
no_unused_parameters: false,
always_strict: strict,
resolve_json_module: false, check_js: false, no_resolve: false,
no_unchecked_side_effect_imports: false,
no_implicit_override: false,
jsx_mode: tsz_common::checker_options::JsxMode::None,
module_explicitly_set: false,
suppress_excess_property_errors: false,
suppress_implicit_any_index_errors: false,
}
}
}
impl TryFrom<ImportCandidateInput> for ImportCandidate {
type Error = JsValue;
fn try_from(input: ImportCandidateInput) -> Result<Self, Self::Error> {
let local_name = input.local_name;
let kind = match input.kind.as_str() {
"named" => {
let export_name = input.export_name.unwrap_or_else(|| local_name.clone());
ImportCandidateKind::Named { export_name }
}
"default" => ImportCandidateKind::Default,
"namespace" => ImportCandidateKind::Namespace,
other => {
return Err(JsValue::from_str(&format!(
"Unsupported import candidate kind: {other}"
)));
}
};
Ok(Self {
module_specifier: input.module_specifier,
local_name,
kind,
is_type_only: input.is_type_only,
})
}
}
#[wasm_bindgen]
pub struct WasmTransformContext {
inner: TransformContext,
target_es5: bool,
module_kind: ModuleKind,
}
#[wasm_bindgen]
impl WasmTransformContext {
#[wasm_bindgen(js_name = getCount)]
pub fn get_count(&self) -> usize {
self.inner.len()
}
}
#[wasm_bindgen]
pub struct Parser {
parser: ParserState,
source_file_idx: Option<parser::NodeIndex>,
binder: Option<BinderState>,
type_interner: TypeInterner,
line_map: Option<LineMap>,
type_cache: Option<checker::TypeCache>,
scope_cache: ScopeCache,
lib_files: Vec<Arc<LibFile>>,
compiler_options: CompilerOptions,
}
#[wasm_bindgen]
impl Parser {
#[wasm_bindgen(constructor)]
pub fn new(file_name: String, source_text: String) -> Self {
Self {
parser: ParserState::new(file_name, source_text),
source_file_idx: None,
binder: None,
type_interner: TypeInterner::new(),
line_map: None,
type_cache: None,
scope_cache: ScopeCache::default(),
lib_files: Vec::new(),
compiler_options: CompilerOptions::default(),
}
}
#[wasm_bindgen(js_name = setCompilerOptions)]
pub fn set_compiler_options(&mut self, options_json: &str) -> Result<(), JsValue> {
match serde_json::from_str::<CompilerOptions>(options_json) {
Ok(options) => {
self.compiler_options = options;
self.type_cache = None;
Ok(())
}
Err(e) => Err(JsValue::from_str(&format!(
"Failed to parse compiler options: {e}"
))),
}
}
#[wasm_bindgen(js_name = addLibFile)]
pub fn add_lib_file(&mut self, file_name: String, source_text: String) {
let mut lib_parser = ParserState::new(file_name.clone(), source_text);
let source_file_idx = lib_parser.parse_source_file();
let mut lib_binder = BinderState::new();
lib_binder.bind_source_file(lib_parser.get_arena(), source_file_idx);
let arena = Arc::new(lib_parser.into_arena());
let binder = Arc::new(lib_binder);
let lib_file = Arc::new(LibFile::new(file_name, arena, binder));
self.lib_files.push(lib_file);
self.binder = None;
self.type_cache = None;
}
#[wasm_bindgen(js_name = parseSourceFile)]
pub fn parse_source_file(&mut self) -> u32 {
let idx = self.parser.parse_source_file();
self.source_file_idx = Some(idx);
self.line_map = None;
self.binder = None;
self.type_cache = None; self.scope_cache.clear();
idx.0
}
#[allow(clippy::missing_const_for_fn)] #[wasm_bindgen(js_name = getNodeCount)]
pub fn get_node_count(&self) -> usize {
self.parser.get_node_count()
}
#[wasm_bindgen(js_name = getDiagnosticsJson)]
pub fn get_diagnostics_json(&self) -> String {
let diags: Vec<_> = self
.parser
.get_diagnostics()
.iter()
.map(|d| {
serde_json::json!({
"message": d.message,
"start": d.start,
"length": d.length,
"code": d.code,
})
})
.collect();
serde_json::to_string(&diags).unwrap_or_else(|_| "[]".to_string())
}
#[wasm_bindgen(js_name = bindSourceFile)]
pub fn bind_source_file(&mut self) -> String {
if let Some(root_idx) = self.source_file_idx {
let mut binder = BinderState::new();
binder.bind_source_file_with_libs(self.parser.get_arena(), root_idx, &self.lib_files);
let symbols: FxHashMap<String, u32> = binder
.file_locals
.iter()
.map(|(name, id)| (name.clone(), id.0))
.collect();
let result = serde_json::json!({
"symbols": symbols,
"symbolCount": binder.symbols.len(),
});
self.binder = Some(binder);
self.scope_cache.clear();
serde_json::to_string(&result).unwrap_or_else(|_| "{}".to_string())
} else {
r#"{"error": "Source file not parsed"}"#.to_string()
}
}
#[wasm_bindgen(js_name = checkSourceFile)]
pub fn check_source_file(&mut self) -> String {
if self.binder.is_none() {
if self.source_file_idx.is_some() {
self.bind_source_file();
}
}
if let (Some(root_idx), Some(binder)) = (self.source_file_idx, &self.binder) {
let file_name = self.parser.get_file_name().to_string();
let checker_options = self.compiler_options.to_checker_options();
let mut checker = if let Some(cache) = self.type_cache.take() {
CheckerState::with_cache_and_options(
self.parser.get_arena(),
binder,
&self.type_interner,
file_name,
cache,
&checker_options,
)
} else {
CheckerState::with_options(
self.parser.get_arena(),
binder,
&self.type_interner,
file_name,
&checker_options,
)
};
if !self.lib_files.is_empty() {
let lib_contexts: Vec<LibContext> = self
.lib_files
.iter()
.map(|lib| LibContext {
arena: Arc::clone(&lib.arena),
binder: Arc::clone(&lib.binder),
})
.collect();
checker.ctx.set_lib_contexts(lib_contexts);
}
checker.check_source_file(root_idx);
let diagnostics = checker
.ctx
.diagnostics
.iter()
.map(|d| {
serde_json::json!({
"message_text": d.message_text.clone(),
"code": d.code,
"start": d.start,
"length": d.length,
"category": format!("{:?}", d.category),
})
})
.collect::<Vec<_>>();
self.type_cache = Some(checker.extract_cache());
let result = serde_json::json!({
"typeCount": self.type_interner.len(),
"diagnostics": diagnostics,
});
serde_json::to_string(&result).unwrap_or_else(|_| "{}".to_string())
} else {
r#"{"error": "Source file not parsed or bound"}"#.to_string()
}
}
#[wasm_bindgen(js_name = getTypeOfNode)]
pub fn get_type_of_node(&mut self, node_idx: u32) -> String {
if let (Some(_), Some(binder)) = (self.source_file_idx, &self.binder) {
let file_name = self.parser.get_file_name().to_string();
let checker_options = self.compiler_options.to_checker_options();
let mut checker = if let Some(cache) = self.type_cache.take() {
CheckerState::with_cache_and_options(
self.parser.get_arena(),
binder,
&self.type_interner,
file_name,
cache,
&checker_options,
)
} else {
CheckerState::with_options(
self.parser.get_arena(),
binder,
&self.type_interner,
file_name,
&checker_options,
)
};
let type_id = checker.get_type_of_node(parser::NodeIndex(node_idx));
let result = checker.format_type(type_id);
self.type_cache = Some(checker.extract_cache());
result
} else {
"unknown".to_string()
}
}
#[wasm_bindgen(js_name = emit)]
pub fn emit(&self) -> String {
if let Some(root_idx) = self.source_file_idx {
let options = PrinterOptions {
target: ScriptTarget::ES5,
..Default::default()
};
let mut ctx = EmitContext::with_options(options);
ctx.auto_detect_module = true;
self.emit_with_context(root_idx, ctx)
} else {
String::new()
}
}
#[wasm_bindgen(js_name = emitModern)]
pub fn emit_modern(&self) -> String {
if let Some(root_idx) = self.source_file_idx {
let options = PrinterOptions {
target: ScriptTarget::ES2015,
..Default::default()
};
let ctx = EmitContext::with_options(options);
self.emit_with_context(root_idx, ctx)
} else {
String::new()
}
}
fn emit_with_context(&self, root_idx: parser::NodeIndex, ctx: EmitContext) -> String {
let transforms = LoweringPass::new(self.parser.get_arena(), &ctx).run(root_idx);
let mut printer = Printer::with_transforms_and_options(
self.parser.get_arena(),
transforms,
ctx.options.clone(),
);
printer.set_target_es5(ctx.target_es5);
printer.set_auto_detect_module(ctx.auto_detect_module);
printer.set_source_text(self.parser.get_source_text());
printer.emit(root_idx);
printer.get_output().to_string()
}
#[wasm_bindgen(js_name = generateTransforms)]
pub fn generate_transforms(&self, target: u32, module: u32) -> WasmTransformContext {
let options = PrinterOptions {
target: match target {
0 => ScriptTarget::ES3,
1 => ScriptTarget::ES5,
2 => ScriptTarget::ES2015,
3 => ScriptTarget::ES2016,
4 => ScriptTarget::ES2017,
5 => ScriptTarget::ES2018,
6 => ScriptTarget::ES2019,
7 => ScriptTarget::ES2020,
8 => ScriptTarget::ES2021,
9 => ScriptTarget::ES2022,
_ => ScriptTarget::ESNext,
},
module: match module {
1 => ModuleKind::CommonJS,
2 => ModuleKind::AMD,
3 => ModuleKind::UMD,
4 => ModuleKind::System,
5 => ModuleKind::ES2015,
6 => ModuleKind::ES2020,
7 => ModuleKind::ES2022,
99 => ModuleKind::ESNext,
100 => ModuleKind::Node16,
199 => ModuleKind::NodeNext,
_ => ModuleKind::None,
},
..Default::default()
};
let ctx = EmitContext::with_options(options);
let transforms = if let Some(root_idx) = self.source_file_idx {
let lowering = LoweringPass::new(self.parser.get_arena(), &ctx);
lowering.run(root_idx)
} else {
TransformContext::new()
};
WasmTransformContext {
inner: transforms,
target_es5: ctx.target_es5,
module_kind: ctx.options.module,
}
}
#[wasm_bindgen(js_name = emitWithTransforms)]
pub fn emit_with_transforms(&self, context: &WasmTransformContext) -> String {
if let Some(root_idx) = self.source_file_idx {
let mut printer =
Printer::with_transforms(self.parser.get_arena(), context.inner.clone());
printer.set_target_es5(context.target_es5);
printer.set_module_kind(context.module_kind);
printer.set_source_text(self.parser.get_source_text());
printer.emit(root_idx);
printer.get_output().to_string()
} else {
String::new()
}
}
#[wasm_bindgen(js_name = getAstJson)]
pub fn get_ast_json(&self) -> String {
if let Some(root_idx) = self.source_file_idx {
let arena = self.parser.get_arena();
format!(
"{{\"nodeCount\": {}, \"rootIdx\": {}}}",
arena.len(),
root_idx.0
)
} else {
"{}".to_string()
}
}
#[wasm_bindgen(js_name = debugTypeLowering)]
pub fn debug_type_lowering(&self, interface_name: &str) -> String {
use parser::syntax_kind_ext;
use tsz_lowering::TypeLowering;
use tsz_solver::TypeData;
let arena = self.parser.get_arena();
let mut result = Vec::new();
let mut interface_decls = Vec::new();
for i in 0..arena.len() {
let idx = parser::NodeIndex(i as u32);
if let Some(node) = arena.get(idx)
&& node.kind == syntax_kind_ext::INTERFACE_DECLARATION
&& let Some(interface) = arena.get_interface(node)
&& let Some(name_node) = arena.get(interface.name)
&& let Some(ident) = arena.get_identifier(name_node)
&& ident.escaped_text == interface_name
{
interface_decls.push(idx);
}
}
if interface_decls.is_empty() {
return format!("Interface '{interface_name}' not found");
}
result.push(format!(
"Found {} declaration(s) for '{}'",
interface_decls.len(),
interface_name
));
let lowering = TypeLowering::new(arena, &self.type_interner);
let type_id = lowering.lower_interface_declarations(&interface_decls);
result.push(format!("Lowered type ID: {type_id:?}"));
if let Some(key) = self.type_interner.lookup(type_id) {
result.push(format!("Type key: {key:?}"));
if let TypeData::Object(shape_id) = key {
let shape = self.type_interner.object_shape(shape_id);
result.push(format!(
"Object shape properties: {}",
shape.properties.len()
));
for prop in &shape.properties {
let name = self.type_interner.resolve_atom(prop.name);
result.push(format!(
" Property '{}': type_id={:?}, optional={}",
name, prop.type_id, prop.optional
));
if let Some(prop_key) = self.type_interner.lookup(prop.type_id) {
result.push(format!(" -> {prop_key:?}"));
}
}
}
}
result.join("\n")
}
#[wasm_bindgen(js_name = debugInterfaceMembers)]
pub fn debug_interface_members(&self, interface_name: &str) -> String {
use parser::syntax_kind_ext;
let arena = self.parser.get_arena();
let mut result = Vec::new();
for i in 0..arena.len() {
let idx = parser::NodeIndex(i as u32);
if let Some(node) = arena.get(idx)
&& node.kind == syntax_kind_ext::INTERFACE_DECLARATION
&& let Some(interface) = arena.get_interface(node)
&& let Some(name_node) = arena.get(interface.name)
&& let Some(ident) = arena.get_identifier(name_node)
&& ident.escaped_text == interface_name
{
result.push(format!("Interface '{interface_name}' found at node {i}"));
result.push(format!(" members list: {:?}", interface.members.nodes));
for (mi, &member_idx) in interface.members.nodes.iter().enumerate() {
if let Some(member_node) = arena.get(member_idx) {
result.push(format!(
" Member {} (idx {}): kind={}",
mi, member_idx.0, member_node.kind
));
result.push(format!(" data_index: {}", member_node.data_index));
if let Some(sig) = arena.get_signature(member_node) {
result.push(format!(" name_idx: {:?}", sig.name));
result.push(format!(
" type_annotation_idx: {:?}",
sig.type_annotation
));
if let Some(name_n) = arena.get(sig.name) {
if let Some(name_id) = arena.get_identifier(name_n) {
result
.push(format!(" name_text: '{}'", name_id.escaped_text));
} else {
result.push(format!(" name_node kind: {}", name_n.kind));
}
}
if let Some(type_n) = arena.get(sig.type_annotation) {
if let Some(type_id) = arena.get_identifier(type_n) {
result
.push(format!(" type_text: '{}'", type_id.escaped_text));
} else {
result.push(format!(" type_node kind: {}", type_n.kind));
}
}
}
}
}
}
}
if result.is_empty() {
format!("Interface '{interface_name}' not found")
} else {
result.join("\n")
}
}
#[wasm_bindgen(js_name = debugScopes)]
pub fn debug_scopes(&self) -> String {
let Some(binder) = &self.binder else {
return "Binder not initialized. Call parseSourceFile and bindSourceFile first."
.to_string();
};
let mut result = Vec::new();
result.push(format!(
"=== Persistent Scopes ({}) ===",
binder.scopes.len()
));
for (i, scope) in binder.scopes.iter().enumerate() {
result.push(format!(
"\nScope {} (parent: {:?}, kind: {:?}):",
i, scope.parent, scope.kind
));
result.push(format!(" table entries: {}", scope.table.len()));
for (name, sym_id) in scope.table.iter() {
if let Some(sym) = binder.symbols.get(*sym_id) {
result.push(format!(
" '{}' -> SymbolId({}) [flags: 0x{:x}]",
name, sym_id.0, sym.flags
));
} else {
result.push(format!(
" '{}' -> SymbolId({}) [MISSING SYMBOL]",
name, sym_id.0
));
}
}
}
result.push(format!(
"\n=== Node -> Scope Mappings ({}) ===",
binder.node_scope_ids.len()
));
for (&node_idx, &scope_id) in &binder.node_scope_ids {
result.push(format!(
" NodeIndex({}) -> ScopeId({})",
node_idx, scope_id.0
));
}
result.push(format!(
"\n=== File Locals ({}) ===",
binder.file_locals.len()
));
for (name, sym_id) in binder.file_locals.iter() {
result.push(format!(" '{}' -> SymbolId({})", name, sym_id.0));
}
result.join("\n")
}
#[wasm_bindgen(js_name = traceParentChain)]
pub fn trace_parent_chain(&self, pos: u32) -> String {
const IDENTIFIER_KIND: u16 = 80; let arena = self.parser.get_arena();
let binder = match &self.binder {
Some(b) => b,
None => return "Binder not initialized".to_string(),
};
let mut result = Vec::new();
result.push(format!("=== Tracing parent chain for position {pos} ==="));
let mut target_node = None;
for i in 0..arena.len() {
let idx = parser::NodeIndex(i as u32);
if let Some(node) = arena.get(idx)
&& node.pos <= pos
&& pos < node.end
&& node.kind == IDENTIFIER_KIND
{
target_node = Some(idx);
}
}
let start_idx = match target_node {
Some(idx) => idx,
None => return format!("No identifier node found at position {pos}"),
};
result.push(format!("Starting node: {start_idx:?}"));
let mut current = start_idx;
let mut depth = 0;
while !current.is_none() && depth < 20 {
if let Some(node) = arena.get(current) {
let kind_name = format!("kind={}", node.kind);
let scope_info = if let Some(&scope_id) = binder.node_scope_ids.get(¤t.0) {
format!(" -> ScopeId({})", scope_id.0)
} else {
String::new()
};
result.push(format!(
" [{}] NodeIndex({}) {} [pos:{}-{}]{}",
depth, current.0, kind_name, node.pos, node.end, scope_info
));
}
if let Some(ext) = arena.get_extended(current) {
if ext.parent.is_none() {
result.push(format!(" [{}] Parent is NodeIndex::NONE", depth + 1));
break;
}
current = ext.parent;
} else {
result.push(format!(
" [{}] No extended info for NodeIndex({})",
depth + 1,
current.0
));
break;
}
depth += 1;
}
result.join("\n")
}
#[wasm_bindgen(js_name = dumpVarDecl)]
pub fn dump_var_decl(&self, var_decl_idx: u32) -> String {
let arena = self.parser.get_arena();
let idx = parser::NodeIndex(var_decl_idx);
let Some(node) = arena.get(idx) else {
return format!("NodeIndex({var_decl_idx}) not found");
};
let Some(var_decl) = arena.get_variable_declaration(node) else {
return format!(
"NodeIndex({}) is not a VARIABLE_DECLARATION (kind={})",
var_decl_idx, node.kind
);
};
format!(
"VariableDeclaration({}):\n name: NodeIndex({})\n type_annotation: NodeIndex({}) (is_none={})\n initializer: NodeIndex({})",
var_decl_idx,
var_decl.name.0,
var_decl.type_annotation.0,
var_decl.type_annotation.is_none(),
var_decl.initializer.0
)
}
#[wasm_bindgen(js_name = dumpAllNodes)]
pub fn dump_all_nodes(&self, start: u32, count: u32) -> String {
let arena = self.parser.get_arena();
let mut result = Vec::new();
for i in start..(start + count).min(arena.len() as u32) {
let idx = parser::NodeIndex(i);
if let Some(node) = arena.get(idx) {
let parent_str = if let Some(ext) = arena.get_extended(idx) {
if ext.parent.is_none() {
"parent:NONE".to_string()
} else {
format!("parent:{}", ext.parent.0)
}
} else {
"no-ext".to_string()
};
let extra = if let Some(ident) = arena.get_identifier(node) {
format!(" \"{}\"", ident.escaped_text)
} else {
String::new()
};
result.push(format!(
" NodeIndex({}) kind={} [pos:{}-{}] {}{}",
i, node.kind, node.pos, node.end, parent_str, extra
));
}
}
result.join("\n")
}
fn ensure_line_map(&mut self) {
if self.line_map.is_none() {
self.line_map = Some(LineMap::build(self.parser.get_source_text()));
}
}
fn ensure_bound(&mut self) -> Result<(), JsValue> {
if self.source_file_idx.is_none() {
return Err(JsValue::from_str("Source file not parsed"));
}
if self.binder.is_none() {
self.bind_source_file();
}
Ok(())
}
#[wasm_bindgen(js_name = getDefinitionAtPosition)]
pub fn get_definition_at_position(
&mut self,
line: u32,
character: u32,
) -> Result<JsValue, JsValue> {
self.ensure_bound()?;
self.ensure_line_map();
let root = self
.source_file_idx
.ok_or_else(|| JsValue::from_str("Source file not available"))?;
let binder = self
.binder
.as_ref()
.ok_or_else(|| JsValue::from_str("Binder not available"))?;
let line_map = self
.line_map
.as_ref()
.ok_or_else(|| JsValue::from_str("Line map not available"))?;
let file_name = self.parser.get_file_name().to_string();
let source_text = self.parser.get_source_text();
let provider = GoToDefinition::new(
self.parser.get_arena(),
binder,
line_map,
file_name,
source_text,
);
let pos = Position::new(line, character);
let result =
provider.get_definition_with_scope_cache(root, pos, &mut self.scope_cache, None);
Ok(serde_wasm_bindgen::to_value(&result)?)
}
#[wasm_bindgen(js_name = getReferencesAtPosition)]
pub fn get_references_at_position(
&mut self,
line: u32,
character: u32,
) -> Result<JsValue, JsValue> {
self.ensure_bound()?;
self.ensure_line_map();
let root = self
.source_file_idx
.ok_or_else(|| JsValue::from_str("Source file not available"))?;
let binder = self
.binder
.as_ref()
.ok_or_else(|| JsValue::from_str("Binder not available"))?;
let line_map = self
.line_map
.as_ref()
.ok_or_else(|| JsValue::from_str("Line map not available"))?;
let file_name = self.parser.get_file_name().to_string();
let source_text = self.parser.get_source_text();
let provider = FindReferences::new(
self.parser.get_arena(),
binder,
line_map,
file_name,
source_text,
);
let pos = Position::new(line, character);
let result =
provider.find_references_with_scope_cache(root, pos, &mut self.scope_cache, None);
Ok(serde_wasm_bindgen::to_value(&result)?)
}
#[wasm_bindgen(js_name = getCompletionsAtPosition)]
pub fn get_completions_at_position(
&mut self,
line: u32,
character: u32,
) -> Result<JsValue, JsValue> {
self.ensure_bound()?;
self.ensure_line_map();
let root = self
.source_file_idx
.ok_or_else(|| JsValue::from_str("Source file not available"))?;
let binder = self
.binder
.as_ref()
.ok_or_else(|| JsValue::from_str("Binder not available"))?;
let line_map = self
.line_map
.as_ref()
.ok_or_else(|| JsValue::from_str("Line map not available"))?;
let source_text = self.parser.get_source_text();
let file_name = self.parser.get_file_name().to_string();
let provider = Completions::new_with_types(
self.parser.get_arena(),
binder,
line_map,
&self.type_interner,
source_text,
file_name,
);
let pos = Position::new(line, character);
let result = provider.get_completions_with_caches(
root,
pos,
&mut self.type_cache,
&mut self.scope_cache,
None,
);
Ok(serde_wasm_bindgen::to_value(&result)?)
}
#[wasm_bindgen(js_name = getHoverAtPosition)]
pub fn get_hover_at_position(&mut self, line: u32, character: u32) -> Result<JsValue, JsValue> {
self.ensure_bound()?;
self.ensure_line_map();
let root = self
.source_file_idx
.ok_or_else(|| JsValue::from_str("Source file not available"))?;
let binder = self
.binder
.as_ref()
.ok_or_else(|| JsValue::from_str("Binder not available"))?;
let line_map = self
.line_map
.as_ref()
.ok_or_else(|| JsValue::from_str("Line map not available"))?;
let source_text = self.parser.get_source_text();
let file_name = self.parser.get_file_name().to_string();
let provider = HoverProvider::new(
self.parser.get_arena(),
binder,
line_map,
&self.type_interner,
source_text,
file_name,
);
let pos = Position::new(line, character);
let result = provider.get_hover_with_scope_cache(
root,
pos,
&mut self.type_cache,
&mut self.scope_cache,
None,
);
Ok(serde_wasm_bindgen::to_value(&result)?)
}
#[wasm_bindgen(js_name = getSignatureHelpAtPosition)]
pub fn get_signature_help_at_position(
&mut self,
line: u32,
character: u32,
) -> Result<JsValue, JsValue> {
self.ensure_bound()?;
self.ensure_line_map();
let root = self
.source_file_idx
.ok_or_else(|| JsValue::from_str("Source file not available"))?;
let binder = self
.binder
.as_ref()
.ok_or_else(|| JsValue::from_str("Binder not available"))?;
let line_map = self
.line_map
.as_ref()
.ok_or_else(|| JsValue::from_str("Line map not available"))?;
let source_text = self.parser.get_source_text();
let file_name = self.parser.get_file_name().to_string();
let provider = SignatureHelpProvider::new(
self.parser.get_arena(),
binder,
line_map,
&self.type_interner,
source_text,
file_name,
);
let pos = Position::new(line, character);
let result = provider.get_signature_help_with_scope_cache(
root,
pos,
&mut self.type_cache,
&mut self.scope_cache,
None,
);
Ok(serde_wasm_bindgen::to_value(&result)?)
}
#[wasm_bindgen(js_name = getDocumentSymbols)]
pub fn get_document_symbols(&mut self) -> Result<JsValue, JsValue> {
self.ensure_bound()?;
self.ensure_line_map();
let root = self
.source_file_idx
.ok_or_else(|| JsValue::from_str("Source file not available"))?;
let line_map = self
.line_map
.as_ref()
.ok_or_else(|| JsValue::from_str("Line map not available"))?;
let source_text = self.parser.get_source_text();
let provider = DocumentSymbolProvider::new(self.parser.get_arena(), line_map, source_text);
let result = provider.get_document_symbols(root);
Ok(serde_wasm_bindgen::to_value(&result)?)
}
#[wasm_bindgen(js_name = getSemanticTokens)]
pub fn get_semantic_tokens(&mut self) -> Result<Vec<u32>, JsValue> {
self.ensure_bound()?;
self.ensure_line_map();
let root = self
.source_file_idx
.ok_or_else(|| JsValue::from_str("Source file not available"))?;
let binder = self
.binder
.as_ref()
.ok_or_else(|| JsValue::from_str("Binder not available"))?;
let line_map = self
.line_map
.as_ref()
.ok_or_else(|| JsValue::from_str("Line map not available"))?;
let source_text = self.parser.get_source_text();
let mut provider =
SemanticTokensProvider::new(self.parser.get_arena(), binder, line_map, source_text);
Ok(provider.get_semantic_tokens(root))
}
#[wasm_bindgen(js_name = prepareRename)]
pub fn prepare_rename(&mut self, line: u32, character: u32) -> Result<JsValue, JsValue> {
self.ensure_bound()?;
self.ensure_line_map();
let binder = self
.binder
.as_ref()
.ok_or_else(|| JsValue::from_str("Internal error: binder not available"))?;
let line_map = self
.line_map
.as_ref()
.ok_or_else(|| JsValue::from_str("Internal error: line map not available"))?;
let file_name = self.parser.get_file_name().to_string();
let source_text = self.parser.get_source_text();
let provider = RenameProvider::new(
self.parser.get_arena(),
binder,
line_map,
file_name,
source_text,
);
let pos = Position::new(line, character);
let result = provider.prepare_rename(pos);
Ok(serde_wasm_bindgen::to_value(&result)?)
}
#[wasm_bindgen(js_name = getRenameEdits)]
pub fn get_rename_edits(
&mut self,
line: u32,
character: u32,
new_name: String,
) -> Result<JsValue, JsValue> {
self.ensure_bound()?;
self.ensure_line_map();
let root = self
.source_file_idx
.ok_or_else(|| JsValue::from_str("Source file not available"))?;
let binder = self
.binder
.as_ref()
.ok_or_else(|| JsValue::from_str("Binder not available"))?;
let line_map = self
.line_map
.as_ref()
.ok_or_else(|| JsValue::from_str("Line map not available"))?;
let file_name = self.parser.get_file_name().to_string();
let source_text = self.parser.get_source_text();
let provider = RenameProvider::new(
self.parser.get_arena(),
binder,
line_map,
file_name,
source_text,
);
let pos = Position::new(line, character);
match provider.provide_rename_edits_with_scope_cache(
root,
pos,
new_name,
&mut self.scope_cache,
None,
) {
Ok(edit) => Ok(serde_wasm_bindgen::to_value(&edit)?),
Err(e) => Err(JsValue::from_str(&e)),
}
}
#[wasm_bindgen(js_name = getCodeActions)]
pub fn get_code_actions(
&mut self,
start_line: u32,
start_char: u32,
end_line: u32,
end_char: u32,
) -> Result<JsValue, JsValue> {
self.ensure_bound()?;
self.ensure_line_map();
let root = self
.source_file_idx
.ok_or_else(|| JsValue::from_str("Source file not available"))?;
let binder = self
.binder
.as_ref()
.ok_or_else(|| JsValue::from_str("Binder not available"))?;
let line_map = self
.line_map
.as_ref()
.ok_or_else(|| JsValue::from_str("Line map not available"))?;
let file_name = self.parser.get_file_name().to_string();
let source_text = self.parser.get_source_text();
let provider = CodeActionProvider::new(
self.parser.get_arena(),
binder,
line_map,
file_name,
source_text,
);
let range = Range::new(
Position::new(start_line, start_char),
Position::new(end_line, end_char),
);
let context = CodeActionContext {
diagnostics: Vec::new(),
only: None,
import_candidates: Vec::new(),
};
let result = provider.provide_code_actions(root, range, context);
Ok(serde_wasm_bindgen::to_value(&result)?)
}
#[wasm_bindgen(js_name = getCodeActionsWithContext)]
pub fn get_code_actions_with_context(
&mut self,
start_line: u32,
start_char: u32,
end_line: u32,
end_char: u32,
context: JsValue,
) -> Result<JsValue, JsValue> {
self.ensure_bound()?;
self.ensure_line_map();
let context = if context.is_null() || context.is_undefined() {
CodeActionContext {
diagnostics: Vec::new(),
only: None,
import_candidates: Vec::new(),
}
} else {
let context_input: CodeActionContextInput = serde_wasm_bindgen::from_value(context)?;
let import_candidates = context_input
.import_candidates
.into_iter()
.map(ImportCandidate::try_from)
.collect::<Result<Vec<_>, _>>()?;
CodeActionContext {
diagnostics: context_input.diagnostics,
only: context_input.only,
import_candidates,
}
};
let root = self
.source_file_idx
.ok_or_else(|| JsValue::from_str("Source file not available"))?;
let binder = self
.binder
.as_ref()
.ok_or_else(|| JsValue::from_str("Binder not available"))?;
let line_map = self
.line_map
.as_ref()
.ok_or_else(|| JsValue::from_str("Line map not available"))?;
let file_name = self.parser.get_file_name().to_string();
let source_text = self.parser.get_source_text();
let provider = CodeActionProvider::new(
self.parser.get_arena(),
binder,
line_map,
file_name,
source_text,
);
let range = Range::new(
Position::new(start_line, start_char),
Position::new(end_line, end_char),
);
let result = provider.provide_code_actions(root, range, context);
Ok(serde_wasm_bindgen::to_value(&result)?)
}
#[wasm_bindgen(js_name = getLspDiagnostics)]
pub fn get_lsp_diagnostics(&mut self) -> Result<JsValue, JsValue> {
self.ensure_bound()?;
self.ensure_line_map();
let root = self
.source_file_idx
.ok_or_else(|| JsValue::from_str("Source file not available"))?;
let binder = self
.binder
.as_ref()
.ok_or_else(|| JsValue::from_str("Binder not available"))?;
let line_map = self
.line_map
.as_ref()
.ok_or_else(|| JsValue::from_str("Line map not available"))?;
let file_name = self.parser.get_file_name().to_string();
let source_text = self.parser.get_source_text();
let checker_options = self.compiler_options.to_checker_options();
let mut checker = if let Some(cache) = self.type_cache.take() {
CheckerState::with_cache_and_options(
self.parser.get_arena(),
binder,
&self.type_interner,
file_name,
cache,
&checker_options,
)
} else {
CheckerState::with_options(
self.parser.get_arena(),
binder,
&self.type_interner,
file_name,
&checker_options,
)
};
checker.check_source_file(root);
let lsp_diagnostics: Vec<_> = checker
.ctx
.diagnostics
.iter()
.map(|diag| convert_diagnostic(diag, line_map, source_text))
.collect();
self.type_cache = Some(checker.extract_cache());
Ok(serde_wasm_bindgen::to_value(&lsp_diagnostics)?)
}
}
#[wasm_bindgen(js_name = createParser)]
pub fn create_parser(file_name: String, source_text: String) -> Parser {
Parser::new(file_name, source_text)
}
use crate::parallel::{
BindResult, MergedProgram, check_files_parallel, merge_bind_results, parse_and_bind_parallel,
};
#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct FileCheckResultJson {
file_name: String,
parse_diagnostics: Vec<ParseDiagnosticJson>,
check_diagnostics: Vec<CheckDiagnosticJson>,
}
#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct ParseDiagnosticJson {
message: String,
start: u32,
length: u32,
code: u32,
}
#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct CheckDiagnosticJson {
message_text: String,
code: u32,
start: u32,
length: u32,
category: String,
}
#[wasm_bindgen]
pub struct WasmProgram {
files: Vec<(String, String)>,
merged: Option<MergedProgram>,
bind_results: Option<Vec<BindResult>>,
lib_files: Vec<(String, String)>,
compiler_options: CompilerOptions,
}
impl Default for WasmProgram {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen]
impl WasmProgram {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
files: Vec::new(),
lib_files: Vec::new(),
merged: None,
bind_results: None,
compiler_options: CompilerOptions::default(),
}
}
#[wasm_bindgen(js_name = addFile)]
pub fn add_file(&mut self, file_name: String, source_text: String) {
self.merged = None;
self.bind_results = None;
if file_name.ends_with("package.json") {
return;
}
self.files.push((file_name, source_text));
}
#[wasm_bindgen(js_name = addLibFile)]
pub fn add_lib_file(&mut self, file_name: String, source_text: String) {
self.merged = None;
self.bind_results = None;
self.lib_files.push((file_name, source_text));
}
#[wasm_bindgen(js_name = setCompilerOptions)]
pub fn set_compiler_options(&mut self, options_json: &str) -> Result<(), JsValue> {
match serde_json::from_str::<CompilerOptions>(options_json) {
Ok(options) => {
self.compiler_options = options;
self.merged = None;
self.bind_results = None;
Ok(())
}
Err(e) => Err(JsValue::from_str(&format!(
"Failed to parse compiler options: {e}"
))),
}
}
#[allow(clippy::missing_const_for_fn)] #[wasm_bindgen(js_name = getFileCount)]
pub fn get_file_count(&self) -> usize {
self.files.len()
}
#[wasm_bindgen]
pub fn clear(&mut self) {
self.files.clear();
self.lib_files.clear();
self.merged = None;
self.bind_results = None;
}
#[wasm_bindgen(js_name = checkAll)]
pub fn check_all(&mut self) -> String {
if self.files.is_empty() && self.lib_files.is_empty() {
return r#"{"files":[],"stats":{"totalFiles":0,"totalDiagnostics":0}}"#.to_string();
}
let lib_file_objects: Vec<Arc<lib_loader::LibFile>> = self
.lib_files
.iter()
.map(|(file_name, source_text)| {
get_or_create_lib_file(file_name.clone(), source_text.clone())
})
.collect();
let bind_results = if !lib_file_objects.is_empty() {
use crate::parallel;
parallel::parse_and_bind_parallel_with_libs(self.files.clone(), &lib_file_objects)
} else {
parse_and_bind_parallel(self.files.clone())
};
let parse_diags: Vec<Vec<_>> = bind_results
.iter()
.map(|r| r.parse_diagnostics.clone())
.collect();
let file_names: Vec<String> = bind_results.iter().map(|r| r.file_name.clone()).collect();
let merged = merge_bind_results(bind_results);
let checker_options = self.compiler_options.to_checker_options();
let check_result = check_files_parallel(&merged, &checker_options, &lib_file_objects);
let mut file_results: Vec<FileCheckResultJson> = Vec::new();
let mut total_diagnostics = 0;
for (i, file_name) in file_names.iter().enumerate() {
let parse_diagnostics: Vec<ParseDiagnosticJson> = parse_diags[i]
.iter()
.map(|d| ParseDiagnosticJson {
message: d.message.clone(),
start: d.start,
length: d.length,
code: d.code,
})
.collect();
let check_diagnostics: Vec<CheckDiagnosticJson> = check_result
.file_results
.iter()
.find(|r| &r.file_name == file_name)
.map(|r| {
r.diagnostics
.iter()
.map(|d| CheckDiagnosticJson {
message_text: d.message_text.clone(),
code: d.code,
start: d.start,
length: d.length,
category: format!("{:?}", d.category),
})
.collect()
})
.unwrap_or_default();
total_diagnostics += parse_diagnostics.len() + check_diagnostics.len();
file_results.push(FileCheckResultJson {
file_name: file_name.clone(),
parse_diagnostics,
check_diagnostics,
});
}
self.merged = Some(merged);
let result = serde_json::json!({
"files": file_results,
"stats": {
"totalFiles": file_names.len(),
"totalDiagnostics": total_diagnostics,
}
});
serde_json::to_string(&result).unwrap_or_else(|_| "{}".to_string())
}
#[wasm_bindgen(js_name = getDiagnosticCodes)]
pub fn get_diagnostic_codes(&mut self) -> String {
if self.files.is_empty() && self.lib_files.is_empty() {
return "{}".to_string();
}
let lib_file_objects: Vec<Arc<lib_loader::LibFile>> = self
.lib_files
.iter()
.map(|(file_name, source_text)| {
get_or_create_lib_file(file_name.clone(), source_text.clone())
})
.collect();
let bind_results = if !lib_file_objects.is_empty() {
use crate::parallel;
parallel::parse_and_bind_parallel_with_libs(self.files.clone(), &lib_file_objects)
} else {
parse_and_bind_parallel(self.files.clone())
};
let mut file_codes: FxHashMap<String, Vec<u32>> = FxHashMap::default();
for result in &bind_results {
let codes: Vec<u32> = result.parse_diagnostics.iter().map(|d| d.code).collect();
file_codes.insert(result.file_name.clone(), codes);
}
let merged = merge_bind_results(bind_results);
let checker_options = self.compiler_options.to_checker_options();
let check_result = check_files_parallel(&merged, &checker_options, &lib_file_objects);
for file_result in &check_result.file_results {
let entry = file_codes.entry(file_result.file_name.clone()).or_default();
for diag in &file_result.diagnostics {
entry.push(diag.code);
}
}
self.merged = Some(merged);
serde_json::to_string(&file_codes).unwrap_or_else(|_| "{}".to_string())
}
#[wasm_bindgen(js_name = getAllDiagnosticCodes)]
pub fn get_all_diagnostic_codes(&mut self) -> Vec<u32> {
if self.files.is_empty() && self.lib_files.is_empty() {
return Vec::new();
}
let lib_file_objects: Vec<Arc<lib_loader::LibFile>> = self
.lib_files
.iter()
.map(|(file_name, source_text)| {
get_or_create_lib_file(file_name.clone(), source_text.clone())
})
.collect();
let bind_results = if !lib_file_objects.is_empty() {
use crate::parallel;
parallel::parse_and_bind_parallel_with_libs(self.files.clone(), &lib_file_objects)
} else {
parse_and_bind_parallel(self.files.clone())
};
let mut all_codes: Vec<u32> = Vec::new();
for result in &bind_results {
for diag in &result.parse_diagnostics {
all_codes.push(diag.code);
}
}
let merged = merge_bind_results(bind_results);
let checker_options = self.compiler_options.to_checker_options();
let check_result = check_files_parallel(&merged, &checker_options, &lib_file_objects);
for file_result in &check_result.file_results {
for diag in &file_result.diagnostics {
all_codes.push(diag.code);
}
}
self.merged = Some(merged);
all_codes
}
}
#[wasm_bindgen(js_name = createProgram)]
pub fn create_program() -> WasmProgram {
WasmProgram::new()
}
#[wasm_bindgen]
#[repr(i32)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Comparison {
LessThan = -1,
EqualTo = 0,
GreaterThan = 1,
}
#[wasm_bindgen(js_name = compareStringsCaseSensitive)]
pub fn compare_strings_case_sensitive(a: Option<String>, b: Option<String>) -> Comparison {
match (a, b) {
(None, None) => Comparison::EqualTo,
(None, Some(_)) => Comparison::LessThan,
(Some(_), None) => Comparison::GreaterThan,
(Some(a), Some(b)) => match a.cmp(&b) {
std::cmp::Ordering::Equal => Comparison::EqualTo,
std::cmp::Ordering::Less => Comparison::LessThan,
std::cmp::Ordering::Greater => Comparison::GreaterThan,
},
}
}
#[wasm_bindgen(js_name = compareStringsCaseInsensitive)]
pub fn compare_strings_case_insensitive(a: Option<String>, b: Option<String>) -> Comparison {
match (a, b) {
(None, None) => Comparison::EqualTo,
(None, Some(_)) => Comparison::LessThan,
(Some(_), None) => Comparison::GreaterThan,
(Some(a), Some(b)) => {
if a == b {
return Comparison::EqualTo;
}
compare_strings_case_insensitive_iter(&a, &b)
}
}
}
#[inline]
fn compare_strings_case_insensitive_iter(a: &str, b: &str) -> Comparison {
use std::cmp::Ordering;
let mut a_chars = a.chars().flat_map(char::to_uppercase);
let mut b_chars = b.chars().flat_map(char::to_uppercase);
loop {
match (a_chars.next(), b_chars.next()) {
(None, None) => return Comparison::EqualTo,
(None, Some(_)) => return Comparison::LessThan,
(Some(_), None) => return Comparison::GreaterThan,
(Some(a_char), Some(b_char)) => match a_char.cmp(&b_char) {
Ordering::Less => return Comparison::LessThan,
Ordering::Greater => return Comparison::GreaterThan,
Ordering::Equal => continue,
},
}
}
}
#[wasm_bindgen(js_name = compareStringsCaseInsensitiveEslintCompatible)]
pub fn compare_strings_case_insensitive_eslint_compatible(
a: Option<String>,
b: Option<String>,
) -> Comparison {
match (a, b) {
(None, None) => Comparison::EqualTo,
(None, Some(_)) => Comparison::LessThan,
(Some(_), None) => Comparison::GreaterThan,
(Some(a), Some(b)) => {
if a == b {
return Comparison::EqualTo;
}
compare_strings_case_insensitive_lower_iter(&a, &b)
}
}
}
#[inline]
fn compare_strings_case_insensitive_lower_iter(a: &str, b: &str) -> Comparison {
use std::cmp::Ordering;
let mut a_chars = a.chars().flat_map(char::to_lowercase);
let mut b_chars = b.chars().flat_map(char::to_lowercase);
loop {
match (a_chars.next(), b_chars.next()) {
(None, None) => return Comparison::EqualTo,
(None, Some(_)) => return Comparison::LessThan,
(Some(_), None) => return Comparison::GreaterThan,
(Some(a_char), Some(b_char)) => match a_char.cmp(&b_char) {
Ordering::Less => return Comparison::LessThan,
Ordering::Greater => return Comparison::GreaterThan,
Ordering::Equal => continue,
},
}
}
}
#[wasm_bindgen(js_name = equateStringsCaseSensitive)]
pub fn equate_strings_case_sensitive(a: &str, b: &str) -> bool {
a == b
}
#[wasm_bindgen(js_name = equateStringsCaseInsensitive)]
pub fn equate_strings_case_insensitive(a: &str, b: &str) -> bool {
if a.len() != b.len() {
}
a.chars()
.flat_map(char::to_uppercase)
.eq(b.chars().flat_map(char::to_uppercase))
}
pub const DIRECTORY_SEPARATOR: char = '/';
pub const ALT_DIRECTORY_SEPARATOR: char = '\\';
#[allow(clippy::missing_const_for_fn)] #[wasm_bindgen(js_name = isAnyDirectorySeparator)]
pub fn is_any_directory_separator(char_code: u32) -> bool {
char_code == DIRECTORY_SEPARATOR as u32 || char_code == ALT_DIRECTORY_SEPARATOR as u32
}
#[wasm_bindgen(js_name = normalizeSlashes)]
pub fn normalize_slashes(path: &str) -> String {
if path.contains('\\') {
path.replace('\\', "/")
} else {
path.to_string()
}
}
#[wasm_bindgen(js_name = hasTrailingDirectorySeparator)]
pub fn has_trailing_directory_separator(path: &str) -> bool {
let last_char = match path.chars().last() {
Some(c) => c,
None => return false,
};
last_char == DIRECTORY_SEPARATOR || last_char == ALT_DIRECTORY_SEPARATOR
}
#[wasm_bindgen(js_name = pathIsRelative)]
pub fn path_is_relative(path: &str) -> bool {
if path.starts_with("./") || path.starts_with(".\\") || path == "." {
return true;
}
if path.starts_with("../") || path.starts_with("..\\") || path == ".." {
return true;
}
false
}
#[wasm_bindgen(js_name = removeTrailingDirectorySeparator)]
pub fn remove_trailing_directory_separator(path: &str) -> String {
if !has_trailing_directory_separator(path) || path.len() <= 1 {
return path.to_string();
}
path.strip_suffix(DIRECTORY_SEPARATOR)
.or_else(|| path.strip_suffix(ALT_DIRECTORY_SEPARATOR))
.unwrap_or(path)
.to_string()
}
#[wasm_bindgen(js_name = ensureTrailingDirectorySeparator)]
pub fn ensure_trailing_directory_separator(path: &str) -> String {
if has_trailing_directory_separator(path) {
path.to_string()
} else {
format!("{path}/")
}
}
#[wasm_bindgen(js_name = hasExtension)]
pub fn has_extension(file_name: &str) -> bool {
get_base_file_name(file_name).contains('.')
}
#[wasm_bindgen(js_name = getBaseFileName)]
pub fn get_base_file_name(path: &str) -> String {
let path = normalize_slashes(path);
let path = if has_trailing_directory_separator(&path) && path.len() > 1 {
path.strip_suffix(DIRECTORY_SEPARATOR)
.or_else(|| path.strip_suffix(ALT_DIRECTORY_SEPARATOR))
.unwrap_or(&path)
} else {
&path
};
match path.rfind('/') {
Some(idx) => path[idx + 1..].to_string(),
None => path.to_string(),
}
}
#[wasm_bindgen(js_name = fileExtensionIs)]
pub fn file_extension_is(path: &str, extension: &str) -> bool {
path.len() > extension.len() && path.ends_with(extension)
}
#[wasm_bindgen(js_name = toFileNameLowerCase)]
pub fn to_file_name_lower_case(x: &str) -> String {
let needs_conversion = x.chars().any(|c| {
!matches!(c,
'\u{0130}' | '\u{0131}' | '\u{00DF}' | 'a'..='z' | '0'..='9' | '\\' | '/' | ':' | '-' | '_' | '.' | ' ' )
});
if !needs_conversion {
return x.to_string();
}
x.to_lowercase()
}
use crate::char_codes::CharacterCodes;
#[allow(clippy::missing_const_for_fn)] #[wasm_bindgen(js_name = isLineBreak)]
pub fn is_line_break(ch: u32) -> bool {
ch == CharacterCodes::LINE_FEED
|| ch == CharacterCodes::CARRIAGE_RETURN
|| ch == CharacterCodes::LINE_SEPARATOR
|| ch == CharacterCodes::PARAGRAPH_SEPARATOR
}
#[wasm_bindgen(js_name = isWhiteSpaceSingleLine)]
pub fn is_white_space_single_line(ch: u32) -> bool {
ch == CharacterCodes::SPACE
|| ch == CharacterCodes::TAB
|| ch == CharacterCodes::VERTICAL_TAB
|| ch == CharacterCodes::FORM_FEED
|| ch == CharacterCodes::NON_BREAKING_SPACE
|| ch == CharacterCodes::NEXT_LINE
|| ch == CharacterCodes::OGHAM
|| (CharacterCodes::EN_QUAD..=CharacterCodes::ZERO_WIDTH_SPACE).contains(&ch)
|| ch == CharacterCodes::NARROW_NO_BREAK_SPACE
|| ch == CharacterCodes::MATHEMATICAL_SPACE
|| ch == CharacterCodes::IDEOGRAPHIC_SPACE
|| ch == CharacterCodes::BYTE_ORDER_MARK
}
#[wasm_bindgen(js_name = isWhiteSpaceLike)]
pub fn is_white_space_like(ch: u32) -> bool {
is_white_space_single_line(ch) || is_line_break(ch)
}
#[wasm_bindgen(js_name = isDigit)]
pub fn is_digit(ch: u32) -> bool {
(CharacterCodes::_0..=CharacterCodes::_9).contains(&ch)
}
#[wasm_bindgen(js_name = isOctalDigit)]
pub fn is_octal_digit(ch: u32) -> bool {
(CharacterCodes::_0..=CharacterCodes::_7).contains(&ch)
}
#[wasm_bindgen(js_name = isHexDigit)]
pub fn is_hex_digit(ch: u32) -> bool {
is_digit(ch)
|| (CharacterCodes::UPPER_A..=CharacterCodes::UPPER_F).contains(&ch)
|| (CharacterCodes::LOWER_A..=CharacterCodes::LOWER_F).contains(&ch)
}
#[wasm_bindgen(js_name = isASCIILetter)]
pub fn is_ascii_letter(ch: u32) -> bool {
(CharacterCodes::UPPER_A..=CharacterCodes::UPPER_Z).contains(&ch)
|| (CharacterCodes::LOWER_A..=CharacterCodes::LOWER_Z).contains(&ch)
}
#[wasm_bindgen(js_name = isWordCharacter)]
pub fn is_word_character(ch: u32) -> bool {
is_ascii_letter(ch) || is_digit(ch) || ch == CharacterCodes::UNDERSCORE
}
#[cfg(test)]
#[path = "../tests/asi_conformance_tests.rs"]
mod asi_conformance_tests;
#[cfg(test)]
#[path = "../tests/debug_asi.rs"]
mod debug_asi;
#[cfg(test)]
#[path = "../tests/p1_error_recovery_tests.rs"]
mod p1_error_recovery_tests;
#[cfg(test)]
#[path = "../crates/tsz-checker/tests/constructor_accessibility.rs"]
mod constructor_accessibility;
#[cfg(test)]
#[path = "../crates/tsz-checker/tests/void_return_exception.rs"]
mod void_return_exception;
#[cfg(test)]
#[path = "../crates/tsz-checker/tests/any_propagation.rs"]
mod any_propagation;
#[cfg(test)]
#[path = "../crates/tsz-checker/tests/any_propagation_tests.rs"]
mod any_propagation_tests;
#[cfg(test)]
#[path = "../crates/tsz-checker/tests/const_assertion_tests.rs"]
mod const_assertion_tests;
#[cfg(test)]
#[path = "../crates/tsz-checker/tests/contextual_typing_tests.rs"]
mod contextual_typing_tests;
#[cfg(test)]
#[path = "../crates/tsz-checker/tests/freshness_stripping_tests.rs"]
mod freshness_stripping_tests;
#[cfg(test)]
#[path = "../crates/tsz-checker/tests/function_bivariance.rs"]
mod function_bivariance;
#[cfg(test)]
#[path = "../crates/tsz-checker/tests/global_type_tests.rs"]
mod global_type_tests;
#[cfg(test)]
#[path = "../crates/tsz-checker/tests/symbol_resolution_tests.rs"]
mod symbol_resolution_tests;
#[cfg(test)]
#[path = "../crates/tsz-checker/tests/ts2304_tests.rs"]
mod ts2304_tests;
#[cfg(test)]
#[path = "../crates/tsz-checker/tests/ts2305_tests.rs"]
mod ts2305_tests;
#[cfg(test)]
#[path = "../crates/tsz-checker/tests/ts2306_tests.rs"]
mod ts2306_tests;
#[cfg(test)]
#[path = "../crates/tsz-checker/tests/widening_integration_tests.rs"]
mod widening_integration_tests;