Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,9 @@ fn valid_query() {
(function_declaration name: (identifier) @name)
"#};

let query = Query::try_from(input).unwrap();
let res = Query::expect_valid_ast(input).unwrap();

assert!(query.is_valid());
insta::assert_snapshot!(query.dump_ast(), @"");
insta::assert_snapshot!(res, @"");
}
```

Expand Down
6 changes: 4 additions & 2 deletions crates/plotnik-cli/src/commands/debug/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,10 @@ pub fn run(args: DebugArgs) {
if let Some(ref q) = query
&& !q.is_valid()
{
let src = query_source.as_ref().unwrap();
eprint!("{}", q.diagnostics().render_colored(src, args.color));
eprint!(
"{}",
q.diagnostics().render_colored(q.source_map(), args.color)
);
std::process::exit(1);
}
}
Expand Down
6 changes: 3 additions & 3 deletions crates/plotnik-cli/src/commands/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::fs;
use std::io::{self, Read};
use std::path::PathBuf;

use plotnik_lib::QueryBuilder;
use plotnik_lib::{QueryBuilder, SourceMap};

use super::debug::source::resolve_lang;

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

// Parse query
let query_parsed = QueryBuilder::new(&query_source)
let query_parsed = QueryBuilder::new(SourceMap::one_liner(&query_source))
.parse()
.unwrap_or_else(|e| {
eprintln!("error: {}", e);
Expand All @@ -46,7 +46,7 @@ pub fn run(args: ExecArgs) {
// Link query against language
let linked = query_analyzed.link(&lang);
if !linked.is_valid() {
eprint!("{}", linked.diagnostics().render(&query_source));
eprint!("{}", linked.diagnostics().render(linked.source_map()));
std::process::exit(1);
}

Expand Down
4 changes: 2 additions & 2 deletions crates/plotnik-cli/src/commands/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ pub fn run(args: TypesArgs) {
.link(&lang);

if !query.is_valid() {
eprint!("{}", query.diagnostics().render(&query_source));
eprint!("{}", query.diagnostics().render(query.source_map()));
std::process::exit(1);
}

// Link query against language
if !query.is_valid() {
eprint!("{}", query.diagnostics().render(&query_source));
eprint!("{}", query.diagnostics().render(query.source_map()));
std::process::exit(1);
}

Expand Down
30 changes: 22 additions & 8 deletions crates/plotnik-lib/src/diagnostics/message.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use rowan::TextRange;

use super::{SourceId, Span};

/// Diagnostic kinds ordered by priority (highest priority first).
///
/// When two diagnostics have overlapping spans, the higher-priority one
Expand Down Expand Up @@ -282,14 +284,14 @@ impl Fix {

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RelatedInfo {
pub(crate) range: TextRange,
pub(crate) span: Span,
pub(crate) message: String,
}

impl RelatedInfo {
pub fn new(range: TextRange, message: impl Into<String>) -> Self {
pub fn new(source: SourceId, range: TextRange, message: impl Into<String>) -> Self {
Self {
range,
span: Span::new(source, range),
message: message.into(),
}
}
Expand All @@ -298,6 +300,8 @@ impl RelatedInfo {
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DiagnosticMessage {
pub(crate) kind: DiagnosticKind,
/// Which source file this diagnostic belongs to.
pub(crate) source: SourceId,
/// The range shown to the user (underlined in output).
pub(crate) range: TextRange,
/// The range used for suppression logic. Errors within another error's
Expand All @@ -312,9 +316,15 @@ pub(crate) struct DiagnosticMessage {
}

impl DiagnosticMessage {
pub(crate) fn new(kind: DiagnosticKind, range: TextRange, message: impl Into<String>) -> Self {
pub(crate) fn new(
source: SourceId,
kind: DiagnosticKind,
range: TextRange,
message: impl Into<String>,
) -> Self {
Self {
kind,
source,
range,
suppression_range: range,
message: message.into(),
Expand All @@ -324,8 +334,12 @@ impl DiagnosticMessage {
}
}

pub(crate) fn with_default_message(kind: DiagnosticKind, range: TextRange) -> Self {
Self::new(kind, range, kind.fallback_message())
pub(crate) fn with_default_message(
source: SourceId,
kind: DiagnosticKind,
range: TextRange,
) -> Self {
Self::new(source, kind, range, kind.fallback_message())
}

pub(crate) fn severity(&self) -> Severity {
Expand Down Expand Up @@ -359,8 +373,8 @@ impl std::fmt::Display for DiagnosticMessage {
f,
" (related: {} at {}..{})",
related.message,
u32::from(related.range.start()),
u32::from(related.range.end())
u32::from(related.span.range.start()),
u32::from(related.span.range.end())
)?;
}
for hint in &self.hints {
Expand Down
83 changes: 50 additions & 33 deletions crates/plotnik-lib/src/diagnostics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,22 @@ pub use printer::DiagnosticsPrinter;

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

// Re-export from query module
pub use crate::query::{SourceId, SourceMap};

/// A location that knows which source it belongs to.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Span {
pub source: SourceId,
pub range: TextRange,
}

impl Span {
pub fn new(source: SourceId, range: TextRange) -> Self {
Self { source, range }
}
}

#[derive(Debug, Clone, Default)]
pub struct Diagnostics {
messages: Vec<DiagnosticMessage>,
Expand All @@ -32,26 +48,15 @@ impl Diagnostics {
/// Create a diagnostic with the given kind and span.
///
/// Uses the kind's default message. Call `.message()` on the builder to override.
pub fn report(&mut self, kind: DiagnosticKind, range: TextRange) -> DiagnosticBuilder<'_> {
DiagnosticBuilder {
diagnostics: self,
message: DiagnosticMessage::with_default_message(kind, range),
}
}

/// Create an error diagnostic (legacy API, prefer `report()`).
pub fn error(&mut self, msg: impl Into<String>, range: TextRange) -> DiagnosticBuilder<'_> {
DiagnosticBuilder {
diagnostics: self,
message: DiagnosticMessage::new(DiagnosticKind::UnexpectedToken, range, msg),
}
}

/// Create a warning diagnostic (legacy API, prefer `report()`).
pub fn warning(&mut self, msg: impl Into<String>, range: TextRange) -> DiagnosticBuilder<'_> {
pub fn report(
&mut self,
source: SourceId,
kind: DiagnosticKind,
range: TextRange,
) -> DiagnosticBuilder<'_> {
DiagnosticBuilder {
diagnostics: self,
message: DiagnosticMessage::new(DiagnosticKind::UnexpectedToken, range, msg),
message: DiagnosticMessage::with_default_message(source, kind, range),
}
}

Expand Down Expand Up @@ -163,29 +168,34 @@ impl Diagnostics {
&self.messages
}

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

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

pub fn render(&self, source: &str) -> String {
self.printer(source).render()
/// Render with source map.
pub fn render(&self, sources: &SourceMap) -> String {
self.printer(sources).render()
}

pub fn render_colored(&self, source: &str, colored: bool) -> String {
self.printer(source).colored(colored).render()
/// Render with source map, colored output.
pub fn render_colored(&self, sources: &SourceMap, colored: bool) -> String {
self.printer(sources).colored(colored).render()
}

pub fn render_filtered(&self, source: &str) -> String {
self.filtered_printer(source).render()
/// Render filtered with source map.
pub fn render_filtered(&self, sources: &SourceMap) -> String {
self.filtered_printer(sources).render()
}

pub fn render_filtered_colored(&self, source: &str, colored: bool) -> String {
self.filtered_printer(source).colored(colored).render()
/// Render filtered with source map, colored output.
pub fn render_filtered_colored(&self, sources: &SourceMap, colored: bool) -> String {
self.filtered_printer(sources).colored(colored).render()
}

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

pub fn related_to(mut self, msg: impl Into<String>, range: TextRange) -> Self {
self.message.related.push(RelatedInfo::new(range, msg));
pub fn related_to(
mut self,
source: SourceId,
range: TextRange,
msg: impl Into<String>,
) -> Self {
self.message
.related
.push(RelatedInfo::new(source, range, msg));
self
}

Expand Down
69 changes: 45 additions & 24 deletions crates/plotnik-lib/src/diagnostics/printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,24 @@ use std::fmt::Write;
use annotate_snippets::{AnnotationKind, Group, Level, Patch, Renderer, Snippet};
use rowan::TextRange;

use super::SourceMap;
use super::message::{DiagnosticMessage, Severity};

pub struct DiagnosticsPrinter<'a> {
diagnostics: Vec<DiagnosticMessage>,
source: &'a str,
path: Option<&'a str>,
sources: &'a SourceMap,
colored: bool,
}

impl<'a> DiagnosticsPrinter<'a> {
pub(crate) fn new(diagnostics: Vec<DiagnosticMessage>, source: &'a str) -> Self {
pub(crate) fn new(diagnostics: Vec<DiagnosticMessage>, sources: &'a SourceMap) -> Self {
Self {
diagnostics,
source,
path: None,
sources,
colored: false,
}
}

pub fn path(mut self, path: &'a str) -> Self {
self.path = Some(path);
self
}

pub fn colored(mut self, value: bool) -> Self {
self.colored = value;
self
Expand All @@ -48,33 +42,56 @@ impl<'a> DiagnosticsPrinter<'a> {
};

for (i, diag) in self.diagnostics.iter().enumerate() {
let range = adjust_range(diag.range, self.source.len());
let primary_content = self.sources.content(diag.source);
let range = adjust_range(diag.range, primary_content.len());

let mut snippet = Snippet::source(self.source)
.line_start(1)
.annotation(AnnotationKind::Primary.span(range.clone()));

if let Some(p) = self.path {
snippet = snippet.path(p);
let mut primary_snippet = Snippet::source(primary_content).line_start(1);
if let Some(name) = self.source_path(diag.source) {
primary_snippet = primary_snippet.path(name);
}
primary_snippet =
primary_snippet.annotation(AnnotationKind::Primary.span(range.clone()));

// Collect same-file and cross-file related info separately
let mut cross_file_snippets = Vec::new();

for related in &diag.related {
snippet = snippet.annotation(
AnnotationKind::Context
.span(adjust_range(related.range, self.source.len()))
.label(&related.message),
);
if related.span.source == diag.source {
// Same file: add annotation to primary snippet
primary_snippet = primary_snippet.annotation(
AnnotationKind::Context
.span(adjust_range(related.span.range, primary_content.len()))
.label(&related.message),
);
} else {
// Different file: create separate snippet
let related_content = self.sources.content(related.span.source);
let mut snippet = Snippet::source(related_content).line_start(1);
if let Some(name) = self.source_path(related.span.source) {
snippet = snippet.path(name);
}
snippet = snippet.annotation(
AnnotationKind::Context
.span(adjust_range(related.span.range, related_content.len()))
.label(&related.message),
);
cross_file_snippets.push(snippet);
}
}

let level = severity_to_level(diag.severity());
let title_group = level.primary_title(&diag.message).element(snippet);
let mut title_group = level.primary_title(&diag.message).element(primary_snippet);

for snippet in cross_file_snippets {
title_group = title_group.element(snippet);
}

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

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

Ok(())
}

fn source_path(&self, source: crate::query::SourceId) -> Option<&'a str> {
self.sources.path(source)
}
}

fn severity_to_level(severity: Severity) -> Level<'static> {
Expand Down
Loading