dmc_transform/builtin/
code_import.rs1use crate::pipeline::Transformer;
2use crate::visit::{NodeAction, Visitor, walk_root};
3use dmc_diagnostic::Code;
4use dmc_diagnostic::metadata::{Origin, SourceMeta};
5use dmc_parser::ast::*;
6use duck_diagnostic::{Diagnostic, Label};
7use std::path::PathBuf;
8use std::sync::Arc;
9
10pub struct CodeImport {
19 pub base_dir: Option<PathBuf>,
20}
21
22type FileMeta = (String, Option<Vec<(usize, usize)>>);
25
26impl Default for CodeImport {
27 fn default() -> Self {
28 Self::new()
29 }
30}
31
32impl CodeImport {
33 pub fn new() -> Self {
34 Self { base_dir: None }
35 }
36
37 pub fn with_base_dir(p: impl Into<PathBuf>) -> Self {
38 Self { base_dir: Some(p.into()) }
39 }
40
41 fn parse_file_meta(meta: &str) -> Option<FileMeta> {
42 for part in meta.split_whitespace() {
43 if let Some(rest) = part.strip_prefix("file=") {
44 let raw = rest.trim_matches(|c| c == '"' || c == '\'');
45 if let Some((path, range)) = raw.split_once('{') {
46 let range = range.trim_end_matches('}');
47 return Some((path.to_string(), Some(Self::parse_ranges(range))));
48 }
49 return Some((raw.to_string(), None));
50 }
51 }
52 None
53 }
54
55 fn parse_ranges(spec: &str) -> Vec<(usize, usize)> {
57 let mut out = Vec::new();
58 for token in spec.split(',') {
59 let token = token.trim();
60 if let Some((a, b)) = token.split_once('-') {
61 if let (Ok(a), Ok(b)) = (a.trim().parse::<usize>(), b.trim().parse::<usize>())
62 && a >= 1
63 && b >= a
64 {
65 out.push((a, b));
66 }
67 } else if let Ok(n) = token.parse::<usize>()
68 && n >= 1
69 {
70 out.push((n, n));
71 }
72 }
73 out
74 }
75
76 fn slice_lines(src: &str, ranges: &[(usize, usize)]) -> String {
79 let lines: Vec<&str> = src.lines().collect();
80 let mut out = String::new();
81 for (a, b) in ranges {
82 let start = a.saturating_sub(1);
83 let end = (*b).min(lines.len());
84 for l in lines.iter().take(end).skip(start) {
85 out.push_str(l);
86 out.push('\n');
87 }
88 }
89 out
90 }
91}
92
93impl Transformer for CodeImport {
94 fn name(&self) -> &str {
95 "code-import"
96 }
97
98 fn transform(
99 &self,
100 doc: &mut Document,
101 meta: &SourceMeta,
102 diag_engine: &mut duck_diagnostic::DiagnosticEngine<Code>,
103 ) {
104 let base_dir = self.base_dir.clone().or_else(|| match &meta.origin {
105 Origin::File(p) => p.parent().map(|p| p.to_path_buf()),
106 _ => None,
107 });
108
109 if base_dir.is_none() {
111 diag_engine.emit(Diagnostic::new(
112 Code::BaseDirNotFound,
113 format!(
114 "code-import: source has no on-disk parent (origin = {:?}); relative `file=` paths cannot be resolved",
115 meta.origin
116 ),
117 ));
118 }
119
120 let mut v = Apply { base_dir, meta_path: meta.path.clone(), pending: Vec::new() };
121 walk_root(&mut doc.children, &mut v);
122 for d in v.pending.drain(..) {
123 diag_engine.emit(d);
124 }
125 }
126}
127
128struct Apply {
129 base_dir: Option<PathBuf>,
130 meta_path: Arc<str>,
131 pending: Vec<Diagnostic<Code>>,
132}
133
134impl Visitor for Apply {
135 fn visit_node(&mut self, node: &mut Node) -> NodeAction {
136 if let Node::CodeBlock(cb) = node
137 && let Some(meta) = cb.meta.as_deref()
138 && let Some((file, ranges)) = CodeImport::parse_file_meta(meta)
139 {
140 if let Some(rs) = &ranges
141 && rs.is_empty()
142 {
143 self.pending.push(Diagnostic::new(
144 Code::InvalidLineRange,
145 format!("code-import: line range in `{}` is empty / malformed", meta),
146 ));
147 return NodeAction::Keep;
148 }
149
150 let path = match &self.base_dir {
151 Some(b) => b.join(&file),
152 None => PathBuf::from(&file),
153 };
154 match std::fs::read_to_string(&path) {
155 Ok(content) => {
156 cb.value = match ranges {
157 Some(rs) => CodeImport::slice_lines(&content, &rs),
158 None => content,
159 };
160 },
161 Err(e) => {
162 self.pending.push(
163 Diagnostic::new(Code::ImportFileNotFound, format!("code-import: cannot read {} ({})", path.display(), e))
164 .with_label(Label::primary(cb.span.clone(), Some(format!("imported from {}", self.meta_path)))),
165 );
166 },
167 }
168 }
169 NodeAction::Keep
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn parse_file_meta_no_range() {
179 assert_eq!(CodeImport::parse_file_meta("file=foo.rs"), Some(("foo.rs".into(), None)));
180 assert_eq!(CodeImport::parse_file_meta("file=\"foo.rs\""), Some(("foo.rs".into(), None)));
181 }
182
183 #[test]
184 fn parse_file_meta_with_range() {
185 let (p, r) = CodeImport::parse_file_meta("file=foo.rs{1,3-5,8}").unwrap();
186 assert_eq!(p, "foo.rs");
187 assert_eq!(r, Some(vec![(1, 1), (3, 5), (8, 8)]));
188 }
189
190 #[test]
191 fn slice_lines_picks_ranges() {
192 let src = "a\nb\nc\nd\ne\n";
193 let out = CodeImport::slice_lines(src, &[(2, 3)]);
194 assert_eq!(out, "b\nc\n");
195 let out = CodeImport::slice_lines(src, &[(1, 1), (4, 5)]);
196 assert_eq!(out, "a\nd\ne\n");
197 }
198}