fix(check): npm resolution errors to tsc diagnostics (#28174)

Closes https://github.com/denoland/deno/issues/27188
This commit is contained in:
David Sherret 2025-02-18 16:44:49 -05:00 committed by GitHub
parent 3747d2759a
commit f62fc9e81f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 196 additions and 69 deletions

View file

@ -40,6 +40,7 @@ use deno_runtime::deno_permissions::PermissionsContainer;
use deno_semver::jsr::JsrDepPackageReq; use deno_semver::jsr::JsrDepPackageReq;
use deno_semver::package::PackageNv; use deno_semver::package::PackageNv;
use deno_semver::SmallStackString; use deno_semver::SmallStackString;
use sys_traits::FsMetadata;
use crate::args::config_to_deno_graph_workspace_member; use crate::args::config_to_deno_graph_workspace_member;
use crate::args::deno_json::TsConfigResolver; use crate::args::deno_json::TsConfigResolver;
@ -151,6 +152,25 @@ pub fn graph_walk_errors<'a>(
roots: &'a [ModuleSpecifier], roots: &'a [ModuleSpecifier],
options: GraphWalkErrorsOptions<'a>, options: GraphWalkErrorsOptions<'a>,
) -> impl Iterator<Item = JsErrorBox> + 'a { ) -> impl Iterator<Item = JsErrorBox> + 'a {
fn should_ignore_error(
sys: &CliSys,
graph_kind: GraphKind,
error: &ModuleGraphError,
) -> bool {
if graph_kind == GraphKind::TypesOnly
&& matches!(
error,
ModuleGraphError::ModuleError(ModuleError::UnsupportedMediaType(..))
)
{
return true;
}
// surface these as typescript diagnostics instead
graph_kind.include_types()
&& has_module_graph_error_for_tsc_diagnostic(sys, error)
}
graph graph
.walk( .walk(
roots.iter(), roots.iter(),
@ -163,6 +183,11 @@ pub fn graph_walk_errors<'a>(
) )
.errors() .errors()
.flat_map(|error| { .flat_map(|error| {
if should_ignore_error(sys, graph.graph_kind(), &error) {
log::debug!("Ignoring: {}", error);
return None;
}
let is_root = match &error { let is_root = match &error {
ModuleGraphError::ResolutionError(_) ModuleGraphError::ResolutionError(_)
| ModuleGraphError::TypesResolutionError(_) => false, | ModuleGraphError::TypesResolutionError(_) => false,
@ -180,32 +205,94 @@ pub fn graph_walk_errors<'a>(
}, },
); );
if graph.graph_kind() == GraphKind::TypesOnly
&& matches!(
error,
ModuleGraphError::ModuleError(ModuleError::UnsupportedMediaType(..))
)
{
log::debug!("Ignoring: {}", message);
return None;
}
if graph.graph_kind().include_types()
&& (message.contains(RUN_WITH_SLOPPY_IMPORTS_MSG)
|| matches!(
error,
ModuleGraphError::ModuleError(ModuleError::Missing(..))
))
{
// ignore and let typescript surface this as a diagnostic instead
log::debug!("Ignoring: {}", message);
return None;
}
Some(JsErrorBox::new(error.get_class(), message)) Some(JsErrorBox::new(error.get_class(), message))
}) })
} }
fn has_module_graph_error_for_tsc_diagnostic(
sys: &CliSys,
error: &ModuleGraphError,
) -> bool {
match error {
ModuleGraphError::ModuleError(error) => {
module_error_for_tsc_diagnostic(sys, error).is_some()
}
ModuleGraphError::ResolutionError(error) => {
resolution_error_for_tsc_diagnostic(error).is_some()
}
ModuleGraphError::TypesResolutionError(error) => {
resolution_error_for_tsc_diagnostic(error).is_some()
}
}
}
pub struct ModuleNotFoundGraphErrorRef<'a> {
pub specifier: &'a ModuleSpecifier,
pub maybe_range: Option<&'a deno_graph::Range>,
}
pub fn module_error_for_tsc_diagnostic<'a>(
sys: &CliSys,
error: &'a ModuleError,
) -> Option<ModuleNotFoundGraphErrorRef<'a>> {
match error {
ModuleError::Missing(specifier, maybe_range) => {
Some(ModuleNotFoundGraphErrorRef {
specifier,
maybe_range: maybe_range.as_ref(),
})
}
ModuleError::LoadingErr(
specifier,
maybe_range,
ModuleLoadError::Loader(_),
) => {
if let Ok(path) = deno_path_util::url_to_file_path(specifier) {
if sys.fs_is_dir_no_err(path) {
return Some(ModuleNotFoundGraphErrorRef {
specifier,
maybe_range: maybe_range.as_ref(),
});
}
}
None
}
_ => None,
}
}
pub struct ModuleNotFoundNodeResolutionErrorRef<'a> {
pub specifier: &'a str,
pub maybe_range: Option<&'a deno_graph::Range>,
}
pub fn resolution_error_for_tsc_diagnostic(
error: &ResolutionError,
) -> Option<ModuleNotFoundNodeResolutionErrorRef> {
match error {
ResolutionError::ResolverError {
error,
specifier,
range,
} => match error.as_ref() {
ResolveError::Other(error) => {
// would be nice if there were an easier way of doing this
let text = error.to_string();
if text.contains("[ERR_MODULE_NOT_FOUND]") {
Some(ModuleNotFoundNodeResolutionErrorRef {
specifier,
maybe_range: Some(range),
})
} else {
None
}
}
_ => None,
},
_ => None,
}
}
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum EnhanceGraphErrorMode { pub enum EnhanceGraphErrorMode {
ShowRange, ShowRange,

View file

@ -15,9 +15,7 @@ use deno_core::error::AnyError;
use deno_core::url::Url; use deno_core::url::Url;
use deno_error::JsErrorBox; use deno_error::JsErrorBox;
use deno_graph::Module; use deno_graph::Module;
use deno_graph::ModuleError;
use deno_graph::ModuleGraph; use deno_graph::ModuleGraph;
use deno_graph::ModuleLoadError;
use deno_lib::util::hash::FastInsecureHasher; use deno_lib::util::hash::FastInsecureHasher;
use deno_semver::npm::NpmPackageNvReference; use deno_semver::npm::NpmPackageNvReference;
use deno_terminal::colors; use deno_terminal::colors;
@ -38,6 +36,8 @@ use crate::cache::Caches;
use crate::cache::TypeCheckCache; use crate::cache::TypeCheckCache;
use crate::factory::CliFactory; use crate::factory::CliFactory;
use crate::graph_util::maybe_additional_sloppy_imports_message; use crate::graph_util::maybe_additional_sloppy_imports_message;
use crate::graph_util::module_error_for_tsc_diagnostic;
use crate::graph_util::resolution_error_for_tsc_diagnostic;
use crate::graph_util::BuildFastCheckGraphOptions; use crate::graph_util::BuildFastCheckGraphOptions;
use crate::graph_util::ModuleGraphBuilder; use crate::graph_util::ModuleGraphBuilder;
use crate::node::CliNodeResolver; use crate::node::CliNodeResolver;
@ -722,7 +722,7 @@ impl<'a> GraphWalker<'a> {
self self
.missing_diagnostics .missing_diagnostics
.push(tsc::Diagnostic::from_missing_error( .push(tsc::Diagnostic::from_missing_error(
specifier, specifier.as_str(),
None, None,
maybe_additional_sloppy_imports_message(self.sys, specifier), maybe_additional_sloppy_imports_message(self.sys, specifier),
)); ));
@ -763,36 +763,23 @@ impl<'a> GraphWalker<'a> {
let module = match self.graph.try_get(specifier) { let module = match self.graph.try_get(specifier) {
Ok(Some(module)) => module, Ok(Some(module)) => module,
Ok(None) => continue, Ok(None) => continue,
Err(ModuleError::Missing(specifier, maybe_range)) => { Err(err) => {
if !is_dynamic { if !is_dynamic {
self if let Some(err) = module_error_for_tsc_diagnostic(self.sys, err) {
.missing_diagnostics self.missing_diagnostics.push(
.push(tsc::Diagnostic::from_missing_error( tsc::Diagnostic::from_missing_error(
specifier, err.specifier.as_str(),
maybe_range.as_ref(), err.maybe_range,
maybe_additional_sloppy_imports_message(self.sys, specifier), maybe_additional_sloppy_imports_message(
)); self.sys,
err.specifier,
),
),
);
}
} }
continue; continue;
} }
Err(ModuleError::LoadingErr(
specifier,
maybe_range,
ModuleLoadError::Loader(_),
)) => {
// these will be errors like attempting to load a directory
if !is_dynamic {
self
.missing_diagnostics
.push(tsc::Diagnostic::from_missing_error(
specifier,
maybe_range.as_ref(),
maybe_additional_sloppy_imports_message(self.sys, specifier),
));
}
continue;
}
Err(_) => continue,
}; };
if is_dynamic && !self.seen.insert(specifier) { if is_dynamic && !self.seen.insert(specifier) {
continue; continue;
@ -831,11 +818,26 @@ impl<'a> GraphWalker<'a> {
if let Some(deps) = maybe_module_dependencies { if let Some(deps) = maybe_module_dependencies {
for dep in deps.values() { for dep in deps.values() {
// walk both the code and type dependencies // walk both the code and type dependencies
if let Some(specifier) = dep.get_code() { for resolution in [&dep.maybe_type, &dep.maybe_code] {
self.handle_specifier(specifier, dep.is_dynamic); match resolution {
} deno_graph::Resolution::Ok(resolution) => {
if let Some(specifier) = dep.get_type() { self.handle_specifier(&resolution.specifier, dep.is_dynamic);
self.handle_specifier(specifier, dep.is_dynamic); }
deno_graph::Resolution::Err(resolution_error) => {
if let Some(err) =
resolution_error_for_tsc_diagnostic(resolution_error)
{
self.missing_diagnostics.push(
tsc::Diagnostic::from_missing_error(
err.specifier,
err.maybe_range,
None,
),
);
}
}
deno_graph::Resolution::None => {}
}
} }
} }
} }

View file

@ -152,7 +152,7 @@ pub struct Diagnostic {
impl Diagnostic { impl Diagnostic {
pub fn from_missing_error( pub fn from_missing_error(
specifier: &ModuleSpecifier, specifier: &str,
maybe_range: Option<&deno_graph::Range>, maybe_range: Option<&deno_graph::Range>,
additional_message: Option<String>, additional_message: Option<String>,
) -> Self { ) -> Self {

View file

@ -20,6 +20,7 @@ use sys_traits::FsMetadata;
use sys_traits::FsRead; use sys_traits::FsRead;
use url::Url; use url::Url;
use crate::errors::ModuleNotFoundError;
use crate::resolution::NodeResolverRc; use crate::resolution::NodeResolverRc;
use crate::InNpmPackageChecker; use crate::InNpmPackageChecker;
use crate::IsBuiltInNodeModuleChecker; use crate::IsBuiltInNodeModuleChecker;
@ -473,7 +474,12 @@ impl<
last = parent; last = parent;
} }
Err(not_found(specifier, referrer_path)) Err(JsErrorBox::from_err(ModuleNotFoundError {
specifier: UrlOrPath::Path(PathBuf::from(specifier)),
typ: "module",
maybe_referrer: Some(UrlOrPath::Path(referrer_path.to_path_buf())),
suggested_ext: None,
}))
} }
fn file_extension_probe( fn file_extension_probe(
@ -509,7 +515,12 @@ impl<
} }
} }
} }
Err(not_found(&p.to_string_lossy(), referrer)) Err(JsErrorBox::from_err(ModuleNotFoundError {
specifier: UrlOrPath::Path(p),
maybe_referrer: Some(UrlOrPath::Path(referrer.to_path_buf())),
typ: "module",
suggested_ext: None,
}))
} }
} }
@ -670,15 +681,6 @@ fn parse_specifier(specifier: &str) -> Option<(String, String)> {
Some((package_name, package_subpath)) Some((package_name, package_subpath))
} }
fn not_found(path: &str, referrer: &Path) -> JsErrorBox {
let msg = format!(
"[ERR_MODULE_NOT_FOUND] Cannot find module \"{}\" imported from \"{}\"",
path,
referrer.to_string_lossy()
);
JsErrorBox::from_err(std::io::Error::new(std::io::ErrorKind::NotFound, msg))
}
fn to_double_quote_string(text: &str) -> String { fn to_double_quote_string(text: &str) -> String {
// serde can handle this for us // serde can handle this for us
serde_json::to_string(text).unwrap() serde_json::to_string(text).unwrap()

View file

@ -0,0 +1,5 @@
{
"args": "check main.ts",
"output": "check.out",
"exitCode": 1
}

View file

@ -0,0 +1,10 @@
Check file:///[WILDLINE]/main.ts
TS2307 [ERROR]: Cannot find module 'package'.
at file:///[WILDLINE]/main.ts:1:22
TS2307 [ERROR]: Cannot find module 'package/missing-js'.
at file:///[WILDLINE]/main.ts:2:23
Found 2 errors.
error: Type checking failed.

View file

@ -0,0 +1,4 @@
import { Test } from "package";
import { Other } from "package/missing-js";
console.log(Test, Other);

View file

@ -0,0 +1,12 @@
{
"exports": {
".": {
"types": "./types.d.ts",
"default": "./index.js"
},
"./missing-js": {
"types": "./missing.d.ts",
"default": "./missing.js"
}
}
}

View file

@ -0,0 +1,5 @@
{
"dependencies": {
"package": "*"
}
}

View file

@ -1,3 +1,3 @@
[# Since this package has multiple cjs export errors and the resolution is done in ] [# Since this package has multiple cjs export errors and the resolution is done in ]
[# parallel, we want to ensure the output is deterministic when there's multiple errors] [# parallel, we want to ensure the output is deterministic when there's multiple errors]
error: [ERR_MODULE_NOT_FOUND] Cannot find module "[WILDLINE]not_exists.js" imported from "[WILDLINE]a.js" error: [ERR_MODULE_NOT_FOUND] Cannot find module '[WILDLINE]not_exists.js' imported from '[WILDLINE]a.js'