ad_astra/analysis/
issues.rs

1////////////////////////////////////////////////////////////////////////////////
2// This file is part of "Ad Astra", an embeddable scripting programming       //
3// language platform.                                                         //
4//                                                                            //
5// This work is proprietary software with source-available code.              //
6//                                                                            //
7// To copy, use, distribute, or contribute to this work, you must agree to    //
8// the terms of the General License Agreement:                                //
9//                                                                            //
10// https://github.com/Eliah-Lakhin/ad-astra/blob/master/EULA.md               //
11//                                                                            //
12// The agreement grants a Basic Commercial License, allowing you to use       //
13// this work in non-commercial and limited commercial products with a total   //
14// gross revenue cap. To remove this commercial limit for one of your         //
15// products, you must acquire a Full Commercial License.                      //
16//                                                                            //
17// If you contribute to the source code, documentation, or related materials, //
18// you must grant me an exclusive license to these contributions.             //
19// Contributions are governed by the "Contributions" section of the General   //
20// License Agreement.                                                         //
21//                                                                            //
22// Copying the work in parts is strictly forbidden, except as permitted       //
23// under the General License Agreement.                                       //
24//                                                                            //
25// If you do not or cannot agree to the terms of this Agreement,              //
26// do not use this work.                                                      //
27//                                                                            //
28// This work is provided "as is", without any warranties, express or implied, //
29// except where such disclaimers are legally invalid.                         //
30//                                                                            //
31// Copyright (c) 2024 Ilya Lakhin (Илья Александрович Лахин).                 //
32// All rights reserved.                                                       //
33////////////////////////////////////////////////////////////////////////////////
34
35use std::{
36    borrow::Cow,
37    fmt::{Display, Formatter},
38    ops::Range,
39};
40
41use compact_str::CompactString;
42use lady_deirdre::{
43    format::AnnotationPriority,
44    syntax::{ErrorRef, NodeRef, NodeRule, RecoveryResult},
45};
46
47use crate::{
48    analysis::DiagnosticsDepth,
49    runtime::{ops::OperatorKind, ScriptOrigin, ScriptType, TypeFamily, TypeHint, TypeMeta},
50    syntax::{PolyRefOrigin, ScriptDoc, ScriptNode, SpanBounds},
51};
52
53/// A classification of module diagnostic issues.
54///
55/// Each [ModuleIssue](crate::analysis::ModuleIssue) object belongs to a specific
56/// class, as described by this enum type.
57///
58/// From an IssueCode, you can obtain additional metadata about the diagnostic
59/// issue, such as a short description (via the Display implementation of
60/// IssueCode) or the severity of the issue.
61///
62/// Additionally, you can convert an IssueCode into a numeric representation,
63/// which can be used to categorize diagnostics by their numeric codes in a
64/// code editor.
65///
66/// The numeric representation is in the XYY decimal digits format, where X
67/// represents the issue's [diagnostics depth](DiagnosticsDepth), and YY
68/// represents the issue's "sub-code" within that depth level.
69#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
70#[repr(u16)]
71#[non_exhaustive]
72pub enum IssueCode {
73    /// Syntax Error.
74    ///
75    /// This error indicates that the source code is not well-formed from a
76    /// syntactical point of view. These types of errors must be addressed
77    /// first.
78    Parse = 101,
79
80    /// Semantics Error.
81    ///
82    /// The specified package in the `use <package>;` import statement is
83    /// unknown to the script engine.
84    UnresolvedPackage = 201,
85    /// Semantics Error.
86    ///
87    /// The component in the `use foo.<component>;` import statement is not a
88    /// valid package.
89    NotAPackage = 202,
90    /// Semantics Error.
91    ///
92    /// The `break` or `continue` statement is used outside of a loop.
93    OrphanedBreak = 203,
94    /// Semantics Error.
95    ///
96    /// The function already has a parameter with the same name. Function
97    /// parameter names must be unique.
98    DuplicateParam = 204,
99    /// Semantics Error.
100    ///
101    /// An attempt to use a variable that is not initialized at this point in
102    /// the control flow.
103    ReadUninit = 205,
104    /// Semantics Error.
105    ///
106    /// An attempt to use an identifier that does not correspond to any known
107    /// variable within this scope or a package symbol.
108    UnresolvedIdent = 206,
109    /// Semantics Error.
110    ///
111    /// Invalid integer literal format.
112    IntParse = 207,
113    /// Semantics Error.
114    ///
115    /// Invalid floating-point literal format.
116    FloatParse = 208,
117    /// Semantics Warning.
118    ///
119    /// This statement is not reachable during the execution of the source code.
120    /// It is considered dead code. Consider removing this statement or
121    /// commenting it out.
122    UnreachableStatement = 209,
123    /// Semantics Warning.
124    ///
125    /// This match arm is not reachable during the execution of the source code.
126    /// It is considered dead code. Consider removing this match arm or
127    /// commenting it out.
128    UnreachableArm = 210,
129    /// Semantics Warning.
130    ///
131    /// The `struct` declaration already contains a field with the same name.
132    DuplicateEntry = 211,
133    /// Semantics Warning.
134    ///
135    /// An attempt to assign to an orphaned literal. This assignment is
136    /// semantically meaningless.
137    LiteralAssignment = 212,
138
139    /// Semantics Warning.
140    ///
141    /// The provided type does not meet the formal requirements of the
142    /// specification.
143    TypeMismatch = 301,
144    /// Semantics Warning.
145    ///
146    /// An attempt to index into a nil object.
147    NilIndex = 302,
148    /// Semantics Warning.
149    ///
150    /// An attempt to index by an expression that is neither a number nor a
151    /// range of integers.
152    IndexTypeMismatch = 303,
153    /// Semantics Warning.
154    ///
155    /// The object does not implement the specified operator.
156    UndefinedOperator = 304,
157    /// Semantics Warning.
158    ///
159    /// The object does not implement the Display operator.
160    UndefinedDisplay = 305,
161    /// Semantics Warning.
162    ///
163    /// The function requires either more or fewer arguments.
164    CallArityMismatch = 306,
165    /// Semantics Warning.
166    ///
167    /// Expected a function with a different number of parameters.
168    FnArityMismatch = 307,
169    /// Semantics Warning.
170    ///
171    /// The function's result type is different from the expected type.
172    ResultMismatch = 308,
173    /// Semantics Warning.
174    ///
175    /// The object does not have the specified field.
176    UnknownComponent = 309,
177    /// Semantics Warning.
178    ///
179    /// The function has inconsistent return points. Some branches return
180    /// non-nil values, while others return nil values. This issue likely
181    /// indicates that a trailing `return <expr>;` statement is missing.
182    InconsistentReturns = 310,
183}
184
185impl Display for IssueCode {
186    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
187        let message = match self {
188            Self::Parse => "Parse error.",
189
190            Self::UnresolvedPackage => "Unresolved import.",
191            Self::NotAPackage => "Importing a component that is not a package.",
192            Self::OrphanedBreak => "Break outside of a loop.",
193            Self::DuplicateParam => "Duplicate function parameter name.",
194            Self::ReadUninit => "Use of possibly uninitialized variable.",
195            Self::UnresolvedIdent => "Unresolved reference.",
196            Self::IntParse => "Invalid integer literal.",
197            Self::FloatParse => "Invalid float literal.",
198            Self::UnreachableStatement => "Unreachable statement.",
199            Self::UnreachableArm => "Unreachable match arm.",
200            Self::DuplicateEntry => "Duplicate struct entry.",
201            Self::LiteralAssignment => "Assignment to literal is meaningless.",
202
203            Self::TypeMismatch => "Type mismatch.",
204            Self::NilIndex => "Index operator is not applicable to nil type.",
205            Self::IndexTypeMismatch => "Index type must be an integer or an integer range.",
206            Self::UndefinedOperator => "Unresolved operator.",
207            Self::UndefinedDisplay => "Type does not implement the Display operator.",
208            Self::CallArityMismatch => "Call arity mismatch.",
209            Self::FnArityMismatch => "Function arity mismatch.",
210            Self::ResultMismatch => "Function result type mismatch.",
211            Self::UnknownComponent => "Unknown field.",
212            Self::InconsistentReturns => "Missing trailing return statement.",
213        };
214
215        formatter.write_str(message)
216    }
217}
218
219impl IssueCode {
220    /// Returns the issue's [severity](IssueSeverity), which is either an error
221    /// or a warning.
222    #[inline(always)]
223    pub fn severity(self) -> IssueSeverity {
224        match self {
225            Self::Parse => IssueSeverity::Error,
226
227            Self::UnresolvedPackage => IssueSeverity::Error,
228            Self::NotAPackage => IssueSeverity::Error,
229            Self::OrphanedBreak => IssueSeverity::Error,
230            Self::DuplicateParam => IssueSeverity::Error,
231            Self::ReadUninit => IssueSeverity::Error,
232            Self::UnresolvedIdent => IssueSeverity::Error,
233            Self::IntParse => IssueSeverity::Error,
234            Self::FloatParse => IssueSeverity::Error,
235            Self::UnreachableStatement => IssueSeverity::Warning,
236            Self::UnreachableArm => IssueSeverity::Warning,
237            Self::DuplicateEntry => IssueSeverity::Warning,
238            Self::LiteralAssignment => IssueSeverity::Warning,
239
240            Self::TypeMismatch => IssueSeverity::Warning,
241            Self::NilIndex => IssueSeverity::Warning,
242            Self::IndexTypeMismatch => IssueSeverity::Warning,
243            Self::UndefinedOperator => IssueSeverity::Warning,
244            Self::UndefinedDisplay => IssueSeverity::Warning,
245            Self::CallArityMismatch => IssueSeverity::Warning,
246            Self::FnArityMismatch => IssueSeverity::Warning,
247            Self::ResultMismatch => IssueSeverity::Warning,
248            Self::UnknownComponent => IssueSeverity::Warning,
249            Self::InconsistentReturns => IssueSeverity::Warning,
250        }
251    }
252
253    /// Returns the level of semantic analysis depth at which this issue was inferred.
254    ///
255    /// See [DiagnosticsDepth] for details.
256    #[inline(always)]
257    pub fn depth(self) -> DiagnosticsDepth {
258        (self as u16 / 100) as DiagnosticsDepth
259    }
260}
261
262/// The severity level of the source code diagnostics.
263#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
264#[repr(u8)]
265pub enum IssueSeverity {
266    /// Hard errors. The analyzer is quite confident that these kinds of
267    /// diagnostic issues would lead to runtime errors.
268    Error = 1 << 0,
269
270    /// These types of diagnostic issues are likely to lead to runtime problems.
271    /// However, the analyzer is not confident enough to classify them as hard
272    /// errors due to the dynamic nature of the script execution model.
273    Warning = 1 << 1,
274}
275
276impl Display for IssueSeverity {
277    #[inline(always)]
278    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
279        match self {
280            IssueSeverity::Error => formatter.write_str("error"),
281            IssueSeverity::Warning => formatter.write_str("warning"),
282        }
283    }
284}
285
286impl IssueSeverity {
287    #[inline(always)]
288    pub(crate) fn priority(self) -> AnnotationPriority {
289        match self {
290            Self::Error => AnnotationPriority::Primary,
291            Self::Warning => AnnotationPriority::Note,
292        }
293    }
294}
295
296#[derive(Clone, PartialEq, Eq, Hash)]
297pub(crate) enum ScriptIssue {
298    Parse {
299        error_ref: ErrorRef,
300    },
301
302    UnresolvedPackage {
303        base: &'static TypeMeta,
304        package_ref: NodeRef,
305        quickfix: CompactString,
306    },
307
308    NotAPackage {
309        ty: TypeHint,
310        package_ref: NodeRef,
311    },
312
313    OrphanedBreak {
314        break_ref: NodeRef,
315    },
316
317    DuplicateParam {
318        var_ref: NodeRef,
319    },
320
321    ReadUninit {
322        ident_ref: NodeRef,
323    },
324
325    UnresolvedIdent {
326        ident_ref: NodeRef,
327        quickfix: CompactString,
328        import: CompactString,
329    },
330
331    IntParse {
332        number_ref: NodeRef,
333    },
334
335    FloatParse {
336        number_ref: NodeRef,
337    },
338
339    UnreachableStatement {
340        st_ref: NodeRef,
341    },
342
343    UnreachableArm {
344        arm_ref: NodeRef,
345    },
346
347    DuplicateEntry {
348        entry_key_ref: NodeRef,
349    },
350
351    LiteralAssignment {
352        op_ref: NodeRef,
353    },
354
355    TypeMismatch {
356        expr_ref: NodeRef,
357        expected: &'static TypeFamily,
358        provided: &'static TypeFamily,
359    },
360
361    NilIndex {
362        op_ref: NodeRef,
363    },
364
365    IndexTypeMismatch {
366        arg_ref: NodeRef,
367        provided: &'static TypeFamily,
368    },
369
370    UndefinedOperator {
371        op_ref: NodeRef,
372        op: OperatorKind,
373        receiver: &'static TypeMeta,
374    },
375
376    CallArityMismatch {
377        args_ref: NodeRef,
378        expected: usize,
379        provided: usize,
380    },
381
382    FnArityMismatch {
383        arg_ref: NodeRef,
384        expected: usize,
385        provided: usize,
386    },
387
388    ResultMismatch {
389        arg_ref: NodeRef,
390        expected: &'static TypeFamily,
391        provided: &'static TypeFamily,
392    },
393
394    UnknownComponent {
395        field_ref: NodeRef,
396        receiver: &'static TypeMeta,
397        quickfix: CompactString,
398    },
399
400    InconsistentReturns {
401        fn_ref: NodeRef,
402    },
403}
404
405impl ScriptIssue {
406    pub(crate) fn code(&self) -> IssueCode {
407        match self {
408            Self::Parse { .. } => IssueCode::Parse,
409            Self::UnresolvedPackage { .. } => IssueCode::UnresolvedPackage,
410            Self::NotAPackage { .. } => IssueCode::NotAPackage,
411            Self::OrphanedBreak { .. } => IssueCode::OrphanedBreak,
412            Self::DuplicateParam { .. } => IssueCode::DuplicateParam,
413            Self::ReadUninit { .. } => IssueCode::ReadUninit,
414            Self::UnresolvedIdent { .. } => IssueCode::UnresolvedIdent,
415            Self::IntParse { .. } => IssueCode::IntParse,
416            Self::FloatParse { .. } => IssueCode::FloatParse,
417            Self::UnreachableStatement { .. } => IssueCode::UnreachableStatement,
418            Self::UnreachableArm { .. } => IssueCode::UnreachableArm,
419            Self::DuplicateEntry { .. } => IssueCode::DuplicateEntry,
420            Self::LiteralAssignment { .. } => IssueCode::LiteralAssignment,
421            Self::TypeMismatch { .. } => IssueCode::TypeMismatch,
422            Self::NilIndex { .. } => IssueCode::NilIndex,
423            Self::IndexTypeMismatch { .. } => IssueCode::IndexTypeMismatch,
424            Self::UndefinedOperator { .. } => IssueCode::UndefinedOperator,
425            Self::CallArityMismatch { .. } => IssueCode::CallArityMismatch,
426            Self::FnArityMismatch { .. } => IssueCode::FnArityMismatch,
427            Self::ResultMismatch { .. } => IssueCode::ResultMismatch,
428            Self::UnknownComponent { .. } => IssueCode::UnknownComponent,
429            Self::InconsistentReturns { .. } => IssueCode::InconsistentReturns,
430        }
431    }
432
433    pub(crate) fn span(&self, doc: &ScriptDoc) -> ScriptOrigin {
434        match self {
435            Self::Parse { error_ref, .. } => match error_ref.deref(doc) {
436                Some(issue) => ScriptOrigin::from(issue.aligned_span(doc)),
437                None => ScriptOrigin::invalid(error_ref.id),
438            },
439
440            Self::UnresolvedPackage { package_ref, .. } => {
441                package_ref.script_origin(doc, SpanBounds::Cover)
442            }
443
444            Self::NotAPackage { package_ref, .. } => {
445                package_ref.script_origin(doc, SpanBounds::Cover)
446            }
447
448            Self::OrphanedBreak { break_ref, .. } => {
449                break_ref.script_origin(doc, SpanBounds::Header)
450            }
451
452            Self::DuplicateParam { var_ref, .. } => var_ref.script_origin(doc, SpanBounds::Header),
453
454            Self::ReadUninit { ident_ref, .. } => ident_ref.script_origin(doc, SpanBounds::Cover),
455
456            Self::UnresolvedIdent { ident_ref, .. } => {
457                ident_ref.script_origin(doc, SpanBounds::Cover)
458            }
459
460            Self::IntParse { number_ref, .. } => number_ref.script_origin(doc, SpanBounds::Cover),
461
462            Self::FloatParse { number_ref, .. } => number_ref.script_origin(doc, SpanBounds::Cover),
463
464            Self::UnreachableStatement { st_ref, .. } => {
465                st_ref.script_origin(doc, SpanBounds::Cover)
466            }
467
468            Self::UnreachableArm { arm_ref, .. } => arm_ref.script_origin(doc, SpanBounds::Cover),
469
470            Self::DuplicateEntry { entry_key_ref, .. } => {
471                entry_key_ref.script_origin(doc, SpanBounds::Cover)
472            }
473
474            Self::LiteralAssignment { op_ref, .. } => op_ref.script_origin(doc, SpanBounds::Cover),
475
476            Self::TypeMismatch { expr_ref, .. } => expr_ref.script_origin(doc, SpanBounds::Cover),
477
478            Self::NilIndex { op_ref, .. } => op_ref.script_origin(doc, SpanBounds::Cover),
479
480            Self::IndexTypeMismatch { arg_ref, .. } => {
481                arg_ref.script_origin(doc, SpanBounds::Cover)
482            }
483
484            Self::UndefinedOperator { op_ref, .. } => op_ref.script_origin(doc, SpanBounds::Cover),
485
486            Self::CallArityMismatch { args_ref, .. } => {
487                args_ref.script_origin(doc, SpanBounds::Cover)
488            }
489
490            Self::FnArityMismatch { arg_ref, .. } => arg_ref.script_origin(doc, SpanBounds::Header),
491
492            Self::ResultMismatch { arg_ref, .. } => arg_ref.script_origin(doc, SpanBounds::Header),
493
494            Self::UnknownComponent { field_ref, .. } => {
495                field_ref.script_origin(doc, SpanBounds::Cover)
496            }
497
498            Self::InconsistentReturns { fn_ref, .. } => {
499                fn_ref.script_origin(doc, SpanBounds::Header)
500            }
501        }
502    }
503
504    pub(crate) fn message(&self, doc: &ScriptDoc) -> Cow<'static, str> {
505        match self {
506            Self::Parse { error_ref, .. } => Self::message_parse(doc, error_ref),
507
508            Self::UnresolvedPackage { base, quickfix, .. } => {
509                match (base.is_nil(), quickfix.is_empty()) {
510                    (true, true) => Cow::from("unresolved import"),
511                    (false, true) => Cow::from(format!("unresolved import from {base}")),
512                    (true, false) => {
513                        Cow::from(format!("unresolved import. did you mean {quickfix:?}?"))
514                    }
515                    (false, false) => Cow::from(format!(
516                        "unresolved import from {base}. did you mean {quickfix:?}?"
517                    )),
518                }
519            }
520
521            Self::NotAPackage { ty, .. } => Cow::from(format!("type '{ty}' is not a package")),
522
523            Self::OrphanedBreak { break_ref, .. } => match break_ref.deref(doc) {
524                Some(ScriptNode::Continue { .. }) => {
525                    Cow::from("continue statement outside of a loop")
526                }
527                _ => Cow::from("break statement outside of a loop"),
528            },
529
530            Self::DuplicateParam { .. } => Cow::from("duplicate fn parameter name"),
531
532            Self::ReadUninit { .. } => Cow::from("use of possibly uninitialized variable"),
533
534            Self::UnresolvedIdent {
535                quickfix, import, ..
536            } => match (quickfix.is_empty(), import.is_empty()) {
537                (true, true) => Cow::from("unresolved reference"),
538
539                (false, true) => Cow::from(format!(
540                    "unresolved reference. did you mean \"{quickfix}\"?"
541                )),
542
543                _ => Cow::from(format!(
544                    "unresolved reference. did you mean \"{import}.{quickfix}\"?"
545                )),
546            },
547
548            Self::IntParse { .. } => Cow::from("invalid integer literal"),
549
550            Self::FloatParse { .. } => Cow::from("invalid float literal"),
551
552            Self::UnreachableStatement { .. } => Cow::from("unreachable statement"),
553
554            Self::UnreachableArm { .. } => Cow::from("unreachable match arm"),
555
556            Self::DuplicateEntry { .. } => Cow::from("duplicate struct entry"),
557
558            Self::LiteralAssignment { .. } => Cow::from("assignment to literal is meaningless"),
559
560            Self::TypeMismatch {
561                expected, provided, ..
562            } => {
563                let expected = TypeHint::from(*expected);
564                let provided = TypeHint::from(*provided);
565
566                Cow::from(format!(
567                    "expected '{expected}' type, but '{provided}' found"
568                ))
569            }
570
571            Self::NilIndex { .. } => Cow::from("nil type cannot be indexed"),
572
573            Self::IndexTypeMismatch { provided, .. } => {
574                let numeric_family = <usize>::type_meta().family();
575                let range_family = <Range<usize>>::type_meta().family();
576
577                Cow::from(format!(
578                    "expected '{numeric_family}' or '{range_family}' type, but '{provided}' found",
579                ))
580            }
581
582            Self::UndefinedOperator { receiver, op, .. } => {
583                let receiver = TypeHint::from(*receiver);
584
585                Cow::from(format!("'{receiver}' does not implement {op}"))
586            }
587
588            Self::CallArityMismatch {
589                expected, provided, ..
590            } => match *expected {
591                1 => Cow::from(format!("expected 1 argument, but {provided} provided",)),
592
593                _ => Cow::from(format!(
594                    "expected {expected} arguments, but {provided} provided",
595                )),
596            },
597
598            Self::FnArityMismatch {
599                expected, provided, ..
600            } => Cow::from(format!(
601                "expected fn({expected}) function, but fn({provided}) provided",
602            )),
603
604            Self::ResultMismatch {
605                expected, provided, ..
606            } => {
607                let expected = TypeHint::from(*expected);
608                let provided = TypeHint::from(*provided);
609
610                Cow::from(format!(
611                    "expected a function with '{expected}' return type, but the function returns '{provided}'"
612                ))
613            }
614
615            Self::UnknownComponent {
616                receiver, quickfix, ..
617            } => {
618                let receiver = TypeHint::from(*receiver);
619
620                match quickfix.is_empty() {
621                    true => Cow::from(format!("unknown '{receiver}' field",)),
622
623                    false => Cow::from(format!(
624                        "unknown '{receiver}' field. did you mean {quickfix:?}?",
625                    )),
626                }
627            }
628
629            Self::InconsistentReturns { .. } => Cow::from("missing trailing return expression"),
630        }
631    }
632
633    fn message_parse(doc: &ScriptDoc, error_ref: &ErrorRef) -> Cow<'static, str> {
634        let Some(issue) = error_ref.deref(doc) else {
635            return Cow::from("parse error");
636        };
637
638        match issue.context {
639            ScriptNode::STRING => Cow::from("unenclosed string literal"),
640            ScriptNode::MULTILINE_COMMENT => Cow::from("unenclosed comment"),
641            ScriptNode::BLOCK if issue.recovery == RecoveryResult::UnexpectedEOI => {
642                Cow::from("unenclosed code block")
643            }
644            ScriptNode::EXPR if issue.recovery == RecoveryResult::PanicRecover => {
645                Cow::from("unexpected operator")
646            }
647
648            _ => {
649                if Self::is_operator_rule(issue.context) {
650                    return Cow::from("missing operand");
651                }
652
653                Cow::from(issue.message::<ScriptNode>(doc).to_string())
654            }
655        }
656    }
657
658    #[inline(always)]
659    fn is_operator_rule(rule: NodeRule) -> bool {
660        match rule {
661            ScriptNode::UNARY_LEFT | ScriptNode::BINARY | ScriptNode::QUERY => true,
662            _ => false,
663        }
664    }
665}