use std::path::Path;
use oxc_allocator::Allocator;
use oxc_ast_visit::Visit;
use oxc_parser::Parser;
use oxc_span::SourceType;
use crate::source_map::ExtractionResult;
use crate::template_expression_scan::{
TemplateScanMode, guarded_import_locals, import_declaration_ranges, narrowable_import_locals,
record_unexplained_mentions, record_unexplained_script_mentions, recorded_member_pairs,
scan_template_usage,
};
use crate::visitor::{ModuleInfoExtractor, extend_member_accesses, extend_whole_object_uses};
use crate::{ImportInfo, MemberAccess, ModuleInfo};
use fallow_types::discover::FileId;
type StatementBlock<'a> = Vec<(usize, &'a str)>;
struct MdxScan<'a> {
blocks: Vec<StatementBlock<'a>>,
prose_lines: Vec<(usize, &'a str)>,
code_lines: Vec<&'a str>,
}
impl<'a> MdxScan<'a> {
fn extraction(&self) -> ExtractionResult {
let mut statements = ExtractionResult::default();
for &(line_start, line) in self.blocks.iter().flatten() {
statements.push_mapped(line, line_start);
}
statements
}
fn statement_lines(&self) -> impl Iterator<Item = (usize, &'a str)> {
self.blocks.iter().flatten().copied()
}
fn demote_rejected_blocks(&mut self) -> bool {
let scanned = self.blocks.len();
let mut kept = Vec::with_capacity(scanned);
for block in std::mem::take(&mut self.blocks) {
if statement_block_is_accepted(&block) {
kept.push(block);
} else {
self.prose_lines.extend(block);
}
}
self.blocks = kept;
if self.blocks.len() == scanned {
return false;
}
self.prose_lines
.sort_unstable_by_key(|&(line_start, _)| line_start);
true
}
}
#[must_use]
pub fn extract_mdx_statements(source: &str) -> String {
extract_mdx_source(source).extraction().body
}
#[must_use]
pub fn extract_mdx_statements_mapped(source: &str) -> ExtractionResult {
accepted_mdx_source(source).1
}
fn extract_mdx_source(source: &str) -> MdxScan<'_> {
let mut scanner = MdxStatementScanner::default();
for (line_start, line) in lines_with_offsets(source) {
scanner.scan_line(line_start, line);
}
scanner.into_scan()
}
fn accepted_mdx_source(source: &str) -> (MdxScan<'_>, ExtractionResult) {
let mut scan = extract_mdx_source(source);
scan.demote_rejected_blocks();
let extraction = scan.extraction();
(scan, extraction)
}
#[derive(Default)]
struct MdxStatementScanner<'a> {
blocks: Vec<StatementBlock<'a>>,
prose_lines: Vec<(usize, &'a str)>,
code_lines: Vec<&'a str>,
collecting_statement: bool,
code_fence: Option<CodeFence>,
}
impl<'a> MdxStatementScanner<'a> {
fn scan_line(&mut self, line_start: usize, line: &'a str) {
let trimmed = line.trim();
if self.consume_code_fence(trimmed) {
self.code_lines.push(line);
return;
}
let statement_line = line.trim_end_matches(['\r', '\n']);
if self.collecting_statement {
if is_top_level_statement_start(statement_line) {
self.collecting_statement = false;
self.push_statement_start(line_start, line, statement_line);
} else {
self.push_multiline_line(line_start, line);
}
return;
}
if is_top_level_statement_start(statement_line) {
self.push_statement_start(line_start, line, statement_line);
return;
}
self.prose_lines.push((line_start, line));
}
fn consume_code_fence(&mut self, trimmed: &str) -> bool {
if let Some(fence) = self.code_fence {
if fence.is_closing_line(trimmed) {
self.code_fence = None;
}
return true;
}
if self.collecting_statement {
return false;
}
if let Some(fence) = CodeFence::opening(trimmed) {
self.code_fence = Some(fence);
return true;
}
false
}
fn push_statement_start(&mut self, line_start: usize, line: &'a str, statement: &str) {
self.blocks.push(vec![(line_start, line)]);
self.collecting_statement = !parses_as_typescript(statement);
}
fn push_multiline_line(&mut self, line_start: usize, line: &'a str) {
if let Some(block) = self.blocks.last_mut() {
block.push((line_start, line));
self.collecting_statement = !statement_block_is_accepted(block);
}
}
fn into_scan(self) -> MdxScan<'a> {
MdxScan {
blocks: self.blocks,
prose_lines: self.prose_lines,
code_lines: self.code_lines,
}
}
}
fn collect_body_usage(
scan: &MdxScan<'_>,
imports: &[ImportInfo],
) -> (Vec<MemberAccess>, Vec<String>) {
let mut accesses = Vec::new();
let mut whole_object_uses = Vec::new();
let import_locals = guarded_import_locals(imports);
for &(_, line) in &scan.prose_lines {
scan_template_usage(
line,
&import_locals,
TemplateScanMode::MdxProse,
&mut accesses,
&mut whole_object_uses,
);
}
for line in &scan.code_lines {
record_unexplained_mentions(line, &import_locals, &[], &mut whole_object_uses);
}
(accesses, whole_object_uses)
}
fn collect_unexplained_statement_mentions(scan: &MdxScan<'_>, info: &ModuleInfo) -> Vec<String> {
let mut whole_object_uses = Vec::new();
let guarded = narrowable_import_locals(&info.imports);
if guarded.is_empty() {
return whole_object_uses;
}
let recorded = recorded_member_pairs(&info.member_accesses, &guarded);
let excluded = import_declaration_ranges(&info.imports);
for (line_start, line) in scan.statement_lines() {
record_unexplained_script_mentions(
line,
line_start,
&guarded,
&excluded,
&recorded,
&mut whole_object_uses,
);
}
whole_object_uses
}
const EXPORT_DECLARATION_KEYWORDS: [&str; 13] = [
"abstract",
"async",
"class",
"const",
"declare",
"default",
"enum",
"function",
"interface",
"let",
"namespace",
"type",
"var",
];
fn is_statement_start(trimmed: &str) -> bool {
if let Some(rest) = statement_keyword_rest(trimmed, "import") {
return is_import_statement_rest(rest) || parses_as_typescript(trimmed);
}
if let Some(rest) = statement_keyword_rest(trimmed, "export") {
return is_export_statement_rest(rest) || parses_as_typescript(trimmed);
}
false
}
fn is_top_level_statement_start(line: &str) -> bool {
line.trim_start().len() == line.len() && is_statement_start(line)
}
fn statement_keyword_rest<'a>(trimmed: &'a str, keyword: &str) -> Option<&'a str> {
let rest = trimmed.strip_prefix(keyword)?;
let next = rest.chars().next()?;
(next.is_whitespace() || next == '{' || next == '(').then(|| rest.trim_start())
}
fn is_import_statement_rest(rest: &str) -> bool {
let Some(first) = rest.chars().next() else {
return false;
};
if first == '\'' || first == '"' {
return true;
}
if rest.contains('{') {
return true;
}
if first == '*' {
return true;
}
if rest.trim_end().ends_with(" from") {
return true;
}
has_source_clause(rest)
}
fn is_export_statement_rest(rest: &str) -> bool {
let Some(first) = rest.chars().next() else {
return false;
};
if rest.contains('{') {
return true;
}
if first == '*' {
return true;
}
EXPORT_DECLARATION_KEYWORDS.contains(&leading_word(rest))
}
fn leading_word(text: &str) -> &str {
let end = text
.find(|ch: char| !ch.is_ascii_alphanumeric() && ch != '_' && ch != '$')
.unwrap_or(text.len());
&text[..end]
}
fn has_source_clause(line: &str) -> bool {
line.match_indices("from").any(|(index, keyword)| {
line[..index]
.chars()
.next_back()
.is_some_and(char::is_whitespace)
&& line[index + keyword.len()..]
.trim_start()
.starts_with(['\'', '"'])
})
}
fn parse_statement_body(
extraction: &ExtractionResult,
file_id: FileId,
content_hash: u64,
parsed_suppressions: crate::suppress::ParsedSuppressions,
) -> (ModuleInfo, bool) {
if extraction.body.is_empty() {
let info =
ModuleInfoExtractor::new().into_module_info(file_id, content_hash, parsed_suppressions);
return (info, true);
}
let allocator = Allocator::default();
let parser_return = Parser::new(&allocator, &extraction.body, SourceType::tsx()).parse();
let accepted = !parser_return.panicked && parser_return.errors.is_empty();
let mut extractor = ModuleInfoExtractor::new();
extractor.visit_program(&parser_return.program);
extractor.remap_spans_with(|span| extraction.remap_span(span));
(
extractor.into_module_info(file_id, content_hash, parsed_suppressions),
accepted,
)
}
fn statement_block_is_accepted(block: &[(usize, &str)]) -> bool {
let mut body = String::new();
for &(_, line) in block {
body.push_str(line);
}
parses_as_typescript(&body)
}
fn parses_as_typescript(source: &str) -> bool {
let allocator = Allocator::default();
let parsed = Parser::new(&allocator, source, SourceType::tsx()).parse();
!parsed.panicked && parsed.errors.is_empty()
}
fn lines_with_offsets(source: &str) -> impl Iterator<Item = (usize, &str)> {
let mut offset = 0usize;
source.split_inclusive('\n').map(move |line| {
let start = offset;
offset += line.len();
(start, line)
})
}
#[derive(Clone, Copy)]
struct CodeFence {
marker: char,
len: usize,
}
impl CodeFence {
fn opening(line: &str) -> Option<Self> {
let marker = line.chars().next()?;
if marker != '`' && marker != '~' {
return None;
}
let len = line.chars().take_while(|&c| c == marker).count();
(len >= 3).then_some(Self { marker, len })
}
fn is_closing_line(self, line: &str) -> bool {
if !line.starts_with(self.marker) {
return false;
}
let len = line.chars().take_while(|&c| c == self.marker).count();
len >= self.len && line[len..].trim().is_empty()
}
}
pub(crate) fn is_mdx_file(path: &Path) -> bool {
path.extension()
.and_then(|e| e.to_str())
.is_some_and(|ext| ext == "mdx")
}
pub(crate) fn parse_mdx_to_module(file_id: FileId, source: &str, content_hash: u64) -> ModuleInfo {
let parsed_suppressions = crate::suppress::parse_suppressions_from_source(source);
let line_offsets = fallow_types::extract::compute_line_offsets(source);
let (scan, extraction) = accepted_mdx_source(source);
let (mut info, _) =
parse_statement_body(&extraction, file_id, content_hash, parsed_suppressions);
let (body_accesses, body_whole_object_uses) = collect_body_usage(&scan, &info.imports);
extend_member_accesses(&mut info, body_accesses);
extend_whole_object_uses(&mut info, body_whole_object_uses);
let statement_whole_object_uses = collect_unexplained_statement_mentions(&scan, &info);
extend_whole_object_uses(&mut info, statement_whole_object_uses);
info.line_offsets = line_offsets;
info
}
#[cfg(all(test, not(miri)))]
mod tests {
use super::*;
#[test]
fn is_mdx_file_positive() {
assert!(is_mdx_file(Path::new("post.mdx")));
}
#[test]
fn is_mdx_file_rejects_md() {
assert!(!is_mdx_file(Path::new("readme.md")));
}
#[test]
fn is_mdx_file_rejects_tsx() {
assert!(!is_mdx_file(Path::new("component.tsx")));
}
#[test]
fn is_mdx_file_rejects_jsx() {
assert!(!is_mdx_file(Path::new("component.jsx")));
}
#[test]
fn extracts_single_import() {
let result = extract_mdx_statements("import { Chart } from './Chart'\n\n# Title\n");
assert!(result.contains("import { Chart } from './Chart'"));
}
#[test]
fn extracts_default_import() {
let result = extract_mdx_statements("import Button from './Button'\n\n# Title\n");
assert!(result.contains("import Button from './Button'"));
}
#[test]
fn extracts_multiple_imports() {
let source = "import { A } from './a'\nimport { B } from './b'\n\n# Title\n";
let result = extract_mdx_statements(source);
assert!(result.contains("import { A } from './a'"));
assert!(result.contains("import { B } from './b'"));
}
#[test]
fn extracts_import_no_space() {
let result = extract_mdx_statements("import{ Chart } from './Chart'\n\n# Title\n");
assert!(result.contains("import{ Chart }"));
}
#[test]
fn extracts_export_const() {
let result = extract_mdx_statements("export const meta = { title: 'Hello' }\n\n# Title\n");
assert!(result.contains("export const meta"));
}
#[test]
fn multiline_export_const_with_object_literal() {
let result = extract_mdx_statements(
"export const meta = {\n title: 'Hello',\n draft: false\n};\n\n# Title\n",
);
assert!(result.contains("export const meta"));
assert!(result.contains("title: 'Hello'"));
assert!(!result.contains("# Title"));
}
#[test]
fn multiline_export_const_closed_by_bare_brace() {
let result =
extract_mdx_statements("export const meta = {\n title: 'Hello'\n}\n\n# Title\n");
assert!(result.contains("export const meta"));
assert!(result.contains("title: 'Hello'"));
assert!(!result.contains("# Title"));
}
#[test]
fn extracts_export_no_space() {
let result = extract_mdx_statements("export{ foo } from './foo'\n\n# Title\n");
assert!(result.contains("export{ foo }"));
}
#[test]
fn multiline_import_with_braces() {
let source =
"import {\n Chart,\n Table,\n Graph\n} from './components'\n\n# Dashboard\n";
let result = extract_mdx_statements(source);
assert!(result.contains("Chart"));
assert!(result.contains("Table"));
assert!(result.contains("Graph"));
assert!(result.contains("from './components'"));
}
#[test]
fn multiline_import_closed_by_from() {
let source = "import {\n Foo,\n Bar\n} from './mod'\n\n# Content\n";
let result = extract_mdx_statements(source);
assert!(result.contains("Foo"));
assert!(result.contains("Bar"));
}
#[test]
fn imports_between_prose() {
let source = "import { Header } from './Header'\n\n# Section 1\n\nSome content.\n\nimport { Footer } from './Footer'\n\n## Section 2\n";
let result = extract_mdx_statements(source);
assert!(result.contains("Header"));
assert!(result.contains("Footer"));
}
#[test]
fn prose_lines_excluded() {
let source =
"import { A } from './a'\n\n# Title\n\nSome **markdown** text.\n\n- List item\n";
let result = extract_mdx_statements(source);
assert!(!result.contains("Title"));
assert!(!result.contains("markdown"));
assert!(!result.contains("List item"));
}
#[test]
fn fenced_import_is_ignored() {
let source = r"import { Live } from './Live'
# Example
```ts
file: exampleSlice.ts
import exampleSliceReducer from './exampleSlice'
```
";
let result = extract_mdx_statements(source);
assert!(result.contains("import { Live } from './Live'"));
assert!(!result.contains("./exampleSlice"));
assert_eq!(result.lines().count(), 1);
}
#[test]
fn fenced_export_is_ignored() {
let source = r"# Example
```tsx
export const Example = () => null
```
";
let result = extract_mdx_statements(source);
assert!(result.is_empty());
}
#[test]
fn tilde_fenced_import_is_ignored() {
let source = r"import { Live } from './Live'
~~~ts
import virtual from './virtual'
~~~
export { Live }
";
let result = extract_mdx_statements(source);
assert!(result.contains("import { Live } from './Live'"));
assert!(result.contains("export { Live }"));
assert!(!result.contains("./virtual"));
assert_eq!(result.lines().count(), 2);
}
#[test]
fn longer_matching_fence_closes_code_block() {
let source = r"````ts
import hidden from './hidden'
````
import { Visible } from './Visible'
";
let result = extract_mdx_statements(source);
assert!(!result.contains("./hidden"));
assert!(result.contains("import { Visible } from './Visible'"));
}
#[test]
fn shorter_matching_fence_does_not_close_code_block() {
let source = r"````ts
import hidden from './hidden'
```
import stillHidden from './still-hidden'
````
import { Visible } from './Visible'
";
let result = extract_mdx_statements(source);
assert!(!result.contains("./hidden"));
assert!(!result.contains("./still-hidden"));
assert!(result.contains("import { Visible } from './Visible'"));
}
#[test]
fn empty_source() {
let result = extract_mdx_statements("");
assert!(result.is_empty());
}
#[test]
fn no_imports_or_exports() {
let result = extract_mdx_statements("# Just Markdown\n\nNo imports here.\n");
assert!(result.is_empty());
}
#[test]
fn import_like_text_not_extracted() {
let result = extract_mdx_statements("This is an important note.\n");
assert!(result.is_empty());
}
#[test]
fn export_like_text_not_extracted() {
let result = extract_mdx_statements("We are exporting goods overseas.\n");
assert!(result.is_empty());
}
#[test]
fn statement_start_requires_a_real_import_or_export_shape() {
for line in [
"import './global.css'",
"import \"./global.css\"",
"import { A } from './a'",
"import{ A } from './a'",
"import Default, {",
"import {",
"import * as NS from './ns'",
"import Button from './Button'",
"import Button from'./Button'",
"import type { A } from './a'",
"export { A }",
"export{ A }",
"export * from './a'",
"export * as ns from './a'",
"export const meta = 1",
"export let x = 1",
"export var x = 1",
"export function render() {}",
"export async function render() {}",
"export class Card {}",
"export abstract class Card {}",
"export type Meta = string",
"export interface Meta {}",
"export enum Kind {}",
"export declare const x: number",
"export namespace Docs {}",
"export default () => null",
] {
assert!(is_statement_start(line), "{line:?} should be a statement");
}
for line in [
"import the thing and render it here.",
"importantly, the docs ship first.",
"import all of your data before you start.",
"export the report to a spreadsheet later.",
"exports are documented below.",
"import",
"export",
] {
assert!(!is_statement_start(line), "{line:?} should stay prose");
}
}
#[test]
fn import_prose_sentence_is_not_extracted() {
let source = "import * as NS from './ns'\n\n<NS.Star />\n\nimport the thing and render <NS.Moon /> here.\n";
let result = extract_mdx_statements(source);
assert_eq!(
result.lines().count(),
1,
"only the real import should be extracted; got {result:?}"
);
assert!(result.contains("import * as NS from './ns'"));
}
#[test]
fn export_prose_sentence_is_not_extracted() {
let source =
"export const meta = { title: 'x' }\n\nexport the report to a spreadsheet later.\n";
let result = extract_mdx_statements(source);
assert_eq!(
result.lines().count(),
1,
"only the real export should be extracted; got {result:?}"
);
assert!(result.contains("export const meta"));
}
fn import_sources(source: &str) -> Vec<String> {
parse_mdx_to_module(fallow_types::discover::FileId(0), source, 0)
.imports
.iter()
.map(|import| import.source.clone())
.collect()
}
fn export_names(source: &str) -> Vec<String> {
parse_mdx_to_module(fallow_types::discover::FileId(0), source, 0)
.exports
.iter()
.map(|export| export.name.to_string())
.collect()
}
#[test]
fn mdx_import_prose_line_keeps_imports_and_credits_tags() {
let source = "import * as NS from '../components/ns'\n\n<NS.Star />\n\nimport the thing and render <NS.Moon /> here.\n";
assert_eq!(
import_sources(source),
vec!["../components/ns".to_string()],
"the namespace import must survive the prose line"
);
assert_eq!(
body_member_accesses(source),
vec![
("NS".to_string(), "Star".to_string()),
("NS".to_string(), "Moon".to_string()),
],
"both body tags should be credited"
);
assert!(
body_whole_object_uses(source).is_empty(),
"every mention is a dotted tag, so the namespace stays precise"
);
}
#[test]
fn unparsable_statement_line_falls_back_to_prose() {
let source = "import * as NS from './ns'\nimport * as Other from './other'\n\n\
import data from './the-api' using Other before rendering.\n\n\
<NS.Star />\n<Other.Star />\n";
assert_eq!(
import_sources(source),
vec!["./ns".to_string(), "./other".to_string()],
"both imports must survive the rejected line"
);
assert_eq!(
body_whole_object_uses(source),
vec!["Other".to_string()],
"the mention on the demoted line must keep Other on the mark-all path"
);
let accesses = body_member_accesses(source);
assert!(
accesses.contains(&("NS".to_string(), "Star".to_string()))
&& accesses.contains(&("Other".to_string(), "Star".to_string())),
"the body tags should still record; got {accesses:?}"
);
}
#[test]
fn unparsable_multiline_block_is_demoted_whole() {
let source = "import { Keep } from './keep'\n\nimport {\n Broken from './broken'\n\nimport { Also } from './also'\n";
assert_eq!(
import_sources(source),
vec!["./keep".to_string(), "./also".to_string()],
"only the rejected block should be lost"
);
}
#[test]
fn parsable_multiline_import_survives_a_demoted_sibling() {
let source = "import data from './the-api' before you begin.\n\nimport {\n Foo,\n Bar\n} from './module'\n";
let info = parse_mdx_to_module(fallow_types::discover::FileId(0), source, 0);
let locals: Vec<&str> = info
.imports
.iter()
.map(|import| import.local_name.as_str())
.collect();
assert_eq!(
locals,
vec!["Foo", "Bar"],
"the multi-line import must survive whole"
);
}
#[test]
fn type_only_import_clause_no_longer_drops_the_other_imports() {
let source = "import type { Meta } from './meta'\n\nimport { Card } from './card'\n";
assert_eq!(
import_sources(source),
vec!["./meta".to_string(), "./card".to_string()],
"both imports must survive"
);
}
#[test]
fn suppressions_survive_the_fallback_reparse() {
let source = "<!-- fallow-ignore-file -->\n<!-- fallow-ignore-file not-a-real-kind -->\n\n\
import data from './the-api' before you begin.\n\n\
import { Card } from './card'\n";
let info = parse_mdx_to_module(fallow_types::discover::FileId(0), source, 0);
assert_eq!(
info.imports.len(),
1,
"the fallback has to have run for this test to pin anything"
);
assert_eq!(
info.suppressions.len(),
1,
"the file-level suppression must survive the retry"
);
assert_eq!(
info.unknown_suppression_kinds.len(),
1,
"the unknown suppression token must survive the retry"
);
}
#[test]
fn source_clause_matches_any_whitespace_around_from() {
for line in [
"import Used from\t'./used'",
"import Used\tfrom './used'",
"import Used from './used'",
"import Used from\u{a0}'./used'",
"import type Meta from\t'./meta'",
] {
assert!(is_statement_start(line), "{line:?} should be a statement");
}
for line in [
"import fromage before you begin.",
"import the fromage and the bread.",
"import data from_the_api and render it.",
] {
assert!(!is_statement_start(line), "{line:?} should stay prose");
}
}
#[test]
fn tab_separated_source_clause_keeps_the_import() {
let source = "import Used from\t'./used'\n\n<Used />\n";
assert_eq!(
import_sources(source),
vec!["./used".to_string()],
"the tab-separated import must resolve like a space-separated one"
);
}
#[test]
fn a_valid_statement_the_shape_table_misses_is_recovered_by_the_parse_probe() {
for line in [
"import /* set up styles */ './global.css'",
"import /* c */ Button from './button'",
"import /* c */ * as NS from './ns'",
"export /* keep */ const commented = 1",
"export /* keep */ function render() {}",
"import ('./dynamic')",
"export = contents;",
] {
assert!(is_statement_start(line), "{line:?} should be a statement");
}
for line in [
"import /* the good parts */ of the library into your head.",
"export /* only */ what the reader needs to see.",
"export FALLOW_FORMAT=json",
"export PATH=\"$BUN_INSTALL/bin:$PATH\"",
"export being referenced.",
"import json, sys",
] {
assert!(!is_statement_start(line), "{line:?} should stay prose");
}
}
#[test]
fn commented_keyword_import_keeps_its_edge() {
let source = "import /* set up styles */ '../styles/global.css'\n\n# Title\n";
assert_eq!(
import_sources(source),
vec!["../styles/global.css".to_string()],
"the commented side-effect import must resolve"
);
}
#[test]
fn commented_keyword_export_stays_an_export() {
let source = "export /* keep */ const commented = 1\n\n# Title\n";
assert_eq!(
export_names(source),
vec!["commented".to_string()],
"the commented export declaration must survive"
);
}
#[test]
fn spaced_dynamic_import_keeps_its_edge() {
let source = "import ('../components/lazy')\n\n# Title\n";
let sources: Vec<String> =
parse_mdx_to_module(fallow_types::discover::FileId(0), source, 0)
.dynamic_imports
.iter()
.map(|import| import.source.clone())
.collect();
assert_eq!(
sources,
vec!["../components/lazy".to_string()],
"the spaced dynamic import must resolve"
);
}
#[test]
fn a_from_inside_a_string_does_not_end_a_statement_block() {
for note in ["Everything\tfrom scratch", "Everything from scratch"] {
let source = format!("export const meta = {{\n note: '{note}',\n}}\n\n# Title\n");
assert_eq!(
export_names(&source),
vec!["meta".to_string()],
"the multi-line export must be collected whole for {note:?}"
);
}
}
#[test]
fn side_effect_import() {
let result = extract_mdx_statements("import './global.css'\n\n# Title\n");
assert!(result.contains("import './global.css'"));
}
#[test]
fn namespace_import() {
let result = extract_mdx_statements("import * as utils from './utils'\n\n# Title\n");
assert!(result.contains("import * as utils from './utils'"));
}
#[test]
fn single_line_import_with_braces_balanced() {
let source = "import { A } from './a'\n# Title\n";
let result = extract_mdx_statements(source);
assert_eq!(result.lines().count(), 1);
}
#[test]
fn multiline_import_with_braces_extracted_as_one() {
let source = "import {\n Foo,\n Bar\n} from './module'\n\n# Title\n";
let result = extract_mdx_statements(source);
assert!(result.contains("Foo"), "Foo should be in the result");
assert!(result.contains("Bar"), "Bar should be in the result");
assert!(
result.contains("from './module'"),
"from clause should be in the result"
);
}
#[test]
fn export_with_braces_from_module() {
let source = "export { Foo, Bar } from './module'\n\n# Title\n";
let result = extract_mdx_statements(source);
assert!(result.contains("export { Foo, Bar } from './module'"));
}
#[test]
fn non_import_lines_between_imports_ignored() {
let source = "import { A } from './a'\n\n# Some heading\n\nA paragraph of text.\n\nimport { B } from './b'\n";
let result = extract_mdx_statements(source);
assert!(result.contains("import { A } from './a'"));
assert!(result.contains("import { B } from './b'"));
assert!(!result.contains("heading"), "prose should not be extracted");
assert!(
!result.contains("paragraph"),
"prose should not be extracted"
);
assert_eq!(result.lines().count(), 2);
}
#[test]
fn multiline_import_terminated_by_semicolon() {
let source = "import {\n Foo,\n Bar\n};\n\n# Content\n";
let result = extract_mdx_statements(source);
assert!(result.contains("Foo"));
assert!(result.contains("Bar"));
}
#[test]
fn multiline_import_terminated_by_from_no_space_single_quote() {
let source = "import {\n Foo\n} from'./module'\n\n# Content\n";
let result = extract_mdx_statements(source);
assert!(result.contains("Foo"));
assert!(result.contains("from'./module'"));
}
#[test]
fn multiline_import_terminated_by_from_no_space_double_quote() {
let source = "import {\n Foo\n} from\"./module\"\n\n# Content\n";
let result = extract_mdx_statements(source);
assert!(result.contains("Foo"));
assert!(result.contains("from\"./module\""));
}
#[test]
fn multiline_export_with_braces() {
let source = "export {\n Foo,\n Bar\n} from './module'\n\n# Content\n";
let result = extract_mdx_statements(source);
assert!(result.contains("Foo"));
assert!(result.contains("Bar"));
assert!(result.contains("from './module'"));
}
#[test]
fn import_with_from_on_same_line_not_multiline() {
let source = "import { A } from './a'\nimport { B } from './b'\n";
let result = extract_mdx_statements(source);
assert_eq!(result.lines().count(), 2);
}
#[test]
fn mdx_empty_source_returns_empty_module() {
let info = parse_mdx_to_module(fallow_types::discover::FileId(0), "", 0);
assert!(info.imports.is_empty());
assert!(info.exports.is_empty());
}
#[test]
fn mdx_only_prose_returns_empty_module() {
let info = parse_mdx_to_module(
fallow_types::discover::FileId(0),
"# Title\n\nSome text.\n",
0,
);
assert!(info.imports.is_empty());
assert!(info.exports.is_empty());
}
fn body_member_accesses(source: &str) -> Vec<(String, String)> {
parse_mdx_to_module(fallow_types::discover::FileId(0), source, 0)
.member_accesses
.iter()
.map(|access| (access.object.clone(), access.member.clone()))
.collect()
}
#[test]
fn mdx_body_member_tag_records_member_access() {
let accesses = body_member_accesses(
"import * as NS from './ns'\n\n# Title\n\n<NS.Card />\n\n<NS.Panel>\ntext\n</NS.Panel>\n",
);
assert_eq!(
accesses,
vec![
("NS".to_string(), "Card".to_string()),
("NS".to_string(), "Panel".to_string()),
],
"body member tags should each record once; got {accesses:?}"
);
}
#[test]
fn mdx_body_nested_member_tag_records_each_level() {
let accesses = body_member_accesses("import * as A from './a'\n\n<A.B.C>nested</A.B.C>\n");
assert_eq!(
accesses,
vec![
("A.B".to_string(), "C".to_string()),
("A".to_string(), "B".to_string()),
],
"<A.B.C> should record both nesting levels; got {accesses:?}"
);
}
fn body_whole_object_uses(source: &str) -> Vec<String> {
parse_mdx_to_module(fallow_types::discover::FileId(0), source, 0)
.whole_object_uses
.to_vec()
}
#[test]
fn mdx_fenced_code_member_tag_records_no_access_but_keeps_mark_all() {
let source = "import * as NS from './ns'\nimport * as Only from './only'\n\n```tsx\n<NS.Fenced />\n```\n\n~~~\n<NS.Tilde />\n~~~\n\n<NS.Visible />\n<Only.Visible />\n";
let accesses = body_member_accesses(source);
assert_eq!(
accesses,
vec![
("NS".to_string(), "Visible".to_string()),
("Only".to_string(), "Visible".to_string()),
],
"fenced member tags must not record; got {accesses:?}"
);
assert_eq!(
body_whole_object_uses(source),
vec!["NS".to_string()],
"a fenced mention must keep NS on the mark-all path while Only stays precise"
);
}
#[test]
fn mdx_inline_code_member_tag_records_no_access_but_keeps_mark_all() {
let source = "import * as NS from './ns'\n\nUse `<NS.Single />` or ``<NS.Double />`` but render <NS.Visible />.\n\nA stray ` backtick before <NS.Literal /> is not a span.\n";
let accesses = body_member_accesses(source);
assert_eq!(
accesses,
vec![
("NS".to_string(), "Visible".to_string()),
("NS".to_string(), "Literal".to_string()),
],
"inline-code member tags must not record; got {accesses:?}"
);
assert_eq!(body_whole_object_uses(source), vec!["NS".to_string()]);
}
#[test]
fn mdx_lowercase_intrinsic_tag_records_nothing() {
let source = "import * as ui from './ui'\n\n<div>\n <a href=\"x\">link</a>\n <br/>\n <ui.Card />\n</div>\n";
let accesses = body_member_accesses(source);
assert_eq!(
accesses,
vec![("ui".to_string(), "Card".to_string())],
"only the dotted tag should record; got {accesses:?}"
);
}
#[test]
fn mdx_export_line_member_tag_is_not_double_recorded() {
let accesses = body_member_accesses(
"import * as NS from './ns'\nexport const Demo = () => <NS.Card />\n\n<NS.Body />\n",
);
assert_eq!(
accesses,
vec![
("NS".to_string(), "Card".to_string()),
("NS".to_string(), "Body".to_string()),
],
"a statement-line member tag should record exactly once; got {accesses:?}"
);
}
#[test]
fn mdx_body_expression_member_access_records() {
let accesses = body_member_accesses(
"import * as NS from './ns'\n\n<NS.Star />\n\n<Callout icon={NS.Moon}>x</Callout>\n\n{NS.Sun()}\n",
);
assert_eq!(
accesses,
vec![
("NS".to_string(), "Star".to_string()),
("NS".to_string(), "Moon".to_string()),
("NS".to_string(), "Sun".to_string()),
],
"expression member accesses should record; got {accesses:?}"
);
}
#[test]
fn mdx_body_multiline_expression_records_member_access() {
let accesses = body_member_accesses(
"import * as NS from './ns'\n\n<NS.Star />\n\n{\n NS.Moon()\n}\n",
);
assert!(
accesses.contains(&("NS".to_string(), "Moon".to_string())),
"a dotted access inside a multi-line expression should record; got {accesses:?}"
);
}
#[test]
fn mdx_body_whole_namespace_pass_records_whole_object_use() {
let whole = body_whole_object_uses(
"import * as NS from './ns'\nimport * as Tags from './tags'\n\n<NS.Star />\n<Tags.Only />\n\n<Callout all={NS}>x</Callout>\n",
);
assert_eq!(
whole,
vec!["NS".to_string()],
"only the namespace passed whole should record a whole-object use; got {whole:?}"
);
}
#[test]
fn mdx_prose_mention_of_import_binding_is_whole_object_use() {
let whole = body_whole_object_uses(
"import * as NS from './ns'\nimport Layout from './Layout'\n\nNS is documented here.\n\n<NS.Star />\n",
);
assert_eq!(whole, vec!["NS".to_string()], "got {whole:?}");
let whole = body_whole_object_uses(
"import * as NS from './ns'\nimport Layout from './Layout'\n\nOnly `NS` in code.\n\n<NS.Star />\n",
);
assert_eq!(
whole,
vec!["NS".to_string()],
"a code-span mention is unexplained; got {whole:?}"
);
}
#[test]
fn mdx_template_literal_inside_expression_keeps_binding_on_mark_all() {
let source = "import * as NS from './ns'\nimport * as Whole from './whole'\nimport styles from './doc.module.css'\n\n\
<NS.Star />\n<Whole.Star />\n<div className={styles.root}></div>\n\n\
<Callout title={`Moon: ${NS.Moon}`}>x</Callout>\n\n\
{`${JSON.stringify(Whole)}`}\n\n\
<div className={`${styles.spare} extra`}></div>\n";
let accesses = body_member_accesses(source);
assert!(
accesses.contains(&("NS".to_string(), "Star".to_string()))
&& accesses.contains(&("styles".to_string(), "root".to_string())),
"structured accesses still record; got {accesses:?}"
);
let mut whole = body_whole_object_uses(source);
whole.sort();
assert_eq!(
whole,
vec!["NS".to_string(), "Whole".to_string(), "styles".to_string()],
"every binding mentioned inside a template literal must keep mark-all"
);
}
#[test]
fn mdx_fully_understood_body_records_no_whole_object_use() {
let source = "import * as NS from './ns'\nimport Layout from './Layout'\nimport styles from './doc.module.css'\n\n\
# Title\n\n<Layout>\n <NS.Star />\n <NS.Panel icon={NS.Moon} className={styles.root}>\n {NS.helper()}\n </NS.Panel>\n</Layout>\n\n\
{\n NS.Sun()\n}\n";
let whole = body_whole_object_uses(source);
assert!(
whole.is_empty(),
"fully understood mentions must not record a whole-object use; got {whole:?}"
);
let accesses = body_member_accesses(source);
for member in ["Star", "Panel", "Moon", "helper", "Sun"] {
assert!(
accesses.contains(&("NS".to_string(), member.to_string())),
"NS.{member} should be recorded; got {accesses:?}"
);
}
}
#[test]
fn mdx_statement_line_bare_mention_keeps_binding_on_mark_all() {
let source = "import * as SW from './sw';\nimport * as SA from './sa';\n\
import * as SD from './sd';\nimport * as SP from './sp';\n\
import Callout from './Callout.astro';\n\n\
export const all = SW;\n\n\
export const Demo = () => <Callout all={SA} />;\n\n\
export default (props) => <Callout all={SD} {...props} />;\n\n\
export const moon = SP.Moon;\n\n\
<SW.Star /><SA.Star /><SD.Star /><SP.Star />\n";
let mut whole = body_whole_object_uses(source);
whole.sort();
assert_eq!(
whole,
vec!["SA".to_string(), "SD".to_string(), "SW".to_string()],
"every bare statement-line mention must keep its namespace on mark-all"
);
let accesses = body_member_accesses(source);
assert!(
accesses.contains(&("SP".to_string(), "Moon".to_string())),
"the dotted statement access should be recorded; got {accesses:?}"
);
}
#[test]
fn mdx_prose_env_mention_records_no_member_access() {
let source = "import * as NS from './ns'\n\n# Setup\n\n\
Set process.env.API_KEY and import.meta.env.SECRET before running {NS.helper()}.\n";
let accesses = body_member_accesses(source);
assert_eq!(
accesses,
vec![("NS".to_string(), "helper".to_string())],
"only import-local roots record; got {accesses:?}"
);
assert!(body_whole_object_uses(source).is_empty());
}
#[test]
fn issue_2393_accepts_typescript_and_dynamic_import_statements() {
let source = "import type { Meta } from './meta'\nimport('./dynamic')\n\n# Docs\n";
let info = parse_mdx_to_module(fallow_types::discover::FileId(0), source, 0);
assert_eq!(
info.imports
.iter()
.map(|import| import.source.as_str())
.collect::<Vec<_>>(),
vec!["./meta"]
);
assert_eq!(
info.dynamic_imports
.iter()
.map(|import| import.source.as_str())
.collect::<Vec<_>>(),
vec!["./dynamic"]
);
}
#[test]
fn issue_2393_ignores_indented_documentation_imports() {
let source =
"import { Used } from './used'\n\n- step\n import { Example } from './example'\n";
assert_eq!(import_sources(source), vec!["./used".to_string()]);
}
#[test]
fn issue_2393_recovers_a_statement_after_rejected_brace_prose() {
let source = "import { the following keys:\n\nimport { Card } from './card'\n";
assert_eq!(import_sources(source), vec!["./card".to_string()]);
}
#[test]
fn issue_2393_keeps_imports_after_a_multiline_comment() {
let source = "import { A } from './a' /*\nthe docs are here */\nimport { B } from './b'\n";
assert_eq!(
import_sources(source),
vec!["./a".to_string(), "./b".to_string()]
);
}
#[test]
fn issue_2393_does_not_end_an_object_at_from_inside_a_string() {
let source =
"export const meta = {\n note: 'copied from \"the docs\"',\n title: 'Q',\n}\n";
assert_eq!(export_names(source), vec!["meta".to_string()]);
}
#[test]
fn issue_2393_accepts_wrapped_import_source_clauses() {
let source = "import Wide from\n './wide'\nimport { A }\n from './a'\n";
assert_eq!(
import_sources(source),
vec!["./wide".to_string(), "./a".to_string()]
);
}
}