Skip to content

Commit 9fa1cc9

Browse files
authored
feat: support analysis of JSDoc imports (#92)
Closes #91
1 parent 90522de commit 9fa1cc9

File tree

5 files changed

+281
-2
lines changed

5 files changed

+281
-2
lines changed

lib/deno_graph.generated.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -803,8 +803,8 @@ const imports = {
803803
__wbindgen_rethrow: function (arg0) {
804804
throw takeObject(arg0);
805805
},
806-
__wbindgen_closure_wrapper1414: function (arg0, arg1, arg2) {
807-
var ret = makeMutClosure(arg0, arg1, 265, __wbg_adapter_24);
806+
__wbindgen_closure_wrapper1426: function (arg0, arg1, arg2) {
807+
var ret = makeMutClosure(arg0, arg1, 268, __wbg_adapter_24);
808808
return addHeapObject(ret);
809809
},
810810
},

lib/deno_graph_bg.wasm

9.16 KB
Binary file not shown.

src/ast.rs

+89
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub use deno_ast::swc::dep_graph::DependencyKind;
88
use anyhow::Result;
99
use deno_ast::parse_module;
1010
use deno_ast::swc::common::comments::Comment;
11+
use deno_ast::swc::common::comments::CommentKind;
1112
use deno_ast::swc::common::BytePos;
1213
use deno_ast::swc::common::Span;
1314
use deno_ast::Diagnostic;
@@ -27,6 +28,8 @@ lazy_static! {
2728
static ref DENO_TYPES_RE: Regex =
2829
Regex::new(r#"(?i)^\s*@deno-types\s*=\s*(?:["']([^"']+)["']|(\S+))"#)
2930
.unwrap();
31+
/// Matches a JSDoc import type reference (`{import("./example.js")}`).
32+
static ref JSDOC_IMPORT_RE: Regex = Regex::new(r#"\{\s*import\(['"]([^'"]+)['"]\)[^}]*}"#).unwrap();
3033
/// Matches the `@jsxImportSource` pragma.
3134
static ref JSX_IMPORT_SOURCE_RE: Regex = Regex::new(r#"(?i)^[\s*]*@jsxImportSource\s+(\S+)"#).unwrap();
3235
/// Matches a `/// <reference ... />` comment reference.
@@ -81,6 +84,29 @@ pub fn analyze_deno_types(
8184
}
8285
}
8386

87+
/// Searches JSDoc comment blocks for type imports
88+
/// (e.g. `{import("./types.d.ts").Type}`) and returns a vector of tuples of
89+
/// the specifier and the span of the import.
90+
pub fn analyze_jsdoc_imports(
91+
parsed_source: &ParsedSource,
92+
) -> Vec<(String, Span)> {
93+
let mut deps = Vec::new();
94+
for comment in parsed_source.comments().get_vec().iter() {
95+
if comment.kind != CommentKind::Block || !comment.text.starts_with('*') {
96+
continue;
97+
}
98+
for captures in JSDOC_IMPORT_RE.captures_iter(&comment.text) {
99+
if let Some(m) = captures.get(1) {
100+
deps.push((
101+
m.as_str().to_string(),
102+
comment_match_to_swc_span(comment, &m),
103+
));
104+
}
105+
}
106+
}
107+
deps
108+
}
109+
84110
/// Searches comments for a `@jsxImportSource` pragma on JSX/TSX media types
85111
pub fn analyze_jsx_import_sources(
86112
parsed_source: &ParsedSource,
@@ -225,6 +251,7 @@ impl SourceParser for DefaultSourceParser {
225251
#[cfg(test)]
226252
mod tests {
227253
use super::*;
254+
use serde_json::json;
228255

229256
#[test]
230257
fn test_parse() {
@@ -370,4 +397,66 @@ mod tests {
370397
Some(&"json".to_string())
371398
);
372399
}
400+
401+
#[test]
402+
fn test_analyze_jsdoc_imports() {
403+
let specifier = ModuleSpecifier::parse("file:///a/test.js").unwrap();
404+
let source = Arc::new(
405+
r#"
406+
/** @module */
407+
408+
/**
409+
* Some stuff here
410+
*
411+
* @type {import("./a.js").A}
412+
*/
413+
const a = "a";
414+
415+
/**
416+
* Some other stuff here
417+
*
418+
* @param {import('./b.js').C}
419+
* @returns {import("./d.js")}
420+
*/
421+
function b(c) {
422+
return;
423+
}
424+
"#
425+
.to_string(),
426+
);
427+
let parser = DefaultSourceParser::new();
428+
let result = parser.parse_module(&specifier, source, MediaType::TypeScript);
429+
assert!(result.is_ok());
430+
let parsed_source = result.unwrap();
431+
let dependencies = analyze_jsdoc_imports(&parsed_source);
432+
assert_eq!(
433+
json!(dependencies),
434+
json!([
435+
[
436+
"./a.js",
437+
{
438+
"start": 62,
439+
"end": 68,
440+
"ctxt": 0
441+
}
442+
],
443+
[
444+
"./b.js",
445+
{
446+
"start": 146,
447+
"end": 152,
448+
"ctxt": 0
449+
}
450+
],
451+
[
452+
"./d.js",
453+
{
454+
"start": 179,
455+
"end": 185,
456+
"ctxt": 0
457+
}
458+
]
459+
])
460+
);
461+
}
373462
}

src/graph.rs

+9
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use crate::ast;
44
use crate::ast::analyze_deno_types;
55
use crate::ast::analyze_dependencies;
6+
use crate::ast::analyze_jsdoc_imports;
67
use crate::ast::analyze_jsx_import_sources;
78
use crate::ast::analyze_ts_references;
89
use crate::ast::SourceParser;
@@ -1190,6 +1191,14 @@ pub(crate) fn parse_module_from_ast(
11901191
dep.maybe_code = resolved_dependency;
11911192
}
11921193

1194+
// Analyze any JSDoc type imports
1195+
for (import_source, span) in analyze_jsdoc_imports(parsed_source) {
1196+
let range = Range::from_swc_span(&module.specifier, parsed_source, &span);
1197+
let resolved_dependency = resolve(&import_source, &range, maybe_resolver);
1198+
let dep = module.dependencies.entry(import_source).or_default();
1199+
dep.maybe_type = resolved_dependency;
1200+
}
1201+
11931202
// Analyze the X-TypeScript-Types header
11941203
if module.maybe_types_dependency.is_none() {
11951204
if let Some(headers) = maybe_headers {

src/lib.rs

+181
Original file line numberDiff line numberDiff line change
@@ -997,6 +997,118 @@ console.log(a);
997997
);
998998
}
999999

1000+
#[tokio::test]
1001+
async fn test_create_graph_with_jsdoc_imports() {
1002+
let mut loader = setup(
1003+
vec![
1004+
(
1005+
"file:///a/test.js",
1006+
Ok((
1007+
"file:///a/test.js",
1008+
None,
1009+
r#"
1010+
/**
1011+
* Some js doc
1012+
*
1013+
* @param {import("./types.d.ts").A} a
1014+
* @return {import("./other.ts").B}
1015+
*/
1016+
export function a(a) {
1017+
return;
1018+
}
1019+
"#,
1020+
)),
1021+
),
1022+
(
1023+
"file:///a/types.d.ts",
1024+
Ok(("file:///a/types.d.ts", None, r#"export type A = string;"#)),
1025+
),
1026+
(
1027+
"file:///a/other.ts",
1028+
Ok((
1029+
"file:///a/other.ts",
1030+
None,
1031+
r#"export type B = string | undefined;"#,
1032+
)),
1033+
),
1034+
],
1035+
vec![],
1036+
);
1037+
let root = ModuleSpecifier::parse("file:///a/test.js").unwrap();
1038+
let graph = create_graph(
1039+
vec![root.clone()],
1040+
false,
1041+
None,
1042+
&mut loader,
1043+
None,
1044+
None,
1045+
None,
1046+
)
1047+
.await;
1048+
assert_eq!(
1049+
json!(graph),
1050+
json!({
1051+
"roots": [
1052+
"file:///a/test.js"
1053+
],
1054+
"modules": [
1055+
{
1056+
"dependencies": [],
1057+
"mediaType": "TypeScript",
1058+
"size": 35,
1059+
"specifier": "file:///a/other.ts"
1060+
},
1061+
{
1062+
"dependencies": [
1063+
{
1064+
"specifier": "./other.ts",
1065+
"type": {
1066+
"specifier": "file:///a/other.ts",
1067+
"span": {
1068+
"start": {
1069+
"line": 5,
1070+
"character": 20
1071+
},
1072+
"end": {
1073+
"line": 5,
1074+
"character": 30
1075+
}
1076+
}
1077+
}
1078+
},
1079+
{
1080+
"specifier": "./types.d.ts",
1081+
"type": {
1082+
"specifier": "file:///a/types.d.ts",
1083+
"span": {
1084+
"start": {
1085+
"line": 4,
1086+
"character": 19
1087+
},
1088+
"end": {
1089+
"line": 4,
1090+
"character": 31
1091+
}
1092+
}
1093+
}
1094+
}
1095+
],
1096+
"mediaType": "JavaScript",
1097+
"size": 138,
1098+
"specifier": "file:///a/test.js"
1099+
},
1100+
{
1101+
"dependencies": [],
1102+
"mediaType": "Dts",
1103+
"size": 23,
1104+
"specifier": "file:///a/types.d.ts"
1105+
}
1106+
],
1107+
"redirects": {}
1108+
})
1109+
);
1110+
}
1111+
10001112
#[tokio::test]
10011113
async fn test_create_graph_with_redirects() {
10021114
let mut loader = setup(
@@ -1720,4 +1832,73 @@ console.log(a);
17201832
);
17211833
assert!(result.is_ok());
17221834
}
1835+
1836+
#[test]
1837+
fn test_parse_module_with_jsdoc_imports() {
1838+
let specifier = ModuleSpecifier::parse("file:///a/test.js").unwrap();
1839+
let result = parse_module(
1840+
&specifier,
1841+
None,
1842+
Arc::new(
1843+
r#"
1844+
/**
1845+
* Some js doc
1846+
*
1847+
* @param {import("./types.d.ts").A} a
1848+
* @return {import("./other.ts").B}
1849+
*/
1850+
export function a(a) {
1851+
return;
1852+
}
1853+
"#
1854+
.to_string(),
1855+
),
1856+
None,
1857+
None,
1858+
);
1859+
assert!(result.is_ok());
1860+
let actual = result.unwrap();
1861+
assert_eq!(
1862+
json!(actual),
1863+
json!({
1864+
"dependencies": [
1865+
{
1866+
"specifier": "./other.ts",
1867+
"type": {
1868+
"specifier": "file:///a/other.ts",
1869+
"span": {
1870+
"start": {
1871+
"line": 5,
1872+
"character": 20,
1873+
},
1874+
"end": {
1875+
"line": 5,
1876+
"character": 30
1877+
}
1878+
}
1879+
}
1880+
},
1881+
{
1882+
"specifier": "./types.d.ts",
1883+
"type": {
1884+
"specifier": "file:///a/types.d.ts",
1885+
"span": {
1886+
"start": {
1887+
"line": 4,
1888+
"character": 19,
1889+
},
1890+
"end": {
1891+
"line": 4,
1892+
"character": 31,
1893+
}
1894+
}
1895+
}
1896+
}
1897+
],
1898+
"mediaType": "JavaScript",
1899+
"size": 138,
1900+
"specifier": "file:///a/test.js"
1901+
})
1902+
);
1903+
}
17231904
}

0 commit comments

Comments
 (0)