use super::{CheckOptions, Server, TsServerRequest, TsServerResponse};
use anyhow::{Context, Result};
use rustc_hash::FxHashMap;
use std::sync::Arc;
use tsz::binder::BinderState;
use tsz::checker::context::{CheckerOptions, LibContext};
use tsz::checker::diagnostics::DiagnosticCategory;
use tsz::checker::module_resolution::build_module_resolution_maps;
use tsz::checker::state::CheckerState;
use tsz::emitter::ScriptTarget;
use tsz::lib_loader::LibFile;
use tsz::parser::ParserState;
use tsz::parser::base::NodeIndex;
use tsz::parser::node::NodeArena;
use tsz_cli::config::{checker_target_from_emitter, default_lib_name_for_target};
use tsz_solver::QueryCache;
use tsz_solver::RelationCacheStats;
use tsz_solver::TypeInterner;
pub(crate) struct RunCheckResult {
pub(crate) codes: Vec<i32>,
pub(crate) relation_cache_stats: RelationCacheStats,
}
impl Server {
pub(crate) fn get_semantic_diagnostics_full(
&mut self,
file_path: &str,
content: &str,
) -> Vec<tsz::checker::diagnostics::Diagnostic> {
self.get_diagnostics_by_category(file_path, content, DiagnosticCategory::Error)
}
pub(crate) fn get_suggestion_diagnostics(
&mut self,
file_path: &str,
content: &str,
) -> Vec<tsz::checker::diagnostics::Diagnostic> {
self.get_diagnostics_by_category(file_path, content, DiagnosticCategory::Suggestion)
}
fn get_diagnostics_by_category(
&mut self,
file_path: &str,
content: &str,
category: DiagnosticCategory,
) -> Vec<tsz::checker::diagnostics::Diagnostic> {
let mut options = CheckOptions::default();
if (self.inferred_module_is_none_for_projects
&& !self.auto_imports_allowed_for_inferred_projects)
|| self.fourslash_module_none_directive_blocks_import_syntax(file_path)
{
options.module = Some("none".to_string());
}
let lib_files = match if options.no_lib {
Ok(vec![])
} else {
self.load_libs_unified(&options)
} {
Ok(libs) => libs,
Err(_) => return Vec::new(),
};
let checker_options = self.build_checker_options(&options);
let type_interner = TypeInterner::new();
let lib_contexts: Vec<LibContext> = lib_files
.iter()
.map(|lib| LibContext {
arena: std::sync::Arc::clone(&lib.arena),
binder: std::sync::Arc::clone(&lib.binder),
})
.collect();
let mut parser = ParserState::new(file_path.to_string(), content.to_string());
let root = parser.parse_source_file();
let parse_diagnostics = parser.get_diagnostics().to_vec();
let arena = Arc::new(parser.into_arena());
let mut binder = BinderState::new();
binder.bind_source_file(&arena, root);
let binder = Arc::new(binder);
let all_arenas: Arc<Vec<Arc<NodeArena>>> = Arc::new(vec![std::sync::Arc::clone(&arena)]);
let all_binders: Arc<Vec<Arc<BinderState>>> =
Arc::new(vec![std::sync::Arc::clone(&binder)]);
let user_file_contexts: Vec<LibContext> = vec![LibContext {
arena: std::sync::Arc::clone(&arena),
binder: std::sync::Arc::clone(&binder),
}];
let mut all_contexts = lib_contexts;
all_contexts.extend(user_file_contexts);
let mut file_names: Vec<String> = self
.open_files
.keys()
.filter(|path| Self::is_checkable_file(path))
.cloned()
.collect();
if !file_names.iter().any(|path| path == file_path) {
file_names.push(file_path.to_string());
}
let (resolved_module_paths, resolved_modules) = build_module_resolution_maps(&file_names);
let resolved_module_paths = Arc::new(resolved_module_paths);
let query_cache = QueryCache::new(&type_interner);
let mut checker = CheckerState::new(
&arena,
&binder,
&query_cache,
file_path.to_string(),
checker_options,
);
if !all_contexts.is_empty() {
checker.ctx.set_lib_contexts(all_contexts);
}
checker.ctx.set_actual_lib_file_count(lib_files.len());
checker.ctx.set_all_arenas(all_arenas);
checker.ctx.set_all_binders(all_binders);
checker.ctx.set_resolved_module_paths(resolved_module_paths);
checker.ctx.set_resolved_modules(resolved_modules);
checker.ctx.set_current_file_idx(0);
checker.check_source_file(root);
let mut diagnostics: Vec<tsz::checker::diagnostics::Diagnostic> = Vec::new();
if category == DiagnosticCategory::Error {
for d in &parse_diagnostics {
diagnostics.push(tsz::checker::diagnostics::Diagnostic::error(
file_path.to_string(),
d.start,
d.length,
d.message.clone(),
d.code,
));
}
}
for diag in checker.ctx.diagnostics {
if diag.category == category {
diagnostics.push(diag);
}
}
if category == DiagnosticCategory::Error {
diagnostics
.retain(|diag| !Self::should_suppress_namespace_global_ts2403(diag, content));
}
diagnostics
}
fn fourslash_module_none_directive_blocks_import_syntax(&self, file_path: &str) -> bool {
self.open_files
.get(file_path)
.and_then(|text| Self::fourslash_module_none_blocking_imports_from_text(text))
.or_else(|| {
self.open_files.iter().find_map(|(path, text)| {
if path == file_path {
return None;
}
Self::fourslash_module_none_blocking_imports_from_text(text)
})
})
.unwrap_or(false)
}
fn fourslash_module_none_blocking_imports_from_text(source_text: &str) -> Option<bool> {
let mut saw_module = false;
let mut module_none = false;
let mut saw_target = false;
let mut target_supports_imports = false;
for line in source_text.lines().take(64) {
let trimmed = line.trim_start();
let directive = trimmed.trim_start_matches('/').trim_start();
if let Some(rest) = directive.strip_prefix("@module:") {
saw_module = true;
module_none = rest.split(',').map(str::trim).any(|value| {
value.eq_ignore_ascii_case("none") || value.parse::<i64>().ok() == Some(0)
});
continue;
}
if let Some(rest) = directive.strip_prefix("@target:") {
saw_target = true;
target_supports_imports = rest.split(',').map(str::trim).any(|value| {
value.eq_ignore_ascii_case("es6")
|| value.eq_ignore_ascii_case("es2015")
|| value.eq_ignore_ascii_case("es2016")
|| value.eq_ignore_ascii_case("es2017")
|| value.eq_ignore_ascii_case("es2018")
|| value.eq_ignore_ascii_case("es2019")
|| value.eq_ignore_ascii_case("es2020")
|| value.eq_ignore_ascii_case("es2021")
|| value.eq_ignore_ascii_case("es2022")
|| value.eq_ignore_ascii_case("es2023")
|| value.eq_ignore_ascii_case("es2024")
|| value.eq_ignore_ascii_case("esnext")
|| value.eq_ignore_ascii_case("latest")
|| value.parse::<i64>().ok().is_some_and(|n| n >= 2)
});
}
}
if saw_module && module_none {
return Some(!(saw_target && target_supports_imports));
}
None
}
fn should_suppress_namespace_global_ts2403(
diag: &tsz::checker::diagnostics::Diagnostic,
content: &str,
) -> bool {
if diag.code
!= tsz::checker::diagnostics::diagnostic_codes::SUBSEQUENT_VARIABLE_DECLARATIONS_MUST_HAVE_THE_SAME_TYPE_VARIABLE_MUST_BE_OF_TYP
{
return false;
}
let marker = "Variable '";
let Some(start) = diag.message_text.find(marker) else {
return false;
};
let tail = &diag.message_text[start + marker.len()..];
let Some(end) = tail.find('\'') else {
return false;
};
let name = &tail[..end];
if name.is_empty() {
return false;
}
let has_ambient_namespace = content.contains("declare namespace");
let has_namespace_var = content.contains(&format!("var {name}:"));
let has_global_decl = content.contains(&format!("declare var {name}:"));
has_ambient_namespace && has_namespace_var && has_global_decl
}
pub(crate) fn handle_tsz_performance(
&mut self,
seq: u64,
request: &TsServerRequest,
) -> TsServerResponse {
let body = (|| -> Option<serde_json::Value> {
let file = request
.arguments
.get("file")
.and_then(|v| v.as_str())
.map(std::string::ToString::to_string)
.or_else(|| self.open_files.keys().next().cloned())?;
let content = request
.arguments
.get("fileContent")
.and_then(|v| v.as_str())
.map(std::string::ToString::to_string)
.or_else(|| self.open_files.get(&file).cloned())
.or_else(|| std::fs::read_to_string(&file).ok())?;
let mut files = FxHashMap::default();
files.insert(file.clone(), content);
let result = self.run_check(files, CheckOptions::default()).ok()?;
let stats = result.relation_cache_stats;
let mut payload = serde_json::json!({
"file": file,
"checksCompleted": self.checks_completed,
"errorCount": result.codes.len(),
"errorCodes": result.codes,
"relationCache": {
"subtypeHits": stats.subtype_hits,
"subtypeMisses": stats.subtype_misses,
"subtypeEntries": stats.subtype_entries,
"assignabilityHits": stats.assignability_hits,
"assignabilityMisses": stats.assignability_misses,
"assignabilityEntries": stats.assignability_entries
}
});
if self.enable_telemetry {
payload["telemetryEvent"] = serde_json::json!({
"eventName": "tszPerformance",
"relationCache": payload["relationCache"].clone()
});
}
Some(payload)
})();
self.stub_response(seq, request, body)
}
pub(crate) fn run_check(
&mut self,
files: FxHashMap<String, String>,
options: CheckOptions,
) -> Result<RunCheckResult> {
let lib_files = if options.no_lib {
vec![]
} else {
self.load_libs_unified(&options)?
};
let checker_options = self.build_checker_options(&options);
let type_interner = TypeInterner::new();
let lib_contexts: Vec<LibContext> = lib_files
.iter()
.map(|lib| LibContext {
arena: std::sync::Arc::clone(&lib.arena),
binder: std::sync::Arc::clone(&lib.binder),
})
.collect();
struct BoundFile {
name: String,
arena: Arc<NodeArena>,
binder: Arc<BinderState>,
root: NodeIndex,
parse_errors: Vec<i32>,
}
let unified_lib_binder =
(!lib_files.is_empty()).then(|| std::sync::Arc::clone(&lib_files[0].binder));
let lib_symbol_count = unified_lib_binder.as_ref().map_or(0, |b| b.symbols.len());
let mut bound_files: Vec<BoundFile> = Vec::with_capacity(files.len());
let mut binary_file_errors: Vec<(String, i32)> = Vec::new();
for (file_name, content) in files {
if !Self::is_checkable_file(&file_name) {
continue;
}
if super::content_appears_binary(&content) {
binary_file_errors
.push((file_name.clone(), super::TS1490_FILE_APPEARS_TO_BE_BINARY));
continue;
}
let mut parser = ParserState::new(file_name.clone(), content);
let root_idx = parser.parse_source_file();
let parse_errors: Vec<i32> = parser
.get_diagnostics()
.iter()
.map(|d| d.code as i32)
.collect();
let arena = Arc::new(parser.into_arena());
let mut binder = BinderState::new();
if let Some(lib_binder) = unified_lib_binder.as_ref() {
binder.symbols = tsz::binder::SymbolArena::new_with_base(lib_symbol_count as u32);
binder.lib_binders.push(Arc::clone(lib_binder));
}
binder.bind_source_file(&arena, root_idx);
bound_files.push(BoundFile {
name: file_name,
arena,
binder: Arc::new(binder),
root: root_idx,
parse_errors,
});
}
let all_arenas: Arc<Vec<Arc<NodeArena>>> = Arc::new(
bound_files
.iter()
.map(|f| std::sync::Arc::clone(&f.arena))
.collect(),
);
let all_binders: Arc<Vec<Arc<BinderState>>> = Arc::new(
bound_files
.iter()
.map(|f| std::sync::Arc::clone(&f.binder))
.collect(),
);
let user_file_contexts: Vec<LibContext> = bound_files
.iter()
.map(|f| LibContext {
arena: std::sync::Arc::clone(&f.arena),
binder: std::sync::Arc::clone(&f.binder),
})
.collect();
let mut all_contexts = lib_contexts;
all_contexts.extend(user_file_contexts);
let all_contexts_arc: Arc<Vec<LibContext>> = Arc::new(all_contexts);
let file_names: Vec<String> = bound_files.iter().map(|f| f.name.clone()).collect();
let (resolved_module_paths, resolved_modules) = build_module_resolution_maps(&file_names);
let resolved_module_paths_arc: Arc<FxHashMap<(usize, String), usize>> =
Arc::new(resolved_module_paths);
let resolved_modules_arc: Arc<rustc_hash::FxHashSet<String>> = Arc::new(resolved_modules);
let query_cache = QueryCache::new(&type_interner);
let mut all_codes: Vec<i32> = Vec::new();
for (_file_name, code) in binary_file_errors {
all_codes.push(code);
}
for (file_idx, bound) in bound_files.iter().enumerate() {
all_codes.extend(&bound.parse_errors);
let mut checker = CheckerState::new(
&bound.arena,
&bound.binder,
&query_cache,
bound.name.clone(),
checker_options.clone(),
);
if !all_contexts_arc.is_empty() {
checker.ctx.set_lib_contexts((*all_contexts_arc).clone());
}
checker.ctx.set_actual_lib_file_count(lib_files.len());
checker.ctx.set_all_arenas(Arc::clone(&all_arenas));
checker.ctx.set_all_binders(Arc::clone(&all_binders));
checker
.ctx
.set_resolved_module_paths(Arc::clone(&resolved_module_paths_arc));
checker
.ctx
.set_resolved_modules((*resolved_modules_arc).clone());
checker.ctx.set_current_file_idx(file_idx);
checker.check_source_file(bound.root);
for diag in &checker.ctx.diagnostics {
if diag.category == DiagnosticCategory::Error {
all_codes.push(diag.code as i32);
}
}
}
Ok(RunCheckResult {
codes: all_codes,
relation_cache_stats: query_cache.relation_cache_stats(),
})
}
pub(crate) fn load_libs_unified(
&mut self,
options: &CheckOptions,
) -> Result<Vec<Arc<LibFile>>> {
let mut lib_names = self.determine_libs(options);
if lib_names.is_empty() {
return Ok(vec![]);
}
lib_names.sort();
if let Some((cached_names, cached_lib)) = &self.unified_lib_cache
&& *cached_names == lib_names
{
return Ok(vec![std::sync::Arc::clone(cached_lib)]);
}
let mut lib_files = Vec::new();
let mut loaded = rustc_hash::FxHashSet::default();
for lib_name in &lib_names {
self.load_lib_recursive(lib_name, &mut lib_files, &mut loaded)?;
}
if lib_files.is_empty() {
return Ok(vec![]);
}
use tsz::binder::LibContext as BinderLibContext;
let lib_contexts: Vec<BinderLibContext> = lib_files
.iter()
.map(|lib| BinderLibContext {
arena: Arc::clone(&lib.arena),
binder: Arc::clone(&lib.binder),
})
.collect();
let mut unified_binder = BinderState::new();
unified_binder.merge_lib_contexts_into_binder(&lib_contexts);
let unified_arena = lib_files.first().map_or_else(
|| Arc::new(tsz::parser::node::NodeArena::new()),
|lib| Arc::clone(&lib.arena),
);
let unified_lib = Arc::new(LibFile::new(
"unified-libs".to_string(),
unified_arena,
Arc::new(unified_binder),
));
self.unified_lib_cache = Some((lib_names, std::sync::Arc::clone(&unified_lib)));
Ok(vec![unified_lib])
}
pub(crate) fn normalize_lib_alias(name: &str) -> String {
match name.to_lowercase().trim() {
"es6" => "es6".to_string(),
"es7" => "es2016".to_string(),
"lib" | "lib.d.ts" => "es5".to_string(),
"dom" => "dom.generated".to_string(),
"dom.iterable" => "dom.iterable.generated".to_string(),
"dom.asynciterable" => "dom.asynciterable.generated".to_string(),
s if s.starts_with("lib.") && s.ends_with(".d.ts") => {
let inner = &s[4..s.len() - 5]; Self::normalize_lib_alias(inner)
}
other => other.to_string(),
}
}
pub(crate) fn load_lib_recursive(
&mut self,
lib_name: &str,
result: &mut Vec<Arc<LibFile>>,
loaded: &mut rustc_hash::FxHashSet<String>,
) -> Result<()> {
let aliased = Self::normalize_lib_alias(lib_name);
let normalized = aliased.trim().to_lowercase();
if loaded.contains(&normalized) {
return Ok(());
}
loaded.insert(normalized.clone());
if let Some((lib, references)) = self.lib_cache.get(&normalized) {
let lib_clone = std::sync::Arc::clone(lib);
let refs = references.clone();
for ref_lib in &refs {
self.load_lib_recursive(ref_lib, result, loaded)?;
}
result.push(lib_clone);
return Ok(());
}
let candidates = [
self.lib_dir.join(format!("{normalized}.d.ts")),
self.lib_dir.join(format!("lib.{normalized}.d.ts")),
self.tests_lib_dir.join(format!("{normalized}.d.ts")),
];
for candidate in &candidates {
if candidate.exists() {
let content = std::fs::read_to_string(candidate)
.with_context(|| format!("failed to read lib file: {}", candidate.display()))?;
let references = Self::parse_lib_references(&content);
for ref_lib in &references {
self.load_lib_recursive(ref_lib, result, loaded)?;
}
let file_name = candidate.file_name().map_or_else(
|| format!("lib.{normalized}.d.ts"),
|s| s.to_string_lossy().to_string(),
);
let mut parser = ParserState::new(file_name.clone(), content);
let root_idx = parser.parse_source_file();
let mut binder = BinderState::new();
binder.bind_source_file(parser.get_arena(), root_idx);
let lib = Arc::new(LibFile::new(
file_name,
Arc::new(parser.into_arena()),
Arc::new(binder),
));
const MAX_LIB_CACHE_ENTRIES: usize = 50;
if self.lib_cache.len() >= MAX_LIB_CACHE_ENTRIES {
self.lib_cache.clear();
self.unified_lib_cache = None;
}
self.lib_cache.insert(
normalized,
(std::sync::Arc::clone(&lib), references.clone()),
);
result.push(lib);
return Ok(());
}
}
Ok(())
}
pub(crate) fn parse_lib_references(content: &str) -> Vec<String> {
let mut refs = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if !trimmed.starts_with("///") {
continue;
}
if let Some(start) = trimmed.find("<reference") {
let rest = &trimmed[start..];
if let Some(lib_start) = rest.find("lib=") {
let after_lib = &rest[lib_start + 4..];
let quote = after_lib.chars().next();
if quote == Some('"') || quote == Some('\'') {
let quote_char = quote.unwrap();
let value_start = 1;
if let Some(end) = after_lib[value_start..].find(quote_char) {
let lib_name = &after_lib[value_start..value_start + end];
refs.push(lib_name.trim().to_lowercase());
}
}
}
}
}
refs
}
pub(crate) fn determine_libs(&self, options: &CheckOptions) -> Vec<String> {
if options.no_lib {
return vec![];
}
if let Some(ref libs) = options.lib {
libs.iter().map(|s| s.trim().to_lowercase()).collect()
} else {
let target = Self::parse_target(&options.target);
let default_lib = default_lib_name_for_target(target);
vec![default_lib.to_string()]
}
}
pub(crate) fn is_checkable_file(file_name: &str) -> bool {
let lower = file_name.to_lowercase();
lower.ends_with(".ts")
|| lower.ends_with(".tsx")
|| lower.ends_with(".js")
|| lower.ends_with(".jsx")
|| lower.ends_with(".mts")
|| lower.ends_with(".cts")
|| lower.ends_with(".mjs")
|| lower.ends_with(".cjs")
}
pub(crate) fn parse_target(target: &Option<String>) -> ScriptTarget {
target.as_ref().map_or(ScriptTarget::ES5, |t| {
let first_target = t.split(',').next().unwrap_or(t).trim().to_lowercase();
match first_target.as_str() {
"es3" => ScriptTarget::ES3,
"es5" => ScriptTarget::ES5,
"es6" | "es2015" => ScriptTarget::ES2015,
"es2016" => ScriptTarget::ES2016,
"es2017" => ScriptTarget::ES2017,
"es2018" => ScriptTarget::ES2018,
"es2019" => ScriptTarget::ES2019,
"es2020" => ScriptTarget::ES2020,
"es2021" => ScriptTarget::ES2021,
"es2022" | "es2023" => ScriptTarget::ES2022,
_ => ScriptTarget::ESNext,
}
})
}
pub(crate) fn build_checker_options(&self, options: &CheckOptions) -> CheckerOptions {
let emitter_target = Self::parse_target(&options.target);
let checker_target = checker_target_from_emitter(emitter_target);
CheckerOptions {
strict: options.strict,
strict_null_checks: options.strict_null_checks.unwrap_or(options.strict),
strict_function_types: options.strict_function_types.unwrap_or(options.strict),
strict_bind_call_apply: options.strict_bind_call_apply.unwrap_or(options.strict),
strict_property_initialization: options
.strict_property_initialization
.unwrap_or(options.strict),
no_implicit_any: options.no_implicit_any.unwrap_or(options.strict),
no_implicit_this: options.no_implicit_this.unwrap_or(options.strict),
no_implicit_returns: options.no_implicit_returns,
exact_optional_property_types: options.exact_optional_property_types,
no_unchecked_indexed_access: options.no_unchecked_indexed_access,
use_unknown_in_catch_variables: options
.use_unknown_in_catch_variables
.unwrap_or(options.strict),
isolated_modules: options.isolated_modules,
no_lib: options.no_lib,
no_types_and_symbols: false,
target: checker_target,
module: if let Some(module_str) = &options.module {
match module_str.to_lowercase().as_str() {
"commonjs" => tsz::ModuleKind::CommonJS,
"amd" => tsz::ModuleKind::AMD,
"umd" => tsz::ModuleKind::UMD,
"system" => tsz::ModuleKind::System,
"es2015" => tsz::ModuleKind::ES2015,
"es2020" => tsz::ModuleKind::ES2020,
"es2022" => tsz::ModuleKind::ES2022,
"esnext" => tsz::ModuleKind::ESNext,
"node16" => tsz::ModuleKind::Node16,
"nodenext" => tsz::ModuleKind::NodeNext,
_ => tsz::ModuleKind::None,
}
} else {
tsz::ModuleKind::CommonJS
},
es_module_interop: options.es_module_interop,
allow_synthetic_default_imports: options
.allow_synthetic_default_imports
.unwrap_or(options.es_module_interop),
allow_unreachable_code: options.allow_unreachable_code,
no_property_access_from_index_signature: options
.no_property_access_from_index_signature,
sound_mode: false, experimental_decorators: options.experimental_decorators,
no_unused_locals: options.no_unused_locals,
no_unused_parameters: options.no_unused_parameters,
always_strict: options.always_strict.unwrap_or(options.strict),
resolve_json_module: options.resolve_json_module,
check_js: options.check_js,
no_resolve: options.no_resolve,
no_unchecked_side_effect_imports: options.no_unchecked_side_effect_imports,
no_implicit_override: options.no_implicit_override,
jsx_factory: "React.createElement".to_string(),
jsx_fragment_factory: "React.Fragment".to_string(),
jsx_mode: tsz_common::checker_options::JsxMode::None,
module_explicitly_set: options.module.is_some(),
suppress_excess_property_errors: false,
suppress_implicit_any_index_errors: false,
}
}
}