Skip to main content

espforge_diagnostics/
codes.rs

1//! The stable diagnostic codes, one per message.
2//!
3//! The ranges are reserved by stage (see the crate docs). Keeping the map in
4//! one place is what stops two stages from picking the same number: a new
5//! message gets a new entry here, and [`for_message`] is the only producer of
6//! a code for the `EspfError` shim.
7//!
8//! Codes are data: the human renderer does not print them, `to_json` does.
9//! Once a code has shipped it is a public contract, so never renumber one for
10//! a different message.
11
12use crate::DiagCode;
13
14/// A parser-junk line that is not a node.
15pub const SYNTAX_NODE_LINE: DiagCode = DiagCode::new("E0101");
16/// A line that should have been a `# section` header.
17pub const SYNTAX_SECTION_HEADER: DiagCode = DiagCode::new("E0102");
18/// Two `# section` blocks with the same name.
19pub const STRUCTURE_DUPLICATE_SECTION: DiagCode = DiagCode::new("E0103");
20/// A required `# section` block is absent.
21pub const STRUCTURE_MISSING_SECTION: DiagCode = DiagCode::new("E0104");
22
23/// An element the `# espforge` schema does not know.
24pub const CORE_UNKNOWN_ELEMENT: DiagCode = DiagCode::new("E0201");
25/// `# espforge` lacks `<project>`, `<chip>` or `<runtime>`.
26pub const CORE_INCOMPLETE: DiagCode = DiagCode::new("E0202");
27/// The same element is written twice in `# espforge`.
28pub const CORE_DUPLICATE_ELEMENT: DiagCode = DiagCode::new("E0203");
29/// `<chip>` without a `type` attribute.
30pub const CORE_CHIP_WITHOUT_TYPE: DiagCode = DiagCode::new("E0204");
31/// A chip name the catalog does not know.
32pub const CORE_UNKNOWN_CHIP: DiagCode = DiagCode::new("E0205");
33/// `<runtime>` without a `type` attribute.
34pub const CORE_RUNTIME_WITHOUT_TYPE: DiagCode = DiagCode::new("E0206");
35/// A runtime name the catalog does not know.
36pub const CORE_UNKNOWN_RUNTIME: DiagCode = DiagCode::new("E0207");
37/// `<console>` without a `type` attribute.
38pub const CORE_CONSOLE_WITHOUT_TYPE: DiagCode = DiagCode::new("E0208");
39/// A console name the catalog does not know.
40pub const CORE_UNKNOWN_CONSOLE: DiagCode = DiagCode::new("E0209");
41
42/// An option name that is not in the schema.
43pub const OPTION_UNKNOWN: DiagCode = DiagCode::new("E0301");
44/// An option value outside its declared choices.
45pub const OPTION_INVALID_VALUE: DiagCode = DiagCode::new("E0302");
46/// An option value that cannot be set in `<options>`.
47pub const OPTION_DERIVED: DiagCode = DiagCode::new("E0306");
48/// An option that needs another option that is off.
49pub const OPTION_REQUIRES: DiagCode = DiagCode::new("E0307");
50/// An option that the selected chip cannot provide.
51pub const OPTION_UNAVAILABLE: DiagCode = DiagCode::new("E0308");
52/// Two options in one selection group are on together.
53pub const OPTION_MUTUALLY_EXCLUSIVE: DiagCode = DiagCode::new("E0309");
54/// The selected console does not exist on the chip.
55pub const OPTION_NO_CONSOLE: DiagCode = DiagCode::new("E0310");
56/// An attribute value with the wrong type.
57pub const VALUE_WRONG_TYPE: DiagCode = DiagCode::new("E0311");
58/// A value that should be a boolean.
59pub const VALUE_NOT_BOOLEAN: DiagCode = DiagCode::new("E0312");
60/// A value that should be an unsigned number.
61pub const VALUE_NOT_NUMBER: DiagCode = DiagCode::new("E0313");
62/// A key used both as a plain value and as an object.
63pub const VALUE_KEY_KIND: DiagCode = DiagCode::new("E0314");
64/// A required attribute is absent.
65pub const VALUE_MISSING_ATTRIBUTE: DiagCode = DiagCode::new("E0315");
66/// A command-line answer that cannot be applied (no source span).
67pub const CLI_ANSWER: DiagCode = DiagCode::new("E0399");
68
69/// A `$` reference to a peripheral id that does not exist.
70pub const GRAPH_UNKNOWN_PERIPHERAL: DiagCode = DiagCode::new("E0401");
71/// A `$` reference to a component id that does not exist.
72pub const GRAPH_UNKNOWN_COMPONENT: DiagCode = DiagCode::new("E0402");
73/// A peripheral without an id.
74pub const GRAPH_PERIPHERAL_WITHOUT_ID: DiagCode = DiagCode::new("E0403");
75/// A component without an id.
76pub const GRAPH_COMPONENT_WITHOUT_ID: DiagCode = DiagCode::new("E0404");
77/// A component without its `i2c=`/`spi=`/`gpio=`/`uart=` link.
78pub const GRAPH_COMPONENT_WITHOUT_LINK: DiagCode = DiagCode::new("E0405");
79/// A device without an id.
80pub const GRAPH_DEVICE_WITHOUT_ID: DiagCode = DiagCode::new("E0406");
81/// A device without its component mapping.
82pub const GRAPH_DEVICE_WITHOUT_MAPPING: DiagCode = DiagCode::new("E0407");
83/// A pin attribute that is not a 0..=255 number.
84pub const GRAPH_INVALID_PIN: DiagCode = DiagCode::new("E0408");
85
86/// A peripheral tag `build` does not lower yet.
87pub const PERIPHERAL_UNSUPPORTED: DiagCode = DiagCode::new("E0501");
88/// `<gpio pin>` that is not a number.
89pub const GPIO_INVALID_PIN: DiagCode = DiagCode::new("E0502");
90/// `<gpio direction>` outside `input`/`output`.
91pub const GPIO_INVALID_DIRECTION: DiagCode = DiagCode::new("E0503");
92/// An `<i2c>` id other than i2c0..i2c2.
93pub const I2C_INVALID_ID: DiagCode = DiagCode::new("E0504");
94/// An `<i2c>` pin that is not a number.
95pub const I2C_INVALID_PIN: DiagCode = DiagCode::new("E0505");
96/// An `<i2c frequency>` that is not `NkHz`.
97pub const I2C_INVALID_FREQUENCY: DiagCode = DiagCode::new("E0506");
98/// An `<spi>` id other than spi2.
99pub const SPI_INVALID_ID: DiagCode = DiagCode::new("E0507");
100/// An `<spi>` pin that is not a number.
101pub const SPI_INVALID_PIN: DiagCode = DiagCode::new("E0508");
102/// An `<spi mode>` outside 0..=3.
103pub const SPI_INVALID_MODE: DiagCode = DiagCode::new("E0509");
104/// An `<spi frequency>` that is not `NkHz`.
105pub const SPI_INVALID_FREQUENCY: DiagCode = DiagCode::new("E0510");
106/// A `<uart>` id other than uart0..uart3.
107pub const UART_INVALID_ID: DiagCode = DiagCode::new("E0511");
108/// An explicit `<uart uart>` outside UART0..UART3.
109pub const UART_INVALID_PERIPHERAL: DiagCode = DiagCode::new("E0512");
110/// A `<uart>` pin that is not a number.
111pub const UART_INVALID_PIN: DiagCode = DiagCode::new("E0513");
112
113/// An `<led>` that cannot be built.
114pub const COMPONENT_INVALID_LED: DiagCode = DiagCode::new("E0601");
115/// A `<button>` that cannot be built.
116pub const COMPONENT_INVALID_BUTTON: DiagCode = DiagCode::new("E0602");
117/// An `<i2c_component>` that cannot be built.
118pub const COMPONENT_INVALID_I2C: DiagCode = DiagCode::new("E0603");
119/// An `<spi_component>` that cannot be built.
120pub const COMPONENT_INVALID_SPI: DiagCode = DiagCode::new("E0604");
121/// A `<uart_component>` that cannot be built.
122pub const COMPONENT_INVALID_UART: DiagCode = DiagCode::new("E0605");
123/// A component tag `build` does not lower yet.
124pub const COMPONENT_UNSUPPORTED: DiagCode = DiagCode::new("E0606");
125/// A chip-select attribute that is neither a pin nor a gpio id.
126pub const COMPONENT_INVALID_CS: DiagCode = DiagCode::new("E0607");
127/// A `<led>`/`<button>` referencing a gpio id that does not exist.
128pub const COMPONENT_UNKNOWN_GPIO: DiagCode = DiagCode::new("E0608");
129/// A led on an input pin or a button on an output pin.
130pub const COMPONENT_WRONG_DIRECTION: DiagCode = DiagCode::new("E0609");
131/// `<uart_component baud_rate="0">`.
132pub const COMPONENT_INVALID_BAUD: DiagCode = DiagCode::new("E0610");
133/// Two `<i2c_component>`s on one `<i2c>`.
134pub const COMPONENT_I2C_BUS_USED: DiagCode = DiagCode::new("E0611");
135/// Two `<uart_component>`s on one `<uart>`.
136pub const COMPONENT_UART_USED: DiagCode = DiagCode::new("E0612");
137/// A component pointing at a bus that is not a bus of that kind.
138pub const COMPONENT_UNKNOWN_BUS: DiagCode = DiagCode::new("E0613");
139/// A component id that cannot become a Rust identifier.
140pub const COMPONENT_INVALID_ID: DiagCode = DiagCode::new("E0614");
141
142/// Two elements on one pin.
143pub const CONFLICT_PIN: DiagCode = DiagCode::new("E0701");
144/// A pin claimed by two elements of the same kind.
145pub const CONFLICT_PIN_USED: DiagCode = DiagCode::new("E0702");
146/// One element needs an input and the other an output.
147pub const CONFLICT_DIRECTION: DiagCode = DiagCode::new("E0703");
148/// Two elements on one `<uart>` peripheral.
149pub const CONFLICT_PERIPHERAL: DiagCode = DiagCode::new("E0704");
150/// An `<i2c>` bus whose SDA and SCL are the same pin.
151pub const CONFLICT_I2C_WIRING: DiagCode = DiagCode::new("E0705");
152/// An `<spi>` bus whose SCLK and MOSI are the same pin.
153pub const CONFLICT_SPI_WIRING: DiagCode = DiagCode::new("E0706");
154/// A `<uart>` whose TX and RX are the same pin.
155pub const CONFLICT_UART_WIRING: DiagCode = DiagCode::new("E0707");
156
157/// `# devices` entries are not lowered yet.
158pub const SERVICE_UNSUPPORTED_DEVICES: DiagCode = DiagCode::new("E0801");
159/// A service other than `<wifi>`.
160pub const SERVICE_UNSUPPORTED: DiagCode = DiagCode::new("E0802");
161/// A `<wifi>` field other than network1.ssid/password.
162pub const SERVICE_UNSUPPORTED_FIELD: DiagCode = DiagCode::new("E0803");
163/// `<wifi>` without an ssid.
164pub const SERVICE_MISSING_SSID: DiagCode = DiagCode::new("E0804");
165/// An ssid outside 1..=32 bytes.
166pub const SERVICE_INVALID_SSID: DiagCode = DiagCode::new("E0805");
167/// A password that is neither empty nor 8..=63 characters.
168pub const SERVICE_INVALID_PASSWORD: DiagCode = DiagCode::new("E0806");
169/// A wifi value that cannot be a Rust string literal.
170pub const SERVICE_INVALID_VALUE: DiagCode = DiagCode::new("E0807");
171/// A second `<wifi>` in `# services`.
172pub const SERVICE_DUPLICATE_WIFI: DiagCode = DiagCode::new("E0808");
173/// A device tag with no compile-time driver.
174pub const DRIVER_UNKNOWN: DiagCode = DiagCode::new("E0809");
175
176/// The fallback for a message the table does not know.
177///
178/// Reaching it is a bug: every producer must have an entry above. The JSON
179/// contract test (`espforge/tests/check_json.rs`) fails if one reaches output.
180pub const UNCLASSIFIED: DiagCode = DiagCode::new("E0999");
181
182/// The code for a command-line answer that never enters the source pipeline.
183///
184/// The human output for these stays the one-line `❌ ...`; only JSON needs a
185/// code. The flag errors reuse the core/option code they belong to.
186pub fn for_cli_message(message: &str) -> DiagCode {
187    if message.starts_with("unknown chip") {
188        return CORE_UNKNOWN_CHIP;
189    }
190    if message.starts_with("unknown option") {
191        return OPTION_UNKNOWN;
192    }
193    if message.starts_with("--option") || message.contains(" --option ") {
194        return OPTION_INVALID_VALUE;
195    }
196    CLI_ANSWER
197}
198
199/// The code for one diagnostic message.
200///
201/// Titles are stable user-facing strings; the few that carry an id or option
202/// name are matched by prefix/suffix.
203pub fn for_message(message: &str) -> DiagCode {
204    // Exact titles first. This is deliberately a table, not a heuristic:
205    // changing a title should force a decision about its code.
206    let exact = match message {
207        "malformed node line" => SYNTAX_NODE_LINE,
208        "invalid section header" => SYNTAX_SECTION_HEADER,
209        "# espforge is incomplete" => CORE_INCOMPLETE,
210        "<chip> is missing its type" => CORE_CHIP_WITHOUT_TYPE,
211        "unknown chip" => CORE_UNKNOWN_CHIP,
212        "<runtime> is missing its type" => CORE_RUNTIME_WITHOUT_TYPE,
213        "unknown runtime" => CORE_UNKNOWN_RUNTIME,
214        "<console> is missing its type" => CORE_CONSOLE_WITHOUT_TYPE,
215        "unknown console" => CORE_UNKNOWN_CONSOLE,
216        "unknown peripheral" => GRAPH_UNKNOWN_PERIPHERAL,
217        "unknown component" => GRAPH_UNKNOWN_COMPONENT,
218        "peripheral is missing an id" => GRAPH_PERIPHERAL_WITHOUT_ID,
219        "component is missing an id" => GRAPH_COMPONENT_WITHOUT_ID,
220        "component is missing its controller link" => GRAPH_COMPONENT_WITHOUT_LINK,
221        "device is missing an id" => GRAPH_DEVICE_WITHOUT_ID,
222        "device is missing its component mapping" => GRAPH_DEVICE_WITHOUT_MAPPING,
223        "invalid pin number" => GRAPH_INVALID_PIN,
224        "unsupported peripheral" => PERIPHERAL_UNSUPPORTED,
225        "invalid gpio pin" => GPIO_INVALID_PIN,
226        "invalid gpio direction" => GPIO_INVALID_DIRECTION,
227        "invalid i2c id" => I2C_INVALID_ID,
228        "invalid i2c pin" => I2C_INVALID_PIN,
229        "invalid i2c frequency" => I2C_INVALID_FREQUENCY,
230        "invalid spi id" => SPI_INVALID_ID,
231        "invalid spi pin" => SPI_INVALID_PIN,
232        "invalid spi mode" => SPI_INVALID_MODE,
233        "invalid spi frequency" => SPI_INVALID_FREQUENCY,
234        "invalid uart id" => UART_INVALID_ID,
235        "invalid uart peripheral" => UART_INVALID_PERIPHERAL,
236        "invalid uart pin" => UART_INVALID_PIN,
237        "invalid led" => COMPONENT_INVALID_LED,
238        "invalid button" => COMPONENT_INVALID_BUTTON,
239        "invalid i2c_component" => COMPONENT_INVALID_I2C,
240        "invalid spi_component" => COMPONENT_INVALID_SPI,
241        "invalid uart_component" => COMPONENT_INVALID_UART,
242        "unsupported component" => COMPONENT_UNSUPPORTED,
243        "invalid cs" => COMPONENT_INVALID_CS,
244        "unknown gpio" => COMPONENT_UNKNOWN_GPIO,
245        "wrong gpio direction" => COMPONENT_WRONG_DIRECTION,
246        "invalid baud_rate" => COMPONENT_INVALID_BAUD,
247        "i2c bus already used" => COMPONENT_I2C_BUS_USED,
248        "uart already used" => COMPONENT_UART_USED,
249        "pin conflict" => CONFLICT_PIN,
250        "pin already used" => CONFLICT_PIN_USED,
251        "pin direction clash" => CONFLICT_DIRECTION,
252        "peripheral clash" => CONFLICT_PERIPHERAL,
253        "i2c wiring" => CONFLICT_I2C_WIRING,
254        "spi wiring" => CONFLICT_SPI_WIRING,
255        "uart wiring" => CONFLICT_UART_WIRING,
256        "unsupported devices" => SERVICE_UNSUPPORTED_DEVICES,
257        "unsupported service" => SERVICE_UNSUPPORTED,
258        "unsupported wifi field" => SERVICE_UNSUPPORTED_FIELD,
259        "missing wifi ssid" => SERVICE_MISSING_SSID,
260        "invalid wifi ssid" => SERVICE_INVALID_SSID,
261        "invalid wifi password" => SERVICE_INVALID_PASSWORD,
262        "invalid wifi value" => SERVICE_INVALID_VALUE,
263        "duplicate wifi" => SERVICE_DUPLICATE_WIFI,
264        _ => return dynamic_code(message),
265    };
266    exact
267}
268
269/// Titles that embed an id, key or section name.
270fn dynamic_code(message: &str) -> DiagCode {
271    if message.starts_with("duplicate section `") {
272        return STRUCTURE_DUPLICATE_SECTION;
273    }
274    if message.starts_with("missing section `") {
275        return STRUCTURE_MISSING_SECTION;
276    }
277    if message.starts_with("unknown element <") {
278        return CORE_UNKNOWN_ELEMENT;
279    }
280    if message.starts_with("duplicate <") {
281        return CORE_DUPLICATE_ELEMENT;
282    }
283    if message.starts_with("unknown option `") {
284        return OPTION_UNKNOWN;
285    }
286    if message.ends_with("has an invalid value") && message.starts_with('`') {
287        return OPTION_INVALID_VALUE;
288    }
289    if message.ends_with("has the wrong type") && message.starts_with('`') {
290        return VALUE_WRONG_TYPE;
291    }
292    if message.ends_with("is not a boolean") && message.starts_with('`') {
293        return VALUE_NOT_BOOLEAN;
294    }
295    if message.ends_with("is not a number") && message.starts_with('`') {
296        return VALUE_NOT_NUMBER;
297    }
298    if message.ends_with("and an object") && message.contains("is both a") {
299        return VALUE_KEY_KIND;
300    }
301    if message.starts_with("missing required attribute `") {
302        return VALUE_MISSING_ATTRIBUTE;
303    }
304    if message.ends_with("cannot be set in <options>") && message.starts_with('`') {
305        return OPTION_DERIVED;
306    }
307    if message.ends_with("is not available for") || message.contains("is not available for ") {
308        return OPTION_UNAVAILABLE;
309    }
310    if message.contains(" requires `") && message.starts_with('`') {
311        return OPTION_REQUIRES;
312    }
313    if message.starts_with("options ") && message.ends_with("are mutually exclusive") {
314        return OPTION_MUTUALLY_EXCLUSIVE;
315    }
316    if message.ends_with("has no USB-Serial-JTAG") {
317        return OPTION_NO_CONSOLE;
318    }
319    if message.starts_with("unknown ") && message.ends_with(" bus") {
320        return COMPONENT_UNKNOWN_BUS;
321    }
322    if message.starts_with("invalid ") && message.ends_with("") {
323        // `invalid {label}` from require_bus; the exact labels are in the
324        // table above, so this catches the generic spelling.
325        if message == "invalid gpio" || message == "invalid i2c" || message == "invalid spi" {
326            return COMPONENT_INVALID_ID;
327        }
328    }
329    UNCLASSIFIED
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335
336    #[test]
337    fn every_known_title_has_its_own_code() {
338        let titles = [
339            "malformed node line",
340            "invalid section header",
341            "# espforge is incomplete",
342            "<chip> is missing its type",
343            "unknown chip",
344            "<runtime> is missing its type",
345            "unknown runtime",
346            "<console> is missing its type",
347            "unknown console",
348            "unknown option `wombat`",
349            "`alloc` has an invalid value",
350            "`alloc` cannot be set in <options>",
351            "`wifi` requires `alloc`",
352            "`wifi` is not available for esp32c3",
353            "options `a`, `b` are mutually exclusive",
354            "esp32c3 has no USB-Serial-JTAG",
355            "`network1.ssid` has the wrong type",
356            "`x` is not a boolean",
357            "`x` is not a number",
358            "`x` is both a string and an object",
359            "missing required attribute `pin`",
360            "unknown peripheral",
361            "unknown component",
362            "peripheral is missing an id",
363            "component is missing an id",
364            "component is missing its controller link",
365            "device is missing an id",
366            "device is missing its component mapping",
367            "invalid pin number",
368            "unsupported peripheral",
369            "invalid gpio pin",
370            "invalid gpio direction",
371            "invalid i2c id",
372            "invalid i2c pin",
373            "invalid i2c frequency",
374            "invalid spi id",
375            "invalid spi pin",
376            "invalid spi mode",
377            "invalid spi frequency",
378            "invalid uart id",
379            "invalid uart peripheral",
380            "invalid uart pin",
381            "invalid led",
382            "invalid button",
383            "invalid i2c_component",
384            "invalid spi_component",
385            "invalid uart_component",
386            "unsupported component",
387            "invalid cs",
388            "unknown gpio",
389            "wrong gpio direction",
390            "invalid baud_rate",
391            "i2c bus already used",
392            "uart already used",
393            "unknown i2c bus",
394            "pin conflict",
395            "pin already used",
396            "pin direction clash",
397            "peripheral clash",
398            "i2c wiring",
399            "spi wiring",
400            "uart wiring",
401            "unsupported devices",
402            "unsupported service",
403            "unsupported wifi field",
404            "missing wifi ssid",
405            "invalid wifi ssid",
406            "invalid wifi password",
407            "invalid wifi value",
408            "duplicate wifi",
409            "duplicate section `# peripherals`",
410            "missing section `# services`",
411            "unknown element <pwm> in # espforge",
412            "duplicate <chip> in # espforge",
413        ];
414
415        let mut seen = std::collections::BTreeSet::new();
416        for title in titles {
417            let code = for_message(title);
418            assert_ne!(code, UNCLASSIFIED, "{title} fell through");
419            assert!(
420                seen.insert(code.as_str()),
421                "{title} reuses code {}",
422                code.as_str()
423            );
424        }
425    }
426
427    #[test]
428    fn an_unknown_title_is_not_silently_given_a_real_code() {
429        assert_eq!(for_message("a message nobody wrote"), UNCLASSIFIED);
430    }
431}