// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
use super::language_server;
use super::tsc;
use crate::ast;
use crate::ast::Location;
use crate::config_file::LintConfig;
use crate::lsp::documents::DocumentData;
use crate::tools::lint::create_linter;
use crate::tools::lint::get_configured_rules;
use deno_ast::swc::ast as swc_ast;
use deno_ast::swc::common::comments::Comment;
use deno_ast::swc::common::BytePos;
use deno_ast::swc::common::Span;
use deno_ast::swc::common::DUMMY_SP;
use deno_ast::swc::visit::Node;
use deno_ast::swc::visit::Visit;
use deno_ast::swc::visit::VisitWith;
use deno_ast::Diagnostic;
use deno_ast::MediaType;
use deno_ast::SourceTextInfo;
use deno_core::error::anyhow;
use deno_core::error::custom_error;
use deno_core::error::AnyError;
use deno_core::serde::Deserialize;
use deno_core::serde_json;
use deno_core::serde_json::json;
use deno_core::url;
use deno_core::ModuleResolutionError;
use deno_core::ModuleSpecifier;
use import_map::ImportMap;
use lspower::lsp;
use lspower::lsp::Position;
use lspower::lsp::Range;
use regex::Regex;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::fmt;
lazy_static::lazy_static! {
/// Diagnostic error codes which actually are the same, and so when grouping
/// fixes we treat them the same.
static ref FIX_ALL_ERROR_CODES: HashMap<&'static str, &'static str> =
(&[("2339", "2339"), ("2345", "2339"),])
.iter()
.cloned()
.collect();
/// Fixes which help determine if there is a preferred fix when there are
/// multiple fixes available.
static ref PREFERRED_FIXES: HashMap<&'static str, (u32, bool)> = (&[
("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)),
])
.iter()
.cloned()
.collect();
static ref IMPORT_SPECIFIER_RE: Regex = Regex::new(r#"\sfrom\s+["']([^"']*)["']"#).unwrap();
static ref DENO_TYPES_RE: Regex =
Regex::new(r#"(?i)^\s*@deno-types\s*=\s*(?:["']([^"']+)["']|(\S+))"#)
.unwrap();
static ref TRIPLE_SLASH_REFERENCE_RE: Regex =
Regex::new(r"(?i)^/\s*").unwrap();
static ref PATH_REFERENCE_RE: Regex =
Regex::new(r#"(?i)\spath\s*=\s*["']([^"']*)["']"#).unwrap();
static ref TYPES_REFERENCE_RE: Regex =
Regex::new(r#"(?i)\stypes\s*=\s*["']([^"']*)["']"#).unwrap();
}
const SUPPORTED_EXTENSIONS: &[&str] = &[".ts", ".tsx", ".js", ".jsx", ".mjs"];
// TODO(@kitsonk) remove after deno_graph migration
#[derive(Debug, Clone, Eq, PartialEq)]
enum TypeScriptReference {
Path(String),
Types(String),
}
fn match_to_span(comment: &Comment, m: ®ex::Match) -> Span {
Span {
lo: comment.span.lo + BytePos((m.start() + 1) as u32),
hi: comment.span.lo + BytePos((m.end() + 1) as u32),
ctxt: comment.span.ctxt,
}
}
// TODO(@kitsonk) remove after deno_graph migration
fn parse_deno_types(comment: &Comment) -> Option<(String, Span)> {
let captures = DENO_TYPES_RE.captures(&comment.text)?;
if let Some(m) = captures.get(1) {
Some((m.as_str().to_string(), match_to_span(comment, &m)))
} else if let Some(m) = captures.get(2) {
Some((m.as_str().to_string(), match_to_span(comment, &m)))
} else {
unreachable!();
}
}
// TODO(@kitsonk) remove after deno_graph migration
fn parse_ts_reference(
comment: &Comment,
) -> Option<(TypeScriptReference, Span)> {
if !TRIPLE_SLASH_REFERENCE_RE.is_match(&comment.text) {
None
} else if let Some(captures) = PATH_REFERENCE_RE.captures(&comment.text) {
let m = captures.get(1).unwrap();
Some((
TypeScriptReference::Path(m.as_str().to_string()),
match_to_span(comment, &m),
))
} else {
TYPES_REFERENCE_RE.captures(&comment.text).map(|captures| {
let m = captures.get(1).unwrap();
(
TypeScriptReference::Types(m.as_str().to_string()),
match_to_span(comment, &m),
)
})
}
}
/// Category of self-generated diagnostic messages (those not coming from)
/// TypeScript.
#[derive(Debug, PartialEq, Eq)]
pub enum Category {
/// A lint diagnostic, where the first element is the message.
Lint {
message: String,
code: String,
hint: Option,
},
}
/// A structure to hold a reference to a diagnostic message.
#[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,
} => lsp::Diagnostic {
range: self.range,
severity: Some(lsp::DiagnosticSeverity::Warning),
code: Some(lsp::NumberOrString::String(code.to_string())),
code_description: None,
source: Some("deno-lint".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, // we should tag unused code
data: None,
},
}
}
}
fn as_lsp_range(range: &deno_lint::diagnostic::Range) -> Range {
Range {
start: Position {
line: range.start.line_index as u32,
character: range.start.column_index as u32,
},
end: Position {
line: range.end.line_index as u32,
character: range.end.column_index as u32,
},
}
}
pub fn get_lint_references(
parsed_source: &deno_ast::ParsedSource,
maybe_lint_config: Option<&LintConfig>,
) -> Result, AnyError> {
let syntax = deno_ast::get_syntax(parsed_source.media_type());
let lint_rules =
get_configured_rules(maybe_lint_config, vec![], vec![], vec![])?;
let linter = create_linter(syntax, lint_rules);
// TODO(dsherret): do not re-parse here again
let (_, lint_diagnostics) = linter.lint(
parsed_source.specifier().to_string(),
parsed_source.source().text_str().to_string(),
)?;
Ok(
lint_diagnostics
.into_iter()
.map(|d| Reference {
category: Category::Lint {
message: d.message,
code: d.code,
hint: d.hint,
},
range: as_lsp_range(&d.range),
})
.collect(),
)
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Dependency {
pub is_dynamic: bool,
pub maybe_code: Option,
pub maybe_code_specifier_range: Option,
pub maybe_type: Option,
pub maybe_type_specifier_range: Option,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResolvedDependencyErr {
InvalidDowngrade,
InvalidLocalImport,
InvalidSpecifier(ModuleResolutionError),
Missing,
}
impl ResolvedDependencyErr {
pub fn as_code(&self) -> lsp::NumberOrString {
match self {
Self::InvalidDowngrade => {
lsp::NumberOrString::String("invalid-downgrade".to_string())
}
Self::InvalidLocalImport => {
lsp::NumberOrString::String("invalid-local-import".to_string())
}
Self::InvalidSpecifier(error) => match error {
ModuleResolutionError::ImportPrefixMissing(_, _) => {
lsp::NumberOrString::String("import-prefix-missing".to_string())
}
ModuleResolutionError::InvalidBaseUrl(_) => {
lsp::NumberOrString::String("invalid-base-url".to_string())
}
ModuleResolutionError::InvalidPath(_) => {
lsp::NumberOrString::String("invalid-path".to_string())
}
ModuleResolutionError::InvalidUrl(_) => {
lsp::NumberOrString::String("invalid-url".to_string())
}
},
Self::Missing => lsp::NumberOrString::String("missing".to_string()),
}
}
}
impl fmt::Display for ResolvedDependencyErr {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::InvalidDowngrade => {
write!(f, "HTTPS modules cannot import HTTP modules.")
}
Self::InvalidLocalImport => {
write!(f, "Remote modules cannot import local modules.")
}
Self::InvalidSpecifier(err) => write!(f, "{}", err),
Self::Missing => write!(f, "The module is unexpectedly missing."),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResolvedDependency {
Resolved(ModuleSpecifier),
Err(ResolvedDependencyErr),
}
impl ResolvedDependency {
pub fn as_hover_text(&self) -> String {
match self {
Self::Resolved(specifier) => match specifier.scheme() {
"data" => "_(a data url)_".to_string(),
"blob" => "_(a blob url)_".to_string(),
_ => format!(
"{}{}",
specifier[..url::Position::AfterScheme].to_string(),
specifier[url::Position::AfterScheme..].to_string()
),
},
Self::Err(_) => "_[errored]_".to_string(),
}
}
}
pub fn resolve_import(
specifier: &str,
referrer: &ModuleSpecifier,
maybe_import_map: &Option,
) -> ResolvedDependency {
let maybe_mapped = if let Some(import_map) = maybe_import_map {
import_map.resolve(specifier, referrer.as_str()).ok()
} else {
None
};
let remapped = maybe_mapped.is_some();
let specifier = if let Some(remapped) = maybe_mapped {
remapped
} else {
match deno_core::resolve_import(specifier, referrer.as_str()) {
Ok(resolved) => resolved,
Err(err) => {
return ResolvedDependency::Err(
ResolvedDependencyErr::InvalidSpecifier(err),
)
}
}
};
let referrer_scheme = referrer.scheme();
let specifier_scheme = specifier.scheme();
if referrer_scheme == "https" && specifier_scheme == "http" {
return ResolvedDependency::Err(ResolvedDependencyErr::InvalidDowngrade);
}
if (referrer_scheme == "https" || referrer_scheme == "http")
&& !(specifier_scheme == "https" || specifier_scheme == "http")
&& !remapped
{
return ResolvedDependency::Err(ResolvedDependencyErr::InvalidLocalImport);
}
ResolvedDependency::Resolved(specifier)
}
pub fn parse_module(
specifier: &ModuleSpecifier,
source: SourceTextInfo,
media_type: MediaType,
) -> Result {
deno_ast::parse_module(deno_ast::ParseParams {
specifier: specifier.as_str().to_string(),
source,
media_type,
// capture the tokens for linting and formatting
capture_tokens: true,
maybe_syntax: None,
})
}
// TODO(@kitsonk) a lot of this logic is duplicated in module_graph.rs in
// Module::parse() and should be refactored out to a common function.
pub fn analyze_dependencies(
specifier: &ModuleSpecifier,
media_type: MediaType,
parsed_source: &deno_ast::ParsedSource,
maybe_import_map: &Option,
) -> (HashMap, Option) {
let mut maybe_type = None;
let mut dependencies = HashMap::::new();
// Parse leading comments for supported triple slash references.
for comment in parsed_source.get_leading_comments().iter() {
if let Some((ts_reference, span)) = parse_ts_reference(comment) {
let loc = parsed_source.source().line_and_column_index(span.lo);
match ts_reference {
TypeScriptReference::Path(import) => {
let dep = dependencies.entry(import.clone()).or_default();
let resolved_import =
resolve_import(&import, specifier, maybe_import_map);
dep.maybe_code = Some(resolved_import);
dep.maybe_code_specifier_range = Some(Range {
start: Position {
line: loc.line_index as u32,
character: loc.column_index as u32,
},
end: Position {
line: loc.line_index as u32,
character: (loc.column_index + import.chars().count() + 2) as u32,
},
});
}
TypeScriptReference::Types(import) => {
let resolved_import =
resolve_import(&import, specifier, maybe_import_map);
if media_type == MediaType::JavaScript || media_type == MediaType::Jsx
{
maybe_type = Some(resolved_import.clone());
}
let dep = dependencies.entry(import.clone()).or_default();
dep.maybe_type = Some(resolved_import);
dep.maybe_type_specifier_range = Some(Range {
start: Position {
line: loc.line_index as u32,
character: loc.column_index as u32,
},
end: Position {
line: loc.line_index as u32,
character: (loc.column_index + import.chars().count() + 2) as u32,
},
});
}
}
}
}
// Parse ES and type only imports
let descriptors = deno_graph::analyze_dependencies(parsed_source);
for desc in descriptors.into_iter().filter(|desc| {
desc.kind != deno_ast::swc::dep_graph::DependencyKind::Require
}) {
let resolved_import =
resolve_import(&desc.specifier, specifier, maybe_import_map);
let maybe_resolved_type_dependency =
// Check for `@deno-types` pragmas that affect the import
if let Some(comment) = desc.leading_comments.last() {
parse_deno_types(comment).as_ref().map(|(deno_types, span)| {
(
resolve_import(deno_types, specifier, maybe_import_map),
deno_types.clone(),
parsed_source.source().line_and_column_index(span.lo)
)
})
} else {
None
};
let dep = dependencies.entry(desc.specifier.to_string()).or_default();
dep.is_dynamic = desc.is_dynamic;
let start = parsed_source
.source()
.line_and_column_index(desc.specifier_span.lo);
let end = parsed_source
.source()
.line_and_column_index(desc.specifier_span.hi);
let range = Range {
start: Position {
line: start.line_index as u32,
character: start.column_index as u32,
},
end: Position {
line: end.line_index as u32,
character: end.column_index as u32,
},
};
dep.maybe_code_specifier_range = Some(range);
dep.maybe_code = Some(resolved_import);
if dep.maybe_type.is_none() {
if let Some((resolved_dependency, specifier, loc)) =
maybe_resolved_type_dependency
{
dep.maybe_type_specifier_range = Some(Range {
start: Position {
line: loc.line_index as u32,
// +1 to skip quote
character: (loc.column_index + 1) as u32,
},
end: Position {
line: loc.line_index as u32,
// +1 to skip quote
character: (loc.column_index + 1 + specifier.chars().count())
as u32,
},
});
dep.maybe_type = Some(resolved_dependency);
}
}
}
(dependencies, maybe_type)
}
fn code_as_string(code: &Option) -> String {
match code {
Some(lsp::NumberOrString::String(str)) => str.clone(),
Some(lsp::NumberOrString::Number(num)) => num.to_string(),
_ => "".to_string(),
}
}
/// Iterate over the supported extensions, concatenating the extension on the
/// specifier, returning the first specifier that is resolve-able, otherwise
/// None if none match.
fn check_specifier(
specifier: &str,
referrer: &ModuleSpecifier,
snapshot: &language_server::StateSnapshot,
maybe_import_map: &Option,
) -> Option {
for ext in SUPPORTED_EXTENSIONS {
let specifier_with_ext = format!("{}{}", specifier, ext);
if let ResolvedDependency::Resolved(resolved_specifier) =
resolve_import(&specifier_with_ext, referrer, maybe_import_map)
{
if snapshot.documents.contains_key(&resolved_specifier)
|| snapshot.sources.contains_key(&resolved_specifier)
{
return Some(specifier_with_ext);
}
}
}
None
}
/// For a set of tsc changes, can them for any that contain something that looks
/// like an import and rewrite the import specifier to include the extension
pub(crate) fn fix_ts_import_changes(
referrer: &ModuleSpecifier,
changes: &[tsc::FileTextChanges],
language_server: &language_server::Inner,
) -> Result, AnyError> {
let mut r = Vec::new();
let snapshot = language_server.snapshot()?;
for change in changes {
let mut text_changes = Vec::new();
for text_change in &change.text_changes {
if let Some(captures) =
IMPORT_SPECIFIER_RE.captures(&text_change.new_text)
{
let specifier = captures
.get(1)
.ok_or_else(|| anyhow!("Missing capture."))?
.as_str();
if let Some(new_specifier) = check_specifier(
specifier,
referrer,
&snapshot,
&language_server.maybe_import_map,
) {
let new_text =
text_change.new_text.replace(specifier, &new_specifier);
text_changes.push(tsc::TextChange {
span: text_change.span.clone(),
new_text,
});
} else {
text_changes.push(text_change.clone());
}
} else {
text_changes.push(text_change.clone());
}
}
r.push(tsc::FileTextChanges {
file_name: change.file_name.clone(),
text_changes,
is_new_file: change.is_new_file,
});
}
Ok(r)
}
/// Fix tsc import code actions so that the module specifier is correct for
/// resolution by Deno (includes the extension).
fn fix_ts_import_action(
referrer: &ModuleSpecifier,
action: &tsc::CodeFixAction,
language_server: &language_server::Inner,
) -> Result {
if action.fix_name == "import" {
let change = action
.changes
.get(0)
.ok_or_else(|| anyhow!("Unexpected action changes."))?;
let text_change = change
.text_changes
.get(0)
.ok_or_else(|| anyhow!("Missing text change."))?;
if let Some(captures) = IMPORT_SPECIFIER_RE.captures(&text_change.new_text)
{
let specifier = captures
.get(1)
.ok_or_else(|| anyhow!("Missing capture."))?
.as_str();
let snapshot = language_server.snapshot()?;
if let Some(new_specifier) = check_specifier(
specifier,
referrer,
&snapshot,
&language_server.maybe_import_map,
) {
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();
return Ok(tsc::CodeFixAction {
description,
changes,
commands: None,
fix_name: action.fix_name.clone(),
fix_id: None,
fix_all_description: None,
});
}
}
}
Ok(action.clone())
}
/// Determines if two TypeScript diagnostic codes are effectively equivalent.
fn is_equivalent_code(
a: &Option,
b: &Option,
) -> 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())
}
/// Return a boolean flag to indicate if the specified action is the preferred
/// action for a given set of actions.
fn is_preferred(
action: &tsc::CodeFixAction,
actions: &[CodeActionKind],
fix_priority: u32,
only_one: bool,
) -> bool {
actions.iter().all(|i| {
if let CodeActionKind::Tsc(_, a) = i {
if action == a {
return true;
}
if a.fix_id.is_some() {
return true;
}
if let Some((other_fix_priority, _)) =
PREFERRED_FIXES.get(a.fix_name.as_str())
{
match other_fix_priority.cmp(&fix_priority) {
Ordering::Less => return true,
Ordering::Greater => return false,
Ordering::Equal => (),
}
if only_one && action.fix_name == a.fix_name {
return false;
}
}
true
} else {
true
}
})
}
/// Convert changes returned from a TypeScript quick fix action into edits
/// for an LSP CodeAction.
pub(crate) async fn ts_changes_to_edit(
changes: &[tsc::FileTextChanges],
language_server: &mut language_server::Inner,
) -> Result