Skip to main content

plc_diagnostics/
diagnostics.rs

1use std::{
2    fmt::Display,
3    ops::{Deref, DerefMut},
4};
5
6use serde::{Deserialize, Serialize};
7
8use crate::diagnostics::diagnostics_registry::DIAGNOSTICS;
9use plc_ast::ast::AstNode;
10use plc_source::{
11    source_location::{SourceLocation, SourceLocationFactory},
12    SourceCode,
13};
14
15pub mod diagnostics_registry;
16
17pub const INTERNAL_LLVM_ERROR: &str = "internal llvm codegen error";
18
19/// a diagnostics severity
20#[derive(Default, Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
21#[serde(rename_all = "lowercase")]
22pub enum Severity {
23    Ignore,
24    #[default]
25    Info,
26    Warning,
27    Error,
28}
29
30/// The `Diagnostics` struct describes an issue encountered during compile time.
31/// The issue is defined by an `error_code` and had a defined `severity`
32/// Diagnostic severity can be overridden when being reported.
33#[derive(Debug)]
34pub struct Diagnostic {
35    pub inner: Box<DiagnosticsInner>,
36}
37
38#[derive(Debug)]
39pub struct DiagnosticsInner {
40    /// The Description of the error being reported.
41    pub message: String,
42    /// Primary location where the diagnostic occurred
43    pub primary_location: SourceLocation,
44    /// Seconday locations relevant to the diagnostics
45    pub secondary_locations: Option<Vec<SourceLocation>>,
46    /// Error code for reference in the documentation
47    pub error_code: &'static str,
48    /// Children of the current diagnostic
49    pub sub_diagnostics: Vec<Diagnostic>,
50    /// If the diagnostic is caused by an error, this field contains the original error
51    pub internal_error: Option<anyhow::Error>,
52}
53
54impl Deref for Diagnostic {
55    type Target = DiagnosticsInner;
56
57    fn deref(&self) -> &Self::Target {
58        &self.inner
59    }
60}
61
62impl DerefMut for Diagnostic {
63    fn deref_mut(&mut self) -> &mut Self::Target {
64        &mut self.inner
65    }
66}
67
68impl std::error::Error for Diagnostic {
69    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
70        self.internal_error.as_ref().and_then(|it| it.source())
71    }
72}
73
74impl From<std::io::Error> for Diagnostic {
75    fn from(value: std::io::Error) -> Self {
76        Diagnostic::new(value.to_string()).with_error_code("E002").with_internal_error(value.into())
77    }
78}
79
80/// Builder for Diagnostics
81impl Diagnostic {
82    pub fn new(message: impl Into<String>) -> Self {
83        let inner = DiagnosticsInner {
84            message: message.into(),
85            primary_location: SourceLocation::undefined(),
86            secondary_locations: Default::default(),
87            error_code: "E001", //Default error if none specified
88            sub_diagnostics: Default::default(),
89            internal_error: Default::default(),
90        };
91        Self { inner: Box::new(inner) }
92    }
93
94    pub fn with_location<T>(mut self, location: T) -> Self
95    where
96        T: Into<SourceLocation>,
97    {
98        self.primary_location = location.into();
99        self
100    }
101
102    pub fn with_secondary_location<T>(mut self, location: T) -> Self
103    where
104        T: Into<SourceLocation>,
105    {
106        self.secondary_locations.get_or_insert_with(Default::default).push(location.into());
107        self
108    }
109
110    pub fn with_secondary_locations(mut self, locations: Vec<SourceLocation>) -> Self {
111        self.secondary_locations.get_or_insert_with(Default::default).extend(locations);
112        self
113    }
114
115    pub fn with_error_code(mut self, code: &'static str) -> Self {
116        debug_assert!(DIAGNOSTICS.get(code).is_some(), "Error {code} does not exist");
117
118        self.error_code = code;
119        self
120    }
121
122    pub fn with_sub_diagnostic(mut self, diagnostic: Diagnostic) -> Self {
123        self.sub_diagnostics.push(diagnostic);
124        self
125    }
126
127    pub fn with_sub_diagnostics(mut self, diagnostics: Vec<Diagnostic>) -> Self {
128        self.sub_diagnostics.extend(diagnostics);
129        self
130    }
131
132    pub fn with_internal_error(mut self, error: anyhow::Error) -> Self {
133        self.internal_error = Some(error);
134        self
135    }
136
137    pub fn from_serde_error(error: serde_json::Error, source: &SourceCode) -> Self {
138        let factory = SourceLocationFactory::for_source(source);
139        let line = error.line();
140        let column = error.column();
141
142        // remove line, column from message
143        let message = error.to_string();
144        let message = if let Some(pos) = message.find("at line") {
145            message.chars().take(pos).collect()
146        } else {
147            message
148        };
149        let range = factory.create_range_to_end_of_line(line, column);
150        Diagnostic::new(message).with_error_code("E088").with_location(range)
151    }
152}
153
154impl PartialEq for Diagnostic {
155    fn eq(&self, other: &Self) -> bool {
156        self.error_code == other.error_code
157            && self.message == other.message
158            && self.primary_location == other.primary_location
159            && self.secondary_locations == other.secondary_locations
160            && self.sub_diagnostics == other.sub_diagnostics
161    }
162}
163
164impl Eq for Diagnostic {}
165
166impl Display for Diagnostic {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        write!(f, "{}", self.get_message())?;
169        let location = self.get_location();
170        if !location.is_undefined() {
171            write!(f, " at: {location}")
172        } else {
173            Ok(())
174        }
175    }
176}
177
178impl Diagnostic {
179    pub fn get_message(&self) -> &str {
180        self.message.as_str()
181    }
182
183    pub fn get_location(&self) -> SourceLocation {
184        self.primary_location.clone()
185    }
186
187    pub fn get_secondary_locations(&self) -> Option<&[SourceLocation]> {
188        self.secondary_locations.as_deref()
189    }
190
191    pub fn get_error_code(&self) -> &'static str {
192        self.error_code
193    }
194
195    pub fn get_sub_diagnostics(&self) -> Vec<&Diagnostic> {
196        let mut diagnostics = vec![];
197        self.sub_diagnostics.iter().for_each(|d| {
198            diagnostics.push(d);
199            diagnostics.extend(d.get_sub_diagnostics());
200        });
201        diagnostics
202    }
203}
204
205//Helper methods for diagnostics
206impl Diagnostic {
207    pub fn unexpected_token_found<T>(expected: &str, found: &str, location: T) -> Diagnostic
208    where
209        T: Into<SourceLocation>,
210    {
211        Diagnostic::new(format!("Unexpected token: expected {expected} but found {found}"))
212            .with_error_code("E007")
213            .with_location(location)
214    }
215
216    pub fn missing_function<T>(location: T) -> Diagnostic
217    where
218        T: Into<SourceLocation>,
219    {
220        Diagnostic::new("Cannot generate code outside of function context.")
221            .with_error_code("E072")
222            .with_location(location.into())
223    }
224
225    pub fn codegen_error<T, U>(message: T, location: U) -> Diagnostic
226    where
227        T: Into<String>,
228        U: Into<SourceLocation>,
229    {
230        Diagnostic::new(message).with_location(location.into()).with_error_code("E071")
231    }
232
233    pub fn llvm_error(file: &str, llvm_error: &str) -> Diagnostic {
234        Diagnostic::new(format!("{file}: Internal llvm error: {:}", llvm_error)).with_error_code("E005")
235    }
236
237    pub fn missing_token<T>(expected_token: &str, location: T) -> Diagnostic
238    where
239        T: Into<SourceLocation>,
240    {
241        Diagnostic::new(format!("Missing expected Token {expected_token}"))
242            .with_location(location)
243            .with_error_code("E006")
244    }
245
246    pub fn invalid_argument_count<T>(expected: usize, actual: usize, location: T) -> Diagnostic
247    where
248        T: Into<SourceLocation>,
249    {
250        // Let's be extra fancy here 🕺
251        fn message(value: usize) -> String {
252            match value {
253                1 => format!("{value} argument"),
254                _ => format!("{value} arguments"),
255            }
256        }
257
258        Diagnostic::new(format!(
259            "this POU takes {expected} but {actual} {was_or_were} supplied",
260            expected = message(expected),
261            actual = message(actual),
262            was_or_were = if actual == 1 { "was" } else { "were" }
263        ))
264        .with_error_code("E032")
265        .with_location(location.into())
266    }
267
268    pub fn unknown_type<T>(type_name: &str, location: T) -> Diagnostic
269    where
270        T: Into<SourceLocation>,
271    {
272        Diagnostic::new(format!("Unknown type: {type_name:}")).with_error_code("E052").with_location(location)
273    }
274
275    pub fn unresolved_reference<T>(reference: &str, location: T) -> Diagnostic
276    where
277        T: Into<SourceLocation>,
278    {
279        Diagnostic::new(format!("Could not resolve reference to {reference:}"))
280            .with_error_code("E048")
281            .with_location(location)
282    }
283
284    pub fn invalid_assignment<T>(right_type: &str, left_type: &str, location: T) -> Diagnostic
285    where
286        T: Into<SourceLocation>,
287    {
288        Diagnostic::new(format!("Invalid assignment: cannot assign '{right_type}' to '{left_type}'"))
289            .with_error_code("E037")
290            .with_location(location)
291    }
292
293    pub fn invalid_polymorphic_assignment<T>(right_type: &str, left_type: &str, location: T) -> Diagnostic
294    where
295        T: Into<SourceLocation>,
296    {
297        Diagnostic::new(format!(
298            "Invalid assignment: '{left_type}' and '{right_type}' are not related and cannot be used polymorphically"
299        ))
300        .with_error_code("E125")
301        .with_location(location)
302    }
303
304    pub fn invalid_interface_pou_assignment<T>(
305        pou_name: &str,
306        interface_name: &str,
307        location: T,
308    ) -> Diagnostic
309    where
310        T: Into<SourceLocation>,
311    {
312        Diagnostic::new(format!(
313            "Invalid assignment: '{pou_name}' does not implement interface '{interface_name}'"
314        ))
315        .with_error_code("E126")
316        .with_location(location)
317    }
318
319    pub fn invalid_interface_assignment<T>(source_iface: &str, target_iface: &str, location: T) -> Diagnostic
320    where
321        T: Into<SourceLocation>,
322    {
323        Diagnostic::new(format!(
324            "Invalid assignment: '{target_iface}' and '{source_iface}' are not related and cannot be used polymorphically"
325        ))
326        .with_error_code("E126")
327        .with_location(location)
328    }
329
330    pub fn invalid_direct_interface_call<T>(location: T) -> Diagnostic
331    where
332        T: Into<SourceLocation>,
333    {
334        Diagnostic::new("Interfaces cannot be called directly")
335            .with_error_code("E129")
336            .with_location(location)
337    }
338
339    pub fn cannot_generate_initializer<T>(variable_name: &str, location: T) -> Diagnostic
340    where
341        T: Into<SourceLocation>,
342    {
343        Self::new(format!(
344            "Cannot generate literal initializer for '{variable_name}': Value cannot be derived"
345        ))
346        .with_error_code("E041")
347        .with_location(location)
348    }
349
350    pub fn cannot_generate_call_statement(operator: &AstNode) -> Diagnostic {
351        //TODO: We could probably get a better slice here
352        Diagnostic::codegen_error(format!("cannot generate call statement for {:?}", operator), operator)
353    }
354
355    pub fn cannot_generate_from_empty_literal<T>(type_name: &str, location: T) -> Diagnostic
356    where
357        T: Into<SourceLocation>,
358    {
359        Diagnostic::codegen_error(
360            format!("Cannot generate {type_name} from empty literal").as_str(),
361            location,
362        )
363    }
364
365    pub fn const_pragma_is_not_allowed<T>(location: T) -> Diagnostic
366    where
367        T: Into<SourceLocation>,
368    {
369        Diagnostic::new("Pragma {constant} is not allowed in POU declarations")
370            .with_location(location)
371            .with_error_code("E105")
372    }
373}
374
375// CFC related diagnostics
376impl Diagnostic {
377    pub fn unnamed_control<T>(location: T) -> Diagnostic
378    where
379        T: Into<SourceLocation>,
380    {
381        Diagnostic::new("Unnamed control").with_error_code("E087").with_location(location)
382    }
383
384    pub fn unsupported_cfc_expression<T>(expression: &str, location: T) -> Diagnostic
385    where
386        T: Into<SourceLocation>,
387    {
388        Diagnostic::new(format!("Unsupported CFC expression: `{expression}`"))
389            .with_error_code("E083")
390            .with_location(location)
391    }
392
393    pub fn unconnected_element<T>(name: &str, location: T) -> Diagnostic
394    where
395        T: Into<SourceLocation>,
396    {
397        Diagnostic::new(format!("Element `{name}` is unconnected and will be ignored"))
398            .with_error_code("E084")
399            .with_location(location)
400    }
401
402    pub fn disconnected_return<T>(location: T) -> Diagnostic
403    where
404        T: Into<SourceLocation>,
405    {
406        Diagnostic::new("Return element is not connected to a condition")
407            .with_error_code("E085")
408            .with_location(location)
409    }
410
411    pub fn duplicate_connector<T>(label: &str, location: T) -> Diagnostic
412    where
413        T: Into<SourceLocation>,
414    {
415        Diagnostic::new(format!("Connector `{label}` is already defined"))
416            .with_error_code("E081")
417            .with_location(location)
418    }
419
420    pub fn dangling_continuation<T>(label: &str, location: T) -> Diagnostic
421    where
422        T: Into<SourceLocation>,
423    {
424        Diagnostic::new(format!("Continuation `{label}` has no matching connector"))
425            .with_error_code("E082")
426            .with_location(location)
427    }
428
429    pub fn open_connector<T>(label: &str, location: T) -> Diagnostic
430    where
431        T: Into<SourceLocation>,
432    {
433        Diagnostic::new(format!("Connector `{label}` has no incoming connection"))
434            .with_error_code("E086")
435            .with_location(location)
436    }
437
438    pub fn undefined_jump_target<T>(label: &str, location: T) -> Diagnostic
439    where
440        T: Into<SourceLocation>,
441    {
442        Diagnostic::new(format!("Jump refers to undefined label `{label}`"))
443            .with_error_code("E142")
444            .with_location(location)
445    }
446
447    pub fn unused_label<T>(label: &str, location: T) -> Diagnostic
448    where
449        T: Into<SourceLocation>,
450    {
451        Diagnostic::new(format!("Label `{label}` is not referenced by any jump"))
452            .with_error_code("E143")
453            .with_location(location)
454    }
455
456    pub fn duplicate_label<T>(label: &str, location: T) -> Diagnostic
457    where
458        T: Into<SourceLocation>,
459    {
460        Diagnostic::new(format!("Label `{label}` is already defined"))
461            .with_error_code("E144")
462            .with_location(location)
463    }
464
465    pub fn unknown_block_type<T>(name: &str, location: T) -> Diagnostic
466    where
467        T: Into<SourceLocation>,
468    {
469        Diagnostic::new(format!("Block `{name}` refers to an undeclared POU"))
470            .with_error_code("E146")
471            .with_location(location)
472    }
473
474    pub fn undeclared_block_output<T>(output: &str, name: &str, location: T) -> Diagnostic
475    where
476        T: Into<SourceLocation>,
477    {
478        Diagnostic::new(format!("Output `{output}` is not declared by `{name}`"))
479            .with_error_code("E147")
480            .with_location(location)
481    }
482
483    pub fn unconnected_en<T>(name: &str, location: T) -> Diagnostic
484    where
485        T: Into<SourceLocation>,
486    {
487        Diagnostic::new(format!("Block `{name}` has an unconnected EN pin"))
488            .with_error_code("E152")
489            .with_location(location)
490    }
491
492    pub fn eno_cycle<T>(name: &str, location: T) -> Diagnostic
493    where
494        T: Into<SourceLocation>,
495    {
496        Diagnostic::new(format!("EN pin of block `{name}` resolves through an ENO cycle"))
497            .with_error_code("E153")
498            .with_location(location)
499    }
500
501    pub fn negated_reference_assignment<T>(name: &str, location: T) -> Diagnostic
502    where
503        T: Into<SourceLocation>,
504    {
505        Diagnostic::new(format!("Reference assignment to `{name}` cannot be negated"))
506            .with_error_code("E154")
507            .with_location(location)
508    }
509
510    pub fn duplicate_return_pin<T>(name: &str, location: T) -> Diagnostic
511    where
512        T: Into<SourceLocation>,
513    {
514        Diagnostic::new(format!("Block `{name}` has more than one return pin"))
515            .with_error_code("E155")
516            .with_location(location)
517    }
518
519    pub fn disconnected_jump<T>(location: T) -> Diagnostic
520    where
521        T: Into<SourceLocation>,
522    {
523        Diagnostic::new("Jump element is not connected to a condition and can never be taken")
524            .with_error_code("E145")
525            .with_location(location)
526    }
527
528    pub fn unresolved_generic_output<T>(block: &str, location: T) -> Diagnostic
529    where
530        T: Into<SourceLocation>,
531    {
532        Diagnostic::new(format!(
533            "Cannot determine a type for generic block `{block}`: no input decides its type"
534        ))
535        .with_error_code("E149")
536        .with_location(location)
537    }
538}
539
540#[cfg(test)]
541mod tests {
542    use codespan_reporting::files::{Location, SimpleFile};
543
544    use crate::{diagnostics::Severity, reporter::clang::ClangFormatDiagnosticReporter};
545
546    #[test]
547    fn test_build_diagnostic_msg() {
548        let reporter = ClangFormatDiagnosticReporter::default();
549        let file = SimpleFile::new("test.st".to_string(), "source".to_string());
550        let start = Location { line_number: 4, column_number: 1 };
551        let end = Location { line_number: 4, column_number: 4 };
552        let res = reporter.build_diagnostic_msg(
553            Some(&file),
554            Some(&start),
555            Some(&end),
556            "E001",
557            &Severity::Error,
558            "This is an error",
559        );
560
561        assert_eq!(res, "test.st:4:1:{4:1-4:4}: error[E001]: This is an error");
562    }
563
564    #[test]
565    fn test_build_diagnostic_msg_equal_start_end() {
566        let reporter = ClangFormatDiagnosticReporter::default();
567        let file = SimpleFile::new("test.st".to_string(), "source".to_string());
568        let start = Location { line_number: 4, column_number: 1 };
569        let end = Location { line_number: 4, column_number: 1 };
570        let res = reporter.build_diagnostic_msg(
571            Some(&file),
572            Some(&start),
573            Some(&end),
574            "E001",
575            &Severity::Error,
576            "This is an error",
577        );
578
579        assert_eq!(res, "test.st:4:1: error[E001]: This is an error");
580    }
581
582    #[test]
583    fn test_build_diagnostic_msg_no_location() {
584        let reporter = ClangFormatDiagnosticReporter::default();
585        let file = SimpleFile::new("test.st".to_string(), "source".to_string());
586        let res = reporter.build_diagnostic_msg(
587            Some(&file),
588            None,
589            None,
590            "E001",
591            &Severity::Error,
592            "This is an error",
593        );
594
595        assert_eq!(res, "test.st: error[E001]: This is an error");
596    }
597
598    #[test]
599    fn test_build_diagnostic_msg_no_file() {
600        let reporter = ClangFormatDiagnosticReporter::default();
601        let start = Location { line_number: 4, column_number: 1 };
602        let end = Location { line_number: 4, column_number: 4 };
603        let res = reporter.build_diagnostic_msg(
604            None,
605            Some(&start),
606            Some(&end),
607            "E001",
608            &Severity::Error,
609            "This is an error",
610        );
611
612        assert_eq!(res, "error[E001]: This is an error");
613    }
614
615    #[test]
616    fn test_build_diagnostic_msg_no_file_no_location() {
617        let reporter = ClangFormatDiagnosticReporter::default();
618        let res =
619            reporter.build_diagnostic_msg(None, None, None, "E001", &Severity::Error, "This is an error");
620
621        assert_eq!(res, "error[E001]: This is an error");
622    }
623}