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}