summaryrefslogtreecommitdiff
path: root/cli/tools/check.rs
diff options
context:
space:
mode:
authorDavid Sherret <dsherret@users.noreply.github.com>2022-09-02 10:54:40 -0400
committerGitHub <noreply@github.com>2022-09-02 10:54:40 -0400
commit387300aed0133e369af090d3795a1fce89603737 (patch)
tree63061cf6bb6853504f507771af91c9f2682880c4 /cli/tools/check.rs
parente719a02bb0a4cc39e11a945dcff83422440e50d4 (diff)
refactor: extract out check code from emit (#15729)
Closes #15535
Diffstat (limited to 'cli/tools/check.rs')
-rw-r--r--cli/tools/check.rs369
1 files changed, 369 insertions, 0 deletions
diff --git a/cli/tools/check.rs b/cli/tools/check.rs
new file mode 100644
index 000000000..bb0b873f4
--- /dev/null
+++ b/cli/tools/check.rs
@@ -0,0 +1,369 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+use std::sync::Arc;
+
+use deno_ast::MediaType;
+use deno_ast::ModuleSpecifier;
+use deno_core::error::AnyError;
+use deno_core::parking_lot::RwLock;
+use deno_graph::ModuleKind;
+use deno_runtime::colors;
+use once_cell::sync::Lazy;
+use regex::Regex;
+
+use crate::args::TsConfig;
+use crate::args::TypeCheckMode;
+use crate::cache::FastInsecureHasher;
+use crate::cache::TypeCheckCache;
+use crate::diagnostics::Diagnostics;
+use crate::graph_util::GraphData;
+use crate::graph_util::ModuleEntry;
+use crate::tsc;
+use crate::tsc::Stats;
+use crate::version;
+
+/// Options for performing a check of a module graph. Note that the decision to
+/// emit or not is determined by the `ts_config` settings.
+pub struct CheckOptions {
+ /// The check flag from the option which can effect the filtering of
+ /// diagnostics in the emit result.
+ pub type_check_mode: TypeCheckMode,
+ /// Set the debug flag on the TypeScript type checker.
+ pub debug: bool,
+ /// The module specifier to the configuration file, passed to tsc so that
+ /// configuration related diagnostics are properly formed.
+ pub maybe_config_specifier: Option<ModuleSpecifier>,
+ /// The derived tsconfig that should be used when checking.
+ pub ts_config: TsConfig,
+ /// If true, `Check <specifier>` will be written to stdout for each root.
+ pub log_checks: bool,
+ /// If true, valid `.tsbuildinfo` files will be ignored and type checking
+ /// will always occur.
+ pub reload: bool,
+}
+
+/// The result of a check of a module graph.
+#[derive(Debug, Default)]
+pub struct CheckResult {
+ pub diagnostics: Diagnostics,
+ pub stats: Stats,
+}
+
+/// Given a set of roots and graph data, type check the module graph.
+///
+/// It is expected that it is determined if a check and/or emit is validated
+/// before the function is called.
+pub fn check(
+ roots: &[(ModuleSpecifier, ModuleKind)],
+ graph_data: Arc<RwLock<GraphData>>,
+ cache: &TypeCheckCache,
+ options: CheckOptions,
+) -> Result<CheckResult, AnyError> {
+ let check_js = options.ts_config.get_check_js();
+ let segment_graph_data = {
+ let graph_data = graph_data.read();
+ graph_data.graph_segment(roots).unwrap()
+ };
+ let check_hash = match get_check_hash(&segment_graph_data, &options) {
+ CheckHashResult::NoFiles => return Ok(Default::default()),
+ CheckHashResult::Hash(hash) => hash,
+ };
+
+ // do not type check if we know this is type checked
+ if !options.reload && cache.has_check_hash(check_hash) {
+ return Ok(Default::default());
+ }
+
+ let root_names = get_tsc_roots(&segment_graph_data, check_js);
+ if options.log_checks {
+ for (root, _) in roots {
+ let root_str = root.to_string();
+ // `$deno` specifiers are internal, don't print them.
+ if !root_str.contains("$deno") {
+ log::info!("{} {}", colors::green("Check"), root);
+ }
+ }
+ }
+ // while there might be multiple roots, we can't "merge" the build info, so we
+ // try to retrieve the build info for first root, which is the most common use
+ // case.
+ let maybe_tsbuildinfo = if options.reload {
+ None
+ } else {
+ cache.get_tsbuildinfo(&roots[0].0)
+ };
+ // to make tsc build info work, we need to consistently hash modules, so that
+ // tsc can better determine if an emit is still valid or not, so we provide
+ // that data here.
+ let hash_data = vec![
+ options.ts_config.as_bytes(),
+ version::deno().as_bytes().to_owned(),
+ ];
+
+ let response = tsc::exec(tsc::Request {
+ config: options.ts_config,
+ debug: options.debug,
+ graph_data,
+ hash_data,
+ maybe_config_specifier: options.maybe_config_specifier,
+ maybe_tsbuildinfo,
+ root_names,
+ })?;
+
+ let diagnostics = if options.type_check_mode == TypeCheckMode::Local {
+ response.diagnostics.filter(|d| {
+ if let Some(file_name) = &d.file_name {
+ !file_name.starts_with("http")
+ } else {
+ true
+ }
+ })
+ } else {
+ response.diagnostics
+ };
+
+ if let Some(tsbuildinfo) = response.maybe_tsbuildinfo {
+ cache.set_tsbuildinfo(&roots[0].0, &tsbuildinfo);
+ }
+
+ if diagnostics.is_empty() {
+ cache.add_check_hash(check_hash);
+ }
+
+ Ok(CheckResult {
+ diagnostics,
+ stats: response.stats,
+ })
+}
+
+enum CheckHashResult {
+ Hash(u64),
+ NoFiles,
+}
+
+/// Gets a hash of the inputs for type checking. This can then
+/// be used to tell
+fn get_check_hash(
+ graph_data: &GraphData,
+ options: &CheckOptions,
+) -> CheckHashResult {
+ let mut hasher = FastInsecureHasher::new();
+ hasher.write_u8(match options.type_check_mode {
+ TypeCheckMode::All => 0,
+ TypeCheckMode::Local => 1,
+ TypeCheckMode::None => 2,
+ });
+ hasher.write(&options.ts_config.as_bytes());
+
+ let check_js = options.ts_config.get_check_js();
+ let mut sorted_entries = graph_data.entries().collect::<Vec<_>>();
+ sorted_entries.sort_by_key(|(s, _)| s.as_str()); // make it deterministic
+ let mut has_file = false;
+ let mut has_file_to_type_check = false;
+ for (specifier, module_entry) in sorted_entries {
+ if let ModuleEntry::Module {
+ code, media_type, ..
+ } = module_entry
+ {
+ let ts_check = has_ts_check(*media_type, code);
+ if ts_check {
+ has_file_to_type_check = true;
+ }
+
+ match media_type {
+ MediaType::TypeScript
+ | MediaType::Dts
+ | MediaType::Dmts
+ | MediaType::Dcts
+ | MediaType::Mts
+ | MediaType::Cts
+ | MediaType::Tsx => {
+ has_file = true;
+ has_file_to_type_check = true;
+ }
+ MediaType::JavaScript
+ | MediaType::Mjs
+ | MediaType::Cjs
+ | MediaType::Jsx => {
+ has_file = true;
+ if !check_js && !ts_check {
+ continue;
+ }
+ }
+ MediaType::Json
+ | MediaType::TsBuildInfo
+ | MediaType::SourceMap
+ | MediaType::Wasm
+ | MediaType::Unknown => continue,
+ }
+ hasher.write_str(specifier.as_str());
+ hasher.write_str(code);
+ }
+ }
+
+ if !has_file || !check_js && !has_file_to_type_check {
+ // no files to type check
+ CheckHashResult::NoFiles
+ } else {
+ CheckHashResult::Hash(hasher.finish())
+ }
+}
+
+/// Transform the graph into root specifiers that we can feed `tsc`. We have to
+/// provide the media type for root modules because `tsc` does not "resolve" the
+/// media type like other modules, as well as a root specifier needs any
+/// redirects resolved. We need to include all the emittable files in
+/// the roots, so they get type checked and optionally emitted,
+/// otherwise they would be ignored if only imported into JavaScript.
+fn get_tsc_roots(
+ graph_data: &GraphData,
+ check_js: bool,
+) -> Vec<(ModuleSpecifier, MediaType)> {
+ graph_data
+ .entries()
+ .into_iter()
+ .filter_map(|(specifier, module_entry)| match module_entry {
+ ModuleEntry::Module {
+ media_type, code, ..
+ } => match media_type {
+ MediaType::TypeScript
+ | MediaType::Tsx
+ | MediaType::Mts
+ | MediaType::Cts
+ | MediaType::Jsx => Some((specifier.clone(), *media_type)),
+ MediaType::JavaScript | MediaType::Mjs | MediaType::Cjs
+ if check_js || has_ts_check(*media_type, code) =>
+ {
+ Some((specifier.clone(), *media_type))
+ }
+ _ => None,
+ },
+ _ => None,
+ })
+ .collect()
+}
+
+/// Matches the `@ts-check` pragma.
+static TS_CHECK_RE: Lazy<Regex> =
+ Lazy::new(|| Regex::new(r#"(?i)^\s*@ts-check(?:\s+|$)"#).unwrap());
+
+fn has_ts_check(media_type: MediaType, file_text: &str) -> bool {
+ match &media_type {
+ MediaType::JavaScript
+ | MediaType::Mjs
+ | MediaType::Cjs
+ | MediaType::Jsx => get_leading_comments(file_text)
+ .iter()
+ .any(|text| TS_CHECK_RE.is_match(text)),
+ _ => false,
+ }
+}
+
+fn get_leading_comments(file_text: &str) -> Vec<String> {
+ let mut chars = file_text.chars().peekable();
+
+ // skip over the shebang
+ if file_text.starts_with("#!") {
+ // skip until the end of the line
+ for c in chars.by_ref() {
+ if c == '\n' {
+ break;
+ }
+ }
+ }
+
+ let mut results = Vec::new();
+ // now handle the comments
+ while chars.peek().is_some() {
+ // skip over any whitespace
+ while chars
+ .peek()
+ .map(|c| char::is_whitespace(*c))
+ .unwrap_or(false)
+ {
+ chars.next();
+ }
+
+ if chars.next() != Some('/') {
+ break;
+ }
+ match chars.next() {
+ Some('/') => {
+ let mut text = String::new();
+ for c in chars.by_ref() {
+ if c == '\n' {
+ break;
+ } else {
+ text.push(c);
+ }
+ }
+ results.push(text);
+ }
+ Some('*') => {
+ let mut text = String::new();
+ while let Some(c) = chars.next() {
+ if c == '*' && chars.peek() == Some(&'/') {
+ chars.next();
+ break;
+ } else {
+ text.push(c);
+ }
+ }
+ results.push(text);
+ }
+ _ => break,
+ }
+ }
+ results
+}
+
+#[cfg(test)]
+mod test {
+ use deno_ast::MediaType;
+
+ use super::get_leading_comments;
+ use super::has_ts_check;
+
+ #[test]
+ fn get_leading_comments_test() {
+ assert_eq!(
+ get_leading_comments(
+ "#!/usr/bin/env deno\r\n// test\n/* 1 *//*2*///3\n//\n /**/ /*4 */"
+ ),
+ vec![
+ " test".to_string(),
+ " 1 ".to_string(),
+ "2".to_string(),
+ "3".to_string(),
+ "".to_string(),
+ "".to_string(),
+ "4 ".to_string(),
+ ]
+ );
+ assert_eq!(
+ get_leading_comments("//1 /* */ \na;"),
+ vec!["1 /* */ ".to_string(),]
+ );
+ assert_eq!(get_leading_comments("//"), vec!["".to_string()]);
+ }
+
+ #[test]
+ fn has_ts_check_test() {
+ assert!(has_ts_check(
+ MediaType::JavaScript,
+ "// @ts-check\nconsole.log(5);"
+ ));
+ assert!(has_ts_check(
+ MediaType::JavaScript,
+ "// deno-lint-ignore\n// @ts-check\n"
+ ));
+ assert!(!has_ts_check(
+ MediaType::JavaScript,
+ "test;\n// @ts-check\n"
+ ));
+ assert!(!has_ts_check(
+ MediaType::JavaScript,
+ "// ts-check\nconsole.log(5);"
+ ));
+ }
+}