use std::borrow::Cow;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::Path;
use std::str::FromStr;
use std::sync::Arc;
use deno_ast::SourceRange;
use deno_ast::SourceTextInfo;
use deno_core::ModuleSpecifier;
use deno_core::anyhow::anyhow;
use deno_core::error::AnyError;
use deno_core::resolve_url;
use deno_core::serde::Deserialize;
use deno_core::serde::Serialize;
use deno_core::serde_json;
use deno_core::serde_json::json;
use deno_core::url::Url;
use deno_error::JsErrorBox;
use deno_lint::diagnostic::LintDiagnosticRange;
use deno_npm::NpmPackageId;
use deno_path_util::url_to_file_path;
use deno_resolver::npm::managed::NpmResolutionCell;
use deno_runtime::deno_node::PathClean;
use deno_semver::SmallStackString;
use deno_semver::StackString;
use deno_semver::Version;
use deno_semver::jsr::JsrPackageNvReference;
use deno_semver::jsr::JsrPackageReqReference;
use deno_semver::npm::NpmPackageReqReference;
use deno_semver::package::PackageNv;
use deno_semver::package::PackageNvReference;
use deno_semver::package::PackageReq;
use deno_semver::package::PackageReqReference;
use import_map::ImportMap;
use lsp_types::Uri;
use node_resolver::InNpmPackageChecker;
use node_resolver::NodeResolutionKind;
use node_resolver::ResolutionMode;
use once_cell::sync::Lazy;
use regex::Regex;
use tokio_util::sync::CancellationToken;
use tower_lsp::lsp_types as lsp;
use tower_lsp::lsp_types::Position;
use tower_lsp::lsp_types::Range;
use super::diagnostics::DiagnosticSource;
use super::documents::DocumentModule;
use super::documents::DocumentModules;
use super::language_server;
use super::resolver::LspResolver;
use super::tsc;
use crate::args::jsr_url;
use crate::lsp::urls::uri_to_url;
use crate::tools::lint::CliLinter;
use crate::util::path::relative_specifier;
static FIX_ALL_ERROR_CODES: Lazy<HashMap<&'static str, &'static str>> =
Lazy::new(|| ([("2339", "2339"), ("2345", "2339")]).into_iter().collect());
static PREFERRED_FIXES: Lazy<HashMap<&'static str, (u32, bool)>> =
Lazy::new(|| {
([
("annotateWithTypeFromJSDoc", (1, false)),
("constructorForDerivedNeedSuperCall", (1, false)),
("extendsInterfaceBecomesImplements", (1, false)),
("awaitInSyncFunction", (1, false)),
("classIncorrectlyImplementsInterface", (3, false)),
("classDoesntImplementInheritedAbstractMember", (3, false)),
("unreachableCode", (1, false)),
("unusedIdentifier", (1, false)),
("forgottenThisPropertyAccess", (1, false)),
("spelling", (2, false)),
("addMissingAwait", (1, false)),
("fixImport", (0, true)),
])
.into_iter()
.collect()
});
static IMPORT_SPECIFIER_RE: Lazy<Regex> = lazy_regex::lazy_regex!(
r#"\sfrom\s+["']([^"']*)["']|import\s*\(\s*["']([^"']*)["']\s*\)"#
);
const SUPPORTED_EXTENSIONS: &[&str] = &[
".ts", ".tsx", ".js", ".jsx", ".mjs", ".mts", ".cjs", ".cts", ".d.ts",
".d.mts", ".d.cts",
];
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DataQuickFixChange {
pub range: Range,
pub new_text: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DataQuickFix {
pub description: String,
pub changes: Vec<DataQuickFixChange>,
}
#[derive(Debug, PartialEq, Eq)]
pub enum Category {
Lint {
message: String,
code: String,
hint: Option<String>,
quick_fixes: Vec<DataQuickFix>,
},
}
#[derive(Debug, PartialEq, Eq)]
pub struct Reference {
category: Category,
range: Range,
}
impl Reference {
pub fn to_diagnostic(&self) -> lsp::Diagnostic {
match &self.category {
Category::Lint {
message,
code,
hint,
quick_fixes,
} => lsp::Diagnostic {
range: self.range,
severity: Some(lsp::DiagnosticSeverity::WARNING),
code: Some(lsp::NumberOrString::String(code.to_string())),
code_description: None,
source: Some(DiagnosticSource::Lint.as_lsp_source().to_string()),
message: {
let mut msg = message.to_string();
if let Some(hint) = hint {
msg.push('\n');
msg.push_str(hint);
}
msg
},
related_information: None,
tags: None, data: if quick_fixes.is_empty() {
None
} else {
serde_json::to_value(quick_fixes).ok()
},
},
}
}
}
fn as_lsp_range_from_lint_diagnostic(
diagnostic_range: &LintDiagnosticRange,
) -> Range {
as_lsp_range(diagnostic_range.range, &diagnostic_range.text_info)
}
fn as_lsp_range(
source_range: SourceRange,
text_info: &SourceTextInfo,
) -> Range {
let start_lc = text_info.line_and_column_index(source_range.start);
let end_lc = text_info.line_and_column_index(source_range.end);
Range {
start: Position {
line: start_lc.line_index as u32,
character: start_lc.column_index as u32,
},
end: Position {
line: end_lc.line_index as u32,
character: end_lc.column_index as u32,
},
}
}
pub fn get_lint_references(
parsed_source: &deno_ast::ParsedSource,
linter: &CliLinter,
token: CancellationToken,
) -> Result<Vec<Reference>, AnyError> {
let lint_diagnostics = linter.lint_with_ast(parsed_source, token)?;
Ok(
lint_diagnostics
.into_iter()
.filter_map(|d| {
let range = d.range.as_ref()?;
Some(Reference {
range: as_lsp_range_from_lint_diagnostic(range),
category: Category::Lint {
message: d.details.message,
code: d.details.code.to_string(),
hint: d.details.hint,
quick_fixes: d
.details
.fixes
.into_iter()
.map(|f| DataQuickFix {
description: f.description.to_string(),
changes: f
.changes
.into_iter()
.map(|change| DataQuickFixChange {
range: as_lsp_range(change.range, &range.text_info),
new_text: change.new_text.to_string(),
})
.collect(),
})
.collect(),
},
})
})
.collect(),
)
}
fn code_as_string(code: &Option<lsp::NumberOrString>) -> String {
match code {
Some(lsp::NumberOrString::String(str)) => str.clone(),
Some(lsp::NumberOrString::Number(num)) => num.to_string(),
_ => "".to_string(),
}
}
pub fn import_map_lookup(
import_map: &ImportMap,
specifier: &Url,
referrer: &Url,
) -> Option<String> {
import_map_lookup_inner(
import_map,
specifier,
referrer,
ReferrerInAddressSkip::SkipAll,
)
}
#[derive(Clone, Copy)]
enum ReferrerInAddressSkip {
SkipAll,
SkipPassthrough,
}
fn import_map_lookup_inner(
import_map: &ImportMap,
specifier: &Url,
referrer: &Url,
skip_mode: ReferrerInAddressSkip,
) -> Option<String> {
let specifier_str = specifier.as_str();
for entry in import_map.entries_for_referrer(referrer) {
if let Some(address) = entry.value {
let address_str = address.as_str();
if referrer.as_str().starts_with(address_str) {
match skip_mode {
ReferrerInAddressSkip::SkipAll => continue,
ReferrerInAddressSkip::SkipPassthrough => {
if address_str
.as_bytes()
.get(address_str.len().wrapping_sub(entry.raw_key.len() + 1))
== Some(&b'/')
&& address_str.ends_with(entry.raw_key)
{
continue;
}
}
}
}
if address_str == specifier_str {
return Some(entry.raw_key.to_string());
}
if address_str.ends_with('/') && specifier_str.starts_with(address_str) {
return Some(specifier_str.replace(address_str, entry.raw_key));
}
}
}
None
}
pub struct TsResponseImportMapper<'a> {
document_modules: &'a DocumentModules,
scope: Option<Arc<ModuleSpecifier>>,
maybe_import_map: Option<&'a ImportMap>,
resolver: &'a LspResolver,
tsc_specifier_map: Arc<tsc::TscSpecifierMap>,
}
impl<'a> TsResponseImportMapper<'a> {
pub fn new(
document_modules: &'a DocumentModules,
scope: Option<Arc<ModuleSpecifier>>,
resolver: &'a LspResolver,
tsc_specifier_map: Arc<tsc::TscSpecifierMap>,
) -> Self {
let maybe_import_map = resolver
.get_scoped_resolver(scope.as_deref())
.as_workspace_resolver()
.maybe_import_map();
Self {
document_modules,
scope,
maybe_import_map,
resolver,
tsc_specifier_map,
}
}
pub fn check_specifier(
&self,
specifier: &ModuleSpecifier,
referrer: &ModuleSpecifier,
) -> Option<String> {
fn concat_npm_specifier(
prefix: &str,
pkg_req: &PackageReq,
sub_path: Option<&str>,
) -> String {
let result = format!("{}{}", prefix, pkg_req);
match sub_path {
Some(path) => format!("{}/{}", result, path),
None => result,
}
}
if specifier.scheme() == "node" {
return Some(specifier.to_string());
}
let scoped_resolver =
self.resolver.get_scoped_resolver(self.scope.as_deref());
if let Some(dep_name) =
scoped_resolver.resource_url_to_configured_dep_key(specifier, referrer)
{
return Some(dep_name);
}
if let Some(jsr_path) = specifier.as_str().strip_prefix(jsr_url().as_str())
{
let mut segments = jsr_path.split('/');
let name = if jsr_path.starts_with('@') {
let scope = segments.next()?;
let name = segments.next()?;
capacity_builder::StringBuilder::<StackString>::build(|builder| {
builder.append(scope);
builder.append("/");
builder.append(name);
})
.unwrap()
} else {
StackString::from(segments.next()?)
};
let version = Version::parse_standard(segments.next()?).ok()?;
let nv = PackageNv { name, version };
let path = segments.collect::<Vec<_>>().join("/");
let export = scoped_resolver.jsr_lookup_export_for_path(&nv, &path)?;
let sub_path = (export != ".")
.then_some(export)
.map(SmallStackString::from_string);
let mut req = None;
req = req.or_else(|| {
let import_map = self.maybe_import_map?;
for entry in import_map.entries_for_referrer(referrer) {
let Some(value) = entry.raw_value else {
continue;
};
let Ok(req_ref) = JsrPackageReqReference::from_str(value) else {
continue;
};
let req = req_ref.req();
if req.name == nv.name
&& req.version_req.tag().is_none()
&& req.version_req.matches(&nv.version)
{
return Some(req.clone());
}
}
None
});
req = req.or_else(|| scoped_resolver.jsr_lookup_req_for_nv(&nv));
let spec_str = if let Some(req) = req {
let req_ref = PackageReqReference { req, sub_path };
JsrPackageReqReference::new(req_ref).to_string()
} else {
let nv_ref = PackageNvReference { nv, sub_path };
JsrPackageNvReference::new(nv_ref).to_string()
};
let specifier = ModuleSpecifier::parse(&spec_str).ok()?;
if let Some(import_map) = self.maybe_import_map {
if let Some(result) =
import_map_lookup(import_map, &specifier, referrer)
{
return Some(result);
}
if let Some(req_ref_str) = specifier.as_str().strip_prefix("jsr:")
&& !req_ref_str.starts_with('/')
{
let specifier_str = format!("jsr:/{req_ref_str}");
if let Ok(specifier) = ModuleSpecifier::parse(&specifier_str)
&& let Some(result) =
import_map_lookup(import_map, &specifier, referrer)
{
return Some(result);
}
}
}
return Some(spec_str);
}
if let Some(npm_resolver) = scoped_resolver.as_maybe_managed_npm_resolver()
{
let match_specifier = || {
let in_npm_pkg = scoped_resolver
.as_in_npm_pkg_checker()
.in_npm_package(specifier);
if !in_npm_pkg {
return None;
}
let pkg_id = npm_resolver
.resolve_pkg_id_from_specifier(specifier)
.ok()??;
let pkg_reqs =
maybe_reverse_definitely_typed(&pkg_id, npm_resolver.resolution())
.unwrap_or_else(|| {
npm_resolver
.resolution()
.resolve_pkg_reqs_from_pkg_id(&pkg_id)
});
if pkg_reqs.is_empty() {
return None;
}
let sub_path = npm_resolver
.resolve_pkg_folder_from_pkg_id(&pkg_id)
.ok()
.and_then(|pkg_folder| {
self.resolve_package_path(specifier, &pkg_folder)
})?;
let sub_path = Some(sub_path).filter(|s| !s.is_empty());
if let Some(import_map) = self.maybe_import_map {
let pkg_reqs = pkg_reqs.iter().collect::<HashSet<_>>();
let mut matches = Vec::new();
for entry in import_map.entries_for_referrer(referrer) {
if let Some(value) = entry.raw_value
&& let Ok(package_ref) = NpmPackageReqReference::from_str(value)
&& pkg_reqs.contains(package_ref.req())
{
let sub_path = sub_path.as_deref().unwrap_or("");
let value_sub_path = package_ref.sub_path().unwrap_or("");
if let Some(key_sub_path) = sub_path.strip_prefix(value_sub_path)
{
if entry.raw_key.ends_with('/') || key_sub_path.is_empty() {
matches.push(format!("{}{}", entry.raw_key, key_sub_path));
}
}
}
}
matches.sort_by_key(|a| a.len());
if let Some(matched) = matches.first() {
return Some(matched.to_string());
}
}
if let Some(pkg_req) = pkg_reqs.first() {
return Some(concat_npm_specifier(
"npm:",
pkg_req,
sub_path.as_deref(),
));
}
None
};
if let Some(result) = match_specifier() {
return Some(result);
}
}
if let Some(bare_package_specifier) =
scoped_resolver.jsr_lookup_bare_specifier_for_workspace_file(specifier)
{
return Some(bare_package_specifier);
}
if let Some(import_map) = self.maybe_import_map
&& let Some(result) = import_map_lookup_inner(
import_map,
specifier,
referrer,
ReferrerInAddressSkip::SkipPassthrough,
)
{
return Some(result);
}
None
}
fn resolve_package_path(
&self,
specifier: &ModuleSpecifier,
package_root_folder: &Path,
) -> Option<String> {
let scoped_resolver = self.resolver.get_scoped_resolver(Some(specifier));
let package_json = scoped_resolver
.as_pkg_json_resolver()
.get_closest_package_json(&package_root_folder.join("package.json"))
.ok()
.flatten()?;
let Some(exports) = &package_json.exports else {
return Some("".to_string());
};
let root_folder = package_json.path.parent()?;
let specifier_path = url_to_file_path(specifier).ok()?;
let mut search_paths = vec![specifier_path.clone()];
if specifier_path.extension().and_then(|e| e.to_str()) == Some("js") {
search_paths.insert(0, specifier_path.with_extension("d.ts"));
} else if let Some(file_name) =
specifier_path.file_name().and_then(|f| f.to_str())
{
if let Some(file_stem) = file_name.strip_suffix(".d.ts") {
search_paths
.push(specifier_path.with_file_name(format!("{}.js", file_stem)));
} else if let Some(file_stem) = file_name.strip_suffix(".d.cts") {
search_paths
.push(specifier_path.with_file_name(format!("{}.cjs", file_stem)));
} else if let Some(file_stem) = file_name.strip_suffix(".d.mts") {
search_paths
.push(specifier_path.with_file_name(format!("{}.mjs", file_stem)));
}
}
for search_path in search_paths {
if let Some(result) =
try_reverse_map_package_json_exports(root_folder, &search_path, exports)
{
return Some(result);
}
}
None
}
pub fn check_unresolved_specifier(
&self,
specifier: &str,
referrer: &ModuleSpecifier,
resolution_mode: ResolutionMode,
new_file_hints: &[Url],
) -> Option<String> {
let specifier_stem = specifier.strip_suffix(".js").unwrap_or(specifier);
let specifiers = std::iter::once(Cow::Borrowed(specifier)).chain(
SUPPORTED_EXTENSIONS
.iter()
.map(|ext| Cow::Owned(format!("{specifier_stem}{ext}"))),
);
let scoped_resolver =
self.resolver.get_scoped_resolver(self.scope.as_deref());
for specifier in specifiers {
if let Some(specifier) = scoped_resolver
.as_cli_resolver()
.resolve(
&specifier,
referrer,
deno_graph::Position::zeroed(),
resolution_mode,
NodeResolutionKind::Types,
)
.ok()
.and_then(|s| self.tsc_specifier_map.normalize(s.as_str()).ok())
.filter(|s| {
new_file_hints.contains(s)
|| self
.document_modules
.specifier_exists(s, self.scope.as_deref())
})
&& let Some(specifier) = self
.check_specifier(&specifier, referrer)
.or_else(|| relative_specifier(referrer, &specifier))
.filter(|s| !s.contains("/node_modules/"))
{
return Some(specifier);
}
}
None
}
pub fn is_valid_import(
&self,
specifier_text: &str,
referrer: &ModuleSpecifier,
resolution_mode: ResolutionMode,
) -> bool {
self
.resolver
.get_scoped_resolver(self.scope.as_deref())
.as_cli_resolver()
.resolve(
specifier_text,
referrer,
deno_graph::Position::zeroed(),
resolution_mode,
NodeResolutionKind::Types,
)
.ok()
.filter(|s| {
let specifier = self
.tsc_specifier_map
.normalize(s.as_str())
.map(Cow::Owned)
.unwrap_or(Cow::Borrowed(s));
!specifier.as_str().contains("/node_modules/")
})
.is_some()
}
}
fn maybe_reverse_definitely_typed(
pkg_id: &NpmPackageId,
resolution: &NpmResolutionCell,
) -> Option<Vec<PackageReq>> {
let rest = pkg_id.nv.name.strip_prefix("@types/")?;
let package_name = if rest.contains("__") {
Cow::Owned(format!("@{}", rest.replace("__", "/")))
} else {
Cow::Borrowed(rest)
};
let reqs = resolution
.package_reqs()
.into_iter()
.filter_map(|(req, nv)| (*nv.name == package_name).then_some(req))
.collect::<Vec<_>>();
if reqs.is_empty() { None } else { Some(reqs) }
}
fn try_reverse_map_package_json_exports(
root_path: &Path,
target_path: &Path,
exports: &serde_json::Map<String, serde_json::Value>,
) -> Option<String> {
use deno_core::serde_json::Value;
fn try_reverse_map_package_json_exports_inner(
root_path: &Path,
target_path: &Path,
exports: &serde_json::Map<String, Value>,
) -> Option<String> {
for (key, value) in exports {
match value {
Value::String(str) => {
if root_path.join(str).clean() == target_path {
return Some(if let Some(suffix) = key.strip_prefix("./") {
suffix.to_string()
} else {
String::new() });
}
}
Value::Object(obj) => {
if let Some(result) = try_reverse_map_package_json_exports_inner(
root_path,
target_path,
obj,
) {
return Some(if let Some(suffix) = key.strip_prefix("./") {
if result.is_empty() {
suffix.to_string()
} else {
format!("{}/{}", suffix, result)
}
} else {
result });
}
}
_ => {}
}
}
None
}
try_reverse_map_package_json_exports_inner(root_path, target_path, exports)
}
pub fn fix_ts_import_changes(
changes: &[tsc::FileTextChanges],
module: &DocumentModule,
language_server: &language_server::Inner,
token: &CancellationToken,
) -> Result<Vec<tsc::FileTextChanges>, AnyError> {
let mut r = Vec::new();
let new_file_hints = changes
.iter()
.filter(|c| c.is_new_file.unwrap_or(false))
.filter_map(|c| resolve_url(&c.file_name).ok())
.collect::<Vec<_>>();
for change in changes {
if token.is_cancelled() {
return Err(anyhow!("request cancelled"));
}
let is_new_file = change.is_new_file.unwrap_or(false);
let Ok(target_specifier) = resolve_url(&change.file_name) else {
continue;
};
let target_module = if is_new_file {
None
} else {
let Some(target_module) =
language_server.document_modules.module_for_specifier(
&target_specifier,
module.scope.as_deref(),
Some(&module.compiler_options_key),
)
else {
continue;
};
Some(target_module)
};
let resolution_mode = target_module
.as_ref()
.map(|m| m.resolution_mode)
.unwrap_or(ResolutionMode::Import);
let import_mapper = language_server.get_ts_response_import_mapper(module);
let mut text_changes = Vec::new();
for text_change in &change.text_changes {
let lines = text_change.new_text.split('\n');
let new_lines: Vec<String> = lines
.map(|line| {
if let Some(captures) = IMPORT_SPECIFIER_RE.captures(line) {
let specifier =
captures.iter().skip(1).find_map(|s| s).unwrap().as_str();
if let Some(new_specifier) = import_mapper
.check_unresolved_specifier(
specifier,
&target_specifier,
resolution_mode,
&new_file_hints,
)
{
line.replace(specifier, &new_specifier)
} else {
line.to_string()
}
} else {
line.to_string()
}
})
.collect();
text_changes.push(tsc::TextChange {
span: text_change.span.clone(),
new_text: new_lines.join("\n").to_string(),
});
}
r.push(tsc::FileTextChanges {
file_name: change.file_name.clone(),
text_changes,
is_new_file: change.is_new_file,
});
}
Ok(r)
}
pub fn fix_ts_import_changes_for_file_rename(
changes: Vec<tsc::FileTextChanges>,
new_uri: &str,
old_module: &DocumentModule,
language_server: &language_server::Inner,
token: &CancellationToken,
) -> Result<Vec<tsc::FileTextChanges>, AnyError> {
let Ok(new_uri) = Uri::from_str(new_uri) else {
return Ok(Vec::new());
};
if !new_uri.scheme().as_str().eq_ignore_ascii_case("file") {
return Ok(Vec::new());
}
let new_file_hints = [uri_to_url(&new_uri)];
let mut r = Vec::with_capacity(changes.len());
for mut change in changes {
if token.is_cancelled() {
return Err(anyhow!("request cancelled"));
}
let Ok(target_specifier) = resolve_url(&change.file_name) else {
continue;
};
let Some(target_module) =
language_server.document_modules.module_for_specifier(
&target_specifier,
old_module.scope.as_deref(),
Some(&old_module.compiler_options_key),
)
else {
continue;
};
let import_mapper =
language_server.get_ts_response_import_mapper(&target_module);
for text_change in &mut change.text_changes {
if let Some(new_specifier) = import_mapper.check_unresolved_specifier(
&text_change.new_text,
&target_module.specifier,
target_module.resolution_mode,
&new_file_hints,
) {
text_change.new_text = new_specifier;
}
}
r.push(change);
}
Ok(r)
}
fn fix_ts_import_action<'a>(
action: &'a tsc::CodeFixAction,
module: &DocumentModule,
language_server: &language_server::Inner,
) -> Option<Cow<'a, tsc::CodeFixAction>> {
if !matches!(
action.fix_name.as_str(),
"import" | "fixMissingFunctionDeclaration"
) {
return Some(Cow::Borrowed(action));
}
let specifier = (|| {
let text_change = action.changes.first()?.text_changes.first()?;
let captures = IMPORT_SPECIFIER_RE.captures(&text_change.new_text)?;
Some(captures.get(1)?.as_str())
})();
let Some(specifier) = specifier else {
return Some(Cow::Borrowed(action));
};
let import_mapper = language_server.get_ts_response_import_mapper(module);
if let Some(new_specifier) = import_mapper.check_unresolved_specifier(
specifier,
&module.specifier,
module.resolution_mode,
&action
.changes
.iter()
.filter(|c| c.is_new_file.unwrap_or(false))
.filter_map(|c| resolve_url(&c.file_name).ok())
.collect::<Vec<_>>(),
) {
let description = action.description.replace(specifier, &new_specifier);
let changes = action
.changes
.iter()
.map(|c| {
let text_changes = c
.text_changes
.iter()
.map(|tc| tsc::TextChange {
span: tc.span.clone(),
new_text: tc.new_text.replace(specifier, &new_specifier),
})
.collect();
tsc::FileTextChanges {
file_name: c.file_name.clone(),
text_changes,
is_new_file: c.is_new_file,
}
})
.collect();
Some(Cow::Owned(tsc::CodeFixAction {
description,
changes,
commands: None,
fix_name: action.fix_name.clone(),
fix_id: None,
fix_all_description: None,
}))
} else if !import_mapper.is_valid_import(
specifier,
&module.specifier,
module.resolution_mode,
) {
None
} else {
Some(Cow::Borrowed(action))
}
}
fn is_equivalent_code(
a: &Option<lsp::NumberOrString>,
b: &Option<lsp::NumberOrString>,
) -> bool {
let a_code = code_as_string(a);
let b_code = code_as_string(b);
FIX_ALL_ERROR_CODES.get(a_code.as_str())
== FIX_ALL_ERROR_CODES.get(b_code.as_str())
}
pub fn ts_changes_to_edit(
changes: &[tsc::FileTextChanges],
module: &DocumentModule,
language_server: &language_server::Inner,
) -> Result<Option<lsp::WorkspaceEdit>, AnyError> {
let mut edits_by_uri = HashMap::with_capacity(changes.len());
for change in changes {
let Some((uri, edits)) = change.to_text_edits(module, language_server)
else {
continue;
};
edits_by_uri.insert(uri, edits);
}
Ok(Some(lsp::WorkspaceEdit {
changes: Some(edits_by_uri),
document_changes: None,
change_annotations: None,
}))
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeActionData {
pub uri: Uri,
pub fix_id: String,
}
#[derive(Debug, Default)]
pub struct TsFixActionCollector {
entries: Vec<(lsp::CodeAction, tsc::CodeFixAction)>,
fix_all_entries_by_id: HashMap<String, (lsp::CodeAction, tsc::CodeFixAction)>,
}
impl TsFixActionCollector {
pub fn add_ts_fix_action(
&mut self,
fix_action: &tsc::CodeFixAction,
diagnostic: &lsp::Diagnostic,
module: &DocumentModule,
language_server: &language_server::Inner,
) -> Result<(), AnyError> {
if fix_action.commands.is_some() {
return Err(
JsErrorBox::new(
"UnsupportedFix",
"The action returned from TypeScript is unsupported.",
)
.into(),
);
}
let Some(fix_action) =
fix_ts_import_action(fix_action, module, language_server)
else {
return Ok(());
};
let edit =
ts_changes_to_edit(&fix_action.changes, module, language_server)?;
let action = lsp::CodeAction {
title: fix_action.description.clone(),
kind: Some(lsp::CodeActionKind::QUICKFIX),
diagnostics: Some(vec![diagnostic.clone()]),
edit,
command: None,
is_preferred: None,
disabled: None,
data: None,
};
self.entries.retain(|(a, f)| {
!(fix_action.fix_name == f.fix_name && action.edit == a.edit)
});
self.entries.push((action, fix_action.as_ref().clone()));
if let Some(fix_id) = &fix_action.fix_id
&& let Some((existing_fix_all, existing_action)) =
self.fix_all_entries_by_id.get(fix_id)
{
self.entries.retain(|(c, _)| c != existing_fix_all);
self
.entries
.push((existing_fix_all.clone(), existing_action.clone()));
}
Ok(())
}
pub fn add_ts_fix_all_action(
&mut self,
action: &tsc::CodeFixAction,
module: &DocumentModule,
diagnostic: &lsp::Diagnostic,
) {
let data = action.fix_id.as_ref().map(|fix_id| {
json!(CodeActionData {
uri: module.uri.as_ref().clone(),
fix_id: fix_id.clone(),
})
});
let title = if let Some(description) = &action.fix_all_description {
description.clone()
} else {
format!("{} (Fix all in file)", action.description)
};
let code_action = lsp::CodeAction {
title,
kind: Some(lsp::CodeActionKind::QUICKFIX),
diagnostics: Some(vec![diagnostic.clone()]),
edit: None,
command: None,
is_preferred: None,
disabled: None,
data,
};
if let Some((existing, _)) = self
.fix_all_entries_by_id
.get(action.fix_id.as_ref().unwrap())
{
self.entries.retain(|(a, _)| a != existing);
}
self.entries.push((code_action.clone(), action.clone()));
self.fix_all_entries_by_id.insert(
action.fix_id.clone().unwrap(),
(code_action, action.clone()),
);
}
pub fn is_fix_all_action(
&self,
action: &tsc::CodeFixAction,
diagnostic: &lsp::Diagnostic,
file_diagnostics: &[lsp::Diagnostic],
) -> bool {
if action
.fix_id
.as_ref()
.is_none_or(|fix_id| self.fix_all_entries_by_id.contains_key(fix_id))
{
false
} else {
file_diagnostics.iter().any(|d| {
if d.source.as_deref() != Some(DiagnosticSource::Ts.as_lsp_source())
|| d == diagnostic
|| d.code.is_none()
|| diagnostic.code.is_none()
{
return false;
}
d.code == diagnostic.code
|| is_equivalent_code(&d.code, &diagnostic.code)
})
}
}
pub fn into_code_actions(
self,
has_deno_code_actions: bool,
) -> impl Iterator<Item = lsp::CodeAction> {
let is_preferred_list = self
.entries
.iter()
.map(|(_, fix_action)| {
if fix_action.fix_id.is_some() {
return None;
}
let (fix_priority, only_one) =
PREFERRED_FIXES.get(fix_action.fix_name.as_str())?;
if has_deno_code_actions {
return Some(false);
}
for (_, other_fix_action) in &self.entries {
if other_fix_action == fix_action {
continue;
}
if other_fix_action.fix_id.is_some() {
continue;
}
let Some((other_fix_priority, _)) =
PREFERRED_FIXES.get(other_fix_action.fix_name.as_str())
else {
continue;
};
match other_fix_priority.cmp(fix_priority) {
Ordering::Less => continue,
Ordering::Greater => return Some(false),
Ordering::Equal => {
if *only_one && fix_action.fix_name == other_fix_action.fix_name {
return Some(false);
}
}
}
}
Some(true)
})
.collect::<Vec<_>>();
self.entries.into_iter().zip(is_preferred_list).map(
|((mut action, _), is_preferred)| {
action.is_preferred = is_preferred;
action
},
)
}
}
pub fn prepend_whitespace(
content: String,
line_content: Option<String>,
) -> String {
if let Some(line) = line_content {
let whitespace_end = line
.char_indices()
.find_map(|(i, c)| (!c.is_whitespace()).then_some(i))
.unwrap_or(0);
let whitespace = &line[0..whitespace_end];
format!("{}{}", &whitespace, content)
} else {
content
}
}
pub fn source_range_to_lsp_range(
range: &SourceRange,
source_text_info: &SourceTextInfo,
) -> lsp::Range {
let start = source_text_info.line_and_column_index(range.start);
let end = source_text_info.line_and_column_index(range.end);
lsp::Range {
start: lsp::Position {
line: start.line_index as u32,
character: start.column_index as u32,
},
end: lsp::Position {
line: end.line_index as u32,
character: end.column_index as u32,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_reference_to_diagnostic() {
let range = Range {
start: Position {
line: 1,
character: 1,
},
end: Position {
line: 2,
character: 2,
},
};
let test_cases = [
(
Reference {
category: Category::Lint {
message: "message1".to_string(),
code: "code1".to_string(),
hint: None,
quick_fixes: Vec::new(),
},
range,
},
lsp::Diagnostic {
range,
severity: Some(lsp::DiagnosticSeverity::WARNING),
code: Some(lsp::NumberOrString::String("code1".to_string())),
source: Some("deno-lint".to_string()),
message: "message1".to_string(),
..Default::default()
},
),
(
Reference {
category: Category::Lint {
message: "message2".to_string(),
code: "code2".to_string(),
hint: Some("hint2".to_string()),
quick_fixes: Vec::new(),
},
range,
},
lsp::Diagnostic {
range,
severity: Some(lsp::DiagnosticSeverity::WARNING),
code: Some(lsp::NumberOrString::String("code2".to_string())),
source: Some("deno-lint".to_string()),
message: "message2\nhint2".to_string(),
..Default::default()
},
),
];
for (input, expected) in test_cases.iter() {
let actual = input.to_diagnostic();
assert_eq!(&actual, expected);
}
}
#[test]
fn test_try_reverse_map_package_json_exports() {
let exports = json!({
".": {
"types": "./src/index.d.ts",
"browser": "./dist/module.js",
},
"./hooks": {
"types": "./hooks/index.d.ts",
"browser": "./dist/devtools.module.js",
},
"./utils": {
"types": {
"./sub_utils": "./utils_sub_utils.d.ts"
}
}
});
let exports = exports.as_object().unwrap();
assert_eq!(
try_reverse_map_package_json_exports(
Path::new("/project/"),
Path::new("/project/hooks/index.d.ts"),
exports,
)
.unwrap(),
"hooks"
);
assert_eq!(
try_reverse_map_package_json_exports(
Path::new("/project/"),
Path::new("/project/dist/devtools.module.js"),
exports,
)
.unwrap(),
"hooks"
);
assert_eq!(
try_reverse_map_package_json_exports(
Path::new("/project/"),
Path::new("/project/src/index.d.ts"),
exports,
)
.unwrap(),
""
);
assert_eq!(
try_reverse_map_package_json_exports(
Path::new("/project/"),
Path::new("/project/utils_sub_utils.d.ts"),
exports,
)
.unwrap(),
"utils/sub_utils"
);
}
#[test]
fn test_prepend_whitespace() {
assert_eq!(
&prepend_whitespace("foo".to_string(), Some("\u{a0}bar".to_string())),
"\u{a0}foo"
);
}
}