Skip to content

Commit f67de64

Browse files
authored
feat: Multi-file compilation (#142)
1 parent 9784c77 commit f67de64

30 files changed

Lines changed: 1180 additions & 380 deletions

AGENTS.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,10 +209,9 @@ fn valid_query() {
209209
(function_declaration name: (identifier) @name)
210210
"#};
211211

212-
let query = Query::try_from(input).unwrap();
212+
let res = Query::expect_valid_ast(input).unwrap();
213213

214-
assert!(query.is_valid());
215-
insta::assert_snapshot!(query.dump_ast(), @"");
214+
insta::assert_snapshot!(res, @"");
216215
}
217216
```
218217

crates/plotnik-cli/src/commands/debug/mod.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,10 @@ pub fn run(args: DebugArgs) {
101101
if let Some(ref q) = query
102102
&& !q.is_valid()
103103
{
104-
let src = query_source.as_ref().unwrap();
105-
eprint!("{}", q.diagnostics().render_colored(src, args.color));
104+
eprint!(
105+
"{}",
106+
q.diagnostics().render_colored(q.source_map(), args.color)
107+
);
106108
std::process::exit(1);
107109
}
108110
}

crates/plotnik-cli/src/commands/exec.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::fs;
22
use std::io::{self, Read};
33
use std::path::PathBuf;
44

5-
use plotnik_lib::QueryBuilder;
5+
use plotnik_lib::{QueryBuilder, SourceMap};
66

77
use super::debug::source::resolve_lang;
88

@@ -33,7 +33,7 @@ pub fn run(args: ExecArgs) {
3333
let lang = resolve_lang(&args.lang, &args.source_text, &args.source_file);
3434

3535
// Parse query
36-
let query_parsed = QueryBuilder::new(&query_source)
36+
let query_parsed = QueryBuilder::new(SourceMap::one_liner(&query_source))
3737
.parse()
3838
.unwrap_or_else(|e| {
3939
eprintln!("error: {}", e);
@@ -46,7 +46,7 @@ pub fn run(args: ExecArgs) {
4646
// Link query against language
4747
let linked = query_analyzed.link(&lang);
4848
if !linked.is_valid() {
49-
eprint!("{}", linked.diagnostics().render(&query_source));
49+
eprint!("{}", linked.diagnostics().render(linked.source_map()));
5050
std::process::exit(1);
5151
}
5252

crates/plotnik-cli/src/commands/types.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,13 @@ pub fn run(args: TypesArgs) {
4141
.link(&lang);
4242

4343
if !query.is_valid() {
44-
eprint!("{}", query.diagnostics().render(&query_source));
44+
eprint!("{}", query.diagnostics().render(query.source_map()));
4545
std::process::exit(1);
4646
}
4747

4848
// Link query against language
4949
if !query.is_valid() {
50-
eprint!("{}", query.diagnostics().render(&query_source));
50+
eprint!("{}", query.diagnostics().render(query.source_map()));
5151
std::process::exit(1);
5252
}
5353

crates/plotnik-lib/src/diagnostics/message.rs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use rowan::TextRange;
22

3+
use super::{SourceId, Span};
4+
35
/// Diagnostic kinds ordered by priority (highest priority first).
46
///
57
/// When two diagnostics have overlapping spans, the higher-priority one
@@ -282,14 +284,14 @@ impl Fix {
282284

283285
#[derive(Debug, Clone, PartialEq, Eq)]
284286
pub struct RelatedInfo {
285-
pub(crate) range: TextRange,
287+
pub(crate) span: Span,
286288
pub(crate) message: String,
287289
}
288290

289291
impl RelatedInfo {
290-
pub fn new(range: TextRange, message: impl Into<String>) -> Self {
292+
pub fn new(source: SourceId, range: TextRange, message: impl Into<String>) -> Self {
291293
Self {
292-
range,
294+
span: Span::new(source, range),
293295
message: message.into(),
294296
}
295297
}
@@ -298,6 +300,8 @@ impl RelatedInfo {
298300
#[derive(Debug, Clone, PartialEq, Eq)]
299301
pub(crate) struct DiagnosticMessage {
300302
pub(crate) kind: DiagnosticKind,
303+
/// Which source file this diagnostic belongs to.
304+
pub(crate) source: SourceId,
301305
/// The range shown to the user (underlined in output).
302306
pub(crate) range: TextRange,
303307
/// The range used for suppression logic. Errors within another error's
@@ -312,9 +316,15 @@ pub(crate) struct DiagnosticMessage {
312316
}
313317

314318
impl DiagnosticMessage {
315-
pub(crate) fn new(kind: DiagnosticKind, range: TextRange, message: impl Into<String>) -> Self {
319+
pub(crate) fn new(
320+
source: SourceId,
321+
kind: DiagnosticKind,
322+
range: TextRange,
323+
message: impl Into<String>,
324+
) -> Self {
316325
Self {
317326
kind,
327+
source,
318328
range,
319329
suppression_range: range,
320330
message: message.into(),
@@ -324,8 +334,12 @@ impl DiagnosticMessage {
324334
}
325335
}
326336

327-
pub(crate) fn with_default_message(kind: DiagnosticKind, range: TextRange) -> Self {
328-
Self::new(kind, range, kind.fallback_message())
337+
pub(crate) fn with_default_message(
338+
source: SourceId,
339+
kind: DiagnosticKind,
340+
range: TextRange,
341+
) -> Self {
342+
Self::new(source, kind, range, kind.fallback_message())
329343
}
330344

331345
pub(crate) fn severity(&self) -> Severity {
@@ -359,8 +373,8 @@ impl std::fmt::Display for DiagnosticMessage {
359373
f,
360374
" (related: {} at {}..{})",
361375
related.message,
362-
u32::from(related.range.start()),
363-
u32::from(related.range.end())
376+
u32::from(related.span.range.start()),
377+
u32::from(related.span.range.end())
364378
)?;
365379
}
366380
for hint in &self.hints {

crates/plotnik-lib/src/diagnostics/mod.rs

Lines changed: 50 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,22 @@ pub use printer::DiagnosticsPrinter;
1111

1212
use message::{DiagnosticMessage, Fix, RelatedInfo};
1313

14+
// Re-export from query module
15+
pub use crate::query::{SourceId, SourceMap};
16+
17+
/// A location that knows which source it belongs to.
18+
#[derive(Clone, Debug, PartialEq, Eq)]
19+
pub struct Span {
20+
pub source: SourceId,
21+
pub range: TextRange,
22+
}
23+
24+
impl Span {
25+
pub fn new(source: SourceId, range: TextRange) -> Self {
26+
Self { source, range }
27+
}
28+
}
29+
1430
#[derive(Debug, Clone, Default)]
1531
pub struct Diagnostics {
1632
messages: Vec<DiagnosticMessage>,
@@ -32,26 +48,15 @@ impl Diagnostics {
3248
/// Create a diagnostic with the given kind and span.
3349
///
3450
/// Uses the kind's default message. Call `.message()` on the builder to override.
35-
pub fn report(&mut self, kind: DiagnosticKind, range: TextRange) -> DiagnosticBuilder<'_> {
36-
DiagnosticBuilder {
37-
diagnostics: self,
38-
message: DiagnosticMessage::with_default_message(kind, range),
39-
}
40-
}
41-
42-
/// Create an error diagnostic (legacy API, prefer `report()`).
43-
pub fn error(&mut self, msg: impl Into<String>, range: TextRange) -> DiagnosticBuilder<'_> {
44-
DiagnosticBuilder {
45-
diagnostics: self,
46-
message: DiagnosticMessage::new(DiagnosticKind::UnexpectedToken, range, msg),
47-
}
48-
}
49-
50-
/// Create a warning diagnostic (legacy API, prefer `report()`).
51-
pub fn warning(&mut self, msg: impl Into<String>, range: TextRange) -> DiagnosticBuilder<'_> {
51+
pub fn report(
52+
&mut self,
53+
source: SourceId,
54+
kind: DiagnosticKind,
55+
range: TextRange,
56+
) -> DiagnosticBuilder<'_> {
5257
DiagnosticBuilder {
5358
diagnostics: self,
54-
message: DiagnosticMessage::new(DiagnosticKind::UnexpectedToken, range, msg),
59+
message: DiagnosticMessage::with_default_message(source, kind, range),
5560
}
5661
}
5762

@@ -163,29 +168,34 @@ impl Diagnostics {
163168
&self.messages
164169
}
165170

166-
pub fn printer<'a>(&self, source: &'a str) -> DiagnosticsPrinter<'a> {
167-
DiagnosticsPrinter::new(self.messages.clone(), source)
171+
/// Create a printer with a source map (multi-file support).
172+
pub fn printer<'a>(&self, sources: &'a SourceMap) -> DiagnosticsPrinter<'a> {
173+
DiagnosticsPrinter::new(self.messages.clone(), sources)
168174
}
169175

170-
/// Printer that uses filtered diagnostics (cascading errors suppressed).
171-
pub fn filtered_printer<'a>(&self, source: &'a str) -> DiagnosticsPrinter<'a> {
172-
DiagnosticsPrinter::new(self.filtered(), source)
176+
/// Filtered printer with source map (cascading errors suppressed).
177+
pub fn filtered_printer<'a>(&self, sources: &'a SourceMap) -> DiagnosticsPrinter<'a> {
178+
DiagnosticsPrinter::new(self.filtered(), sources)
173179
}
174180

175-
pub fn render(&self, source: &str) -> String {
176-
self.printer(source).render()
181+
/// Render with source map.
182+
pub fn render(&self, sources: &SourceMap) -> String {
183+
self.printer(sources).render()
177184
}
178185

179-
pub fn render_colored(&self, source: &str, colored: bool) -> String {
180-
self.printer(source).colored(colored).render()
186+
/// Render with source map, colored output.
187+
pub fn render_colored(&self, sources: &SourceMap, colored: bool) -> String {
188+
self.printer(sources).colored(colored).render()
181189
}
182190

183-
pub fn render_filtered(&self, source: &str) -> String {
184-
self.filtered_printer(source).render()
191+
/// Render filtered with source map.
192+
pub fn render_filtered(&self, sources: &SourceMap) -> String {
193+
self.filtered_printer(sources).render()
185194
}
186195

187-
pub fn render_filtered_colored(&self, source: &str, colored: bool) -> String {
188-
self.filtered_printer(source).colored(colored).render()
196+
/// Render filtered with source map, colored output.
197+
pub fn render_filtered_colored(&self, sources: &SourceMap, colored: bool) -> String {
198+
self.filtered_printer(sources).colored(colored).render()
189199
}
190200

191201
pub fn extend(&mut self, other: Diagnostics) {
@@ -201,8 +211,15 @@ impl<'a> DiagnosticBuilder<'a> {
201211
self
202212
}
203213

204-
pub fn related_to(mut self, msg: impl Into<String>, range: TextRange) -> Self {
205-
self.message.related.push(RelatedInfo::new(range, msg));
214+
pub fn related_to(
215+
mut self,
216+
source: SourceId,
217+
range: TextRange,
218+
msg: impl Into<String>,
219+
) -> Self {
220+
self.message
221+
.related
222+
.push(RelatedInfo::new(source, range, msg));
206223
self
207224
}
208225

crates/plotnik-lib/src/diagnostics/printer.rs

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,24 @@ use std::fmt::Write;
55
use annotate_snippets::{AnnotationKind, Group, Level, Patch, Renderer, Snippet};
66
use rowan::TextRange;
77

8+
use super::SourceMap;
89
use super::message::{DiagnosticMessage, Severity};
910

1011
pub struct DiagnosticsPrinter<'a> {
1112
diagnostics: Vec<DiagnosticMessage>,
12-
source: &'a str,
13-
path: Option<&'a str>,
13+
sources: &'a SourceMap,
1414
colored: bool,
1515
}
1616

1717
impl<'a> DiagnosticsPrinter<'a> {
18-
pub(crate) fn new(diagnostics: Vec<DiagnosticMessage>, source: &'a str) -> Self {
18+
pub(crate) fn new(diagnostics: Vec<DiagnosticMessage>, sources: &'a SourceMap) -> Self {
1919
Self {
2020
diagnostics,
21-
source,
22-
path: None,
21+
sources,
2322
colored: false,
2423
}
2524
}
2625

27-
pub fn path(mut self, path: &'a str) -> Self {
28-
self.path = Some(path);
29-
self
30-
}
31-
3226
pub fn colored(mut self, value: bool) -> Self {
3327
self.colored = value;
3428
self
@@ -48,33 +42,56 @@ impl<'a> DiagnosticsPrinter<'a> {
4842
};
4943

5044
for (i, diag) in self.diagnostics.iter().enumerate() {
51-
let range = adjust_range(diag.range, self.source.len());
45+
let primary_content = self.sources.content(diag.source);
46+
let range = adjust_range(diag.range, primary_content.len());
5247

53-
let mut snippet = Snippet::source(self.source)
54-
.line_start(1)
55-
.annotation(AnnotationKind::Primary.span(range.clone()));
56-
57-
if let Some(p) = self.path {
58-
snippet = snippet.path(p);
48+
let mut primary_snippet = Snippet::source(primary_content).line_start(1);
49+
if let Some(name) = self.source_path(diag.source) {
50+
primary_snippet = primary_snippet.path(name);
5951
}
52+
primary_snippet =
53+
primary_snippet.annotation(AnnotationKind::Primary.span(range.clone()));
54+
55+
// Collect same-file and cross-file related info separately
56+
let mut cross_file_snippets = Vec::new();
6057

6158
for related in &diag.related {
62-
snippet = snippet.annotation(
63-
AnnotationKind::Context
64-
.span(adjust_range(related.range, self.source.len()))
65-
.label(&related.message),
66-
);
59+
if related.span.source == diag.source {
60+
// Same file: add annotation to primary snippet
61+
primary_snippet = primary_snippet.annotation(
62+
AnnotationKind::Context
63+
.span(adjust_range(related.span.range, primary_content.len()))
64+
.label(&related.message),
65+
);
66+
} else {
67+
// Different file: create separate snippet
68+
let related_content = self.sources.content(related.span.source);
69+
let mut snippet = Snippet::source(related_content).line_start(1);
70+
if let Some(name) = self.source_path(related.span.source) {
71+
snippet = snippet.path(name);
72+
}
73+
snippet = snippet.annotation(
74+
AnnotationKind::Context
75+
.span(adjust_range(related.span.range, related_content.len()))
76+
.label(&related.message),
77+
);
78+
cross_file_snippets.push(snippet);
79+
}
6780
}
6881

6982
let level = severity_to_level(diag.severity());
70-
let title_group = level.primary_title(&diag.message).element(snippet);
83+
let mut title_group = level.primary_title(&diag.message).element(primary_snippet);
84+
85+
for snippet in cross_file_snippets {
86+
title_group = title_group.element(snippet);
87+
}
7188

7289
let mut report: Vec<Group> = vec![title_group];
7390

7491
if let Some(fix) = &diag.fix {
7592
report.push(
7693
Level::HELP.secondary_title(&fix.description).element(
77-
Snippet::source(self.source)
94+
Snippet::source(primary_content)
7895
.line_start(1)
7996
.patch(Patch::new(range, &fix.replacement)),
8097
),
@@ -93,6 +110,10 @@ impl<'a> DiagnosticsPrinter<'a> {
93110

94111
Ok(())
95112
}
113+
114+
fn source_path(&self, source: crate::query::SourceId) -> Option<&'a str> {
115+
self.sources.path(source)
116+
}
96117
}
97118

98119
fn severity_to_level(severity: Severity) -> Level<'static> {

0 commit comments

Comments
 (0)