// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use std::collections::HashSet; use std::sync::Arc; use deno_ast::swc::common::comments::CommentKind; use deno_ast::ParsedSource; use deno_ast::SourceRangedForSpanned; use deno_ast::SourceTextInfo; use deno_core::error::AnyError; use deno_core::url::Url; use deno_graph::ModuleEntryRef; use deno_graph::ModuleGraph; use deno_graph::ResolutionResolved; use deno_graph::WalkOptions; use deno_semver::jsr::JsrPackageReqReference; use deno_semver::npm::NpmPackageReqReference; use crate::cache::ParsedSourceCache; use super::diagnostics::PublishDiagnostic; use super::diagnostics::PublishDiagnosticsCollector; pub struct GraphDiagnosticsCollector { parsed_source_cache: Arc, } impl GraphDiagnosticsCollector { pub fn new(parsed_source_cache: Arc) -> Self { Self { parsed_source_cache, } } pub fn collect_diagnostics_for_graph( &self, graph: &ModuleGraph, diagnostics_collector: &PublishDiagnosticsCollector, ) -> Result<(), AnyError> { let mut visited = HashSet::new(); let mut skip_specifiers: HashSet = HashSet::new(); let mut collect_if_invalid = |skip_specifiers: &mut HashSet, source_text: &Arc, specifier_text: &str, resolution: &ResolutionResolved| { if visited.insert(resolution.specifier.clone()) { match resolution.specifier.scheme() { "file" | "data" | "node" | "bun" => {} "jsr" => { skip_specifiers.insert(resolution.specifier.clone()); // check for a missing version constraint if let Ok(jsr_req_ref) = JsrPackageReqReference::from_specifier(&resolution.specifier) { if jsr_req_ref.req().version_req.version_text() == "*" { let maybe_version = graph .packages .mappings() .get(jsr_req_ref.req()) .map(|nv| nv.version.clone()); diagnostics_collector.push( PublishDiagnostic::MissingConstraint { specifier: resolution.specifier.clone(), specifier_text: specifier_text.to_string(), resolved_version: maybe_version, text_info: SourceTextInfo::new(source_text.clone()), referrer: resolution.range.clone(), }, ); } } } "npm" => { skip_specifiers.insert(resolution.specifier.clone()); // check for a missing version constraint if let Ok(jsr_req_ref) = NpmPackageReqReference::from_specifier(&resolution.specifier) { if jsr_req_ref.req().version_req.version_text() == "*" { let maybe_version = graph .get(&resolution.specifier) .and_then(|m| m.npm()) .map(|n| n.nv_reference.nv().version.clone()); diagnostics_collector.push( PublishDiagnostic::MissingConstraint { specifier: resolution.specifier.clone(), specifier_text: specifier_text.to_string(), resolved_version: maybe_version, text_info: SourceTextInfo::new(source_text.clone()), referrer: resolution.range.clone(), }, ); } } } "http" | "https" => { skip_specifiers.insert(resolution.specifier.clone()); diagnostics_collector.push( PublishDiagnostic::InvalidExternalImport { kind: format!("non-JSR '{}'", resolution.specifier.scheme()), text_info: SourceTextInfo::new(source_text.clone()), imported: resolution.specifier.clone(), referrer: resolution.range.clone(), }, ); } _ => { skip_specifiers.insert(resolution.specifier.clone()); diagnostics_collector.push( PublishDiagnostic::InvalidExternalImport { kind: format!("'{}'", resolution.specifier.scheme()), text_info: SourceTextInfo::new(source_text.clone()), imported: resolution.specifier.clone(), referrer: resolution.range.clone(), }, ); } } } }; let options = WalkOptions { check_js: true, follow_dynamic: true, // search the entire graph and not just the fast check subset prefer_fast_check_graph: false, kind: deno_graph::GraphKind::All, }; let mut iter = graph.walk(graph.roots.iter(), options); while let Some((specifier, entry)) = iter.next() { if skip_specifiers.contains(specifier) { iter.skip_previous_dependencies(); continue; } let ModuleEntryRef::Module(module) = entry else { continue; }; let Some(module) = module.js() else { continue; }; let parsed_source = self .parsed_source_cache .get_parsed_source_from_js_module(module)?; // surface syntax errors for diagnostic in parsed_source.diagnostics() { diagnostics_collector .push(PublishDiagnostic::SyntaxError(diagnostic.clone())); } check_for_banned_triple_slash_directives( &parsed_source, diagnostics_collector, ); for (specifier_text, dep) in &module.dependencies { if let Some(resolved) = dep.maybe_code.ok() { collect_if_invalid( &mut skip_specifiers, &module.source, specifier_text, resolved, ); } if let Some(resolved) = dep.maybe_type.ok() { collect_if_invalid( &mut skip_specifiers, &module.source, specifier_text, resolved, ); } } } Ok(()) } } fn check_for_banned_triple_slash_directives( parsed_source: &ParsedSource, diagnostics_collector: &PublishDiagnosticsCollector, ) { let triple_slash_re = lazy_regex::regex!( r#"^/\s+\s*$"# ); let Some(comments) = parsed_source.get_leading_comments() else { return; }; for comment in comments { if comment.kind != CommentKind::Line { continue; } if triple_slash_re.is_match(&comment.text) { diagnostics_collector.push( PublishDiagnostic::BannedTripleSlashDirectives { specifier: parsed_source.specifier().clone(), range: comment.range(), text_info: parsed_source.text_info_lazy().clone(), }, ); } } }