Skip to main content

demo/
demo.rs

1use duck_diagnostic::*;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4enum LangError {
5  UnterminatedString,
6  UnexpectedToken,
7  TypeMismatch,
8  UnusedVariable,
9}
10
11impl DiagnosticCode for LangError {
12  fn code(&self) -> &str {
13    match self {
14      Self::UnterminatedString => "E0001",
15      Self::UnexpectedToken => "E0100",
16      Self::TypeMismatch => "E0201",
17      Self::UnusedVariable => "W0001",
18    }
19  }
20  fn severity(&self) -> Severity {
21    match self {
22      Self::UnusedVariable => Severity::Warning,
23      _ => Severity::Error,
24    }
25  }
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29enum SqlError {
30  UnknownColumn,
31  DivisionByZero,
32  FullTableScan,
33}
34
35impl DiagnosticCode for SqlError {
36  fn code(&self) -> &str {
37    match self {
38      Self::UnknownColumn => "SQL0003",
39      Self::DivisionByZero => "SQL0006",
40      Self::FullTableScan => "SQL-W001",
41    }
42  }
43  fn severity(&self) -> Severity {
44    match self {
45      Self::FullTableScan => Severity::Warning,
46      _ => Severity::Error,
47    }
48  }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52enum ConfigError {
53  DuplicateKey,
54  InvalidValue,
55  DeprecatedField,
56}
57
58impl DiagnosticCode for ConfigError {
59  fn code(&self) -> &str {
60    match self {
61      Self::DuplicateKey => "CFG004",
62      Self::InvalidValue => "CFG003",
63      Self::DeprecatedField => "CFG-W01",
64    }
65  }
66  fn severity(&self) -> Severity {
67    match self {
68      Self::DeprecatedField => Severity::Warning,
69      _ => Severity::Error,
70    }
71  }
72}
73
74fn demo_compiler() {
75  println!("[compiler]\n");
76
77  let source = r#"fn main() {
78    let name = "hello
79    let x = 42;
80    let result = a + b * c;
81    println(name);
82}"#;
83
84  let mut engine = DiagnosticEngine::<LangError>::new();
85
86  engine.emit(
87    Diagnostic::new(LangError::UnterminatedString, "unterminated string literal")
88      .with_label(Label::primary(
89        Span::new("main.lang", 2, 16, 6),
90        Some("string starts here but never closes".into()),
91      ))
92      .with_help("close the string with a matching `\"`"),
93  );
94
95  engine.emit(
96    Diagnostic::new(LangError::TypeMismatch, "mismatched types in expression")
97      .with_label(Label::primary(
98        Span::new("main.lang", 4, 17, 1),
99        Some("this is a string".into()),
100      ))
101      .with_label(Label::secondary(
102        Span::new("main.lang", 4, 21, 1),
103        Some("this is an int".into()),
104      ))
105      .with_note("cannot add `String` and `i32`")
106      .with_help("convert one side: `a.parse::<i32>()`"),
107  );
108
109  engine.emit(
110    Diagnostic::new(LangError::UnusedVariable, "unused variable `x`")
111      .with_label(Label::primary(
112        Span::new("main.lang", 3, 8, 1),
113        Some("declared here but never used".into()),
114      ))
115      .with_help("prefix with `_` to silence: `_x`"),
116  );
117
118  engine.print_all(source);
119}
120
121fn demo_sql() {
122  println!("\n[sql engine]\n");
123
124  let query = r#"SELECT u.name, o.total
125FROM users u
126JOIN orders o ON u.id = o.user_id
127WHERE u.age / 0 > 10
128  AND o.status = active"#;
129
130  let mut engine = DiagnosticEngine::<SqlError>::new();
131
132  engine.emit(
133    Diagnostic::new(SqlError::DivisionByZero, "division by zero in expression")
134      .with_label(Label::primary(
135        Span::new("query.sql", 4, 6, 11),
136        Some("this will always fail at runtime".into()),
137      ))
138      .with_note("division by a literal zero is never valid"),
139  );
140
141  engine.emit(
142    Diagnostic::new(SqlError::UnknownColumn, "unknown column `active`")
143      .with_label(Label::primary(
144        Span::new("query.sql", 5, 18, 6),
145        Some("not a known column".into()),
146      ))
147      .with_help("did you mean the string `'active'`?"),
148  );
149
150  engine.emit(
151    Diagnostic::new(SqlError::FullTableScan, "query requires a full table scan on `users`")
152      .with_label(Label::primary(
153        Span::new("query.sql", 2, 5, 7),
154        Some("no index on `users.age`".into()),
155      ))
156      .with_help("consider adding an index: CREATE INDEX idx_users_age ON users(age)"),
157  );
158
159  engine.print_all(query);
160}
161
162fn demo_config() {
163  println!("\n[config linter]\n");
164
165  let config = r#"[package]
166name = "my-app"
167version = "1.0"
168edition = "2018"
169authors = ["me"]
170authors = ["you"]
171license = 42"#;
172
173  let mut engine = DiagnosticEngine::<ConfigError>::new();
174
175  engine.emit(
176    Diagnostic::new(ConfigError::DuplicateKey, "duplicate key `authors`")
177      .with_label(Label::primary(
178        Span::new("Cargo.toml", 6, 0, 7),
179        Some("second definition here".into()),
180      ))
181      .with_label(Label::secondary(
182        Span::new("Cargo.toml", 5, 0, 7),
183        Some("first defined here".into()),
184      ))
185      .with_help("remove one of the duplicate entries"),
186  );
187
188  engine.emit(
189    Diagnostic::new(ConfigError::InvalidValue, "expected string for `license`, found integer")
190      .with_label(Label::primary(
191        Span::new("Cargo.toml", 7, 10, 2),
192        Some("expected a string like \"MIT\"".into()),
193      )),
194  );
195
196  engine.emit(
197    Diagnostic::new(ConfigError::DeprecatedField, "`edition = \"2018\"` is outdated")
198      .with_label(Label::primary(
199        Span::new("Cargo.toml", 4, 0, 18),
200        Some("consider updating to \"2021\" or \"2024\"".into()),
201      )),
202  );
203
204  engine.print_all(config);
205}
206
207fn main() {
208  demo_compiler();
209  demo_sql();
210  demo_config();
211}