mago-semantics 1.20.1

PHP Semantics Checker.
Documentation
use mago_php_version::feature::Feature;
use mago_reporting::Annotation;
use mago_reporting::Issue;
use mago_span::HasSpan;
use mago_syntax::ast::Hint;

use crate::internal::context::Context;

#[inline]
pub fn check_hint(hint: &Hint, context: &mut Context<'_, '_, '_>) {
    match hint {
        Hint::Parenthesized(parenthesized_hint) if !parenthesized_hint.hint.is_parenthesizable() => {
            let val = context.get_code_snippet(parenthesized_hint.hint);

            context.report(
                Issue::error(format!("Type `{val}` cannot be parenthesized."))
                    .with_annotation(
                        Annotation::primary(parenthesized_hint.hint.span()).with_message("Invalid parenthesized type."),
                    )
                    .with_annotation(
                        Annotation::secondary(parenthesized_hint.span())
                            .with_message("Parenthesized type defined here."),
                    )
                    .with_note("Only union or intersection types can be enclosed in parentheses.")
                    .with_help("Remove the parentheses around the type."),
            );
        }
        Hint::Nullable(nullable_hint) => {
            if !context.version.is_supported(Feature::NullableTypeHint) {
                context.report(
                    Issue::error("The `?` nullable type hint is only available in PHP 7.1 and above.")
                        .with_annotation(
                            Annotation::primary(hint.span()).with_message("`?` nullable type hint used here."),
                        )
                        .with_help("Upgrade to PHP 7.1 or above to use the `?` nullable type hint."),
                );
            }

            if nullable_hint.hint.is_standalone() || nullable_hint.hint.is_complex() {
                let val = context.get_code_snippet(nullable_hint.hint);

                context.report(
                    Issue::error(format!("Type `{val}` cannot be nullable."))
                        .with_annotation(
                            Annotation::primary(nullable_hint.hint.span()).with_message("Invalid nullable type."),
                        )
                        .with_annotation(
                            Annotation::secondary(nullable_hint.span()).with_message("Nullable type defined here."),
                        )
                        .with_help("Replace the type or remove the nullable modifier."),
                );
            }
        }
        Hint::Union(union_hint) => {
            if !union_hint.left.is_unionable() {
                let val = context.get_code_snippet(union_hint.left);

                context.report(
                    Issue::error(format!("Type `{val}` cannot be part of a union."))
                        .with_annotation(
                            Annotation::primary(union_hint.left.span()).with_message("Invalid union type."),
                        )
                        .with_annotation(
                            Annotation::secondary(union_hint.pipe).with_message("Union operator `|` used here."),
                        )
                        .with_note("Intersection and standalone types cannot be part of a union.")
                        .with_help("Replace the type or remove it from the union."),
                );
            }

            if !union_hint.right.is_unionable() {
                let val = context.get_code_snippet(union_hint.right);

                context.report(
                    Issue::error(format!("Type `{val}` cannot be part of a union."))
                        .with_annotation(
                            Annotation::primary(union_hint.right.span()).with_message("Invalid union type."),
                        )
                        .with_annotation(
                            Annotation::secondary(union_hint.pipe).with_message("Union operator `|` used here."),
                        )
                        .with_note("Intersection and standalone types cannot be part of a union.")
                        .with_help("Replace the type or remove it from the union."),
                );
            }
        }
        Hint::Intersection(intersection_hint) => {
            if !context.version.is_supported(Feature::PureIntersectionTypes) {
                context.report(
                    Issue::error("Intersection types are only available in PHP 8.1 and above.")
                    .with_annotation(
                        Annotation::primary(intersection_hint.span()).with_message("Intersection type used here."),
                    )
                    .with_note(
                        "Intersection types allow combining multiple types into a single type, but are only available in PHP 8.2 and above.",
                    )
                    .with_help("Upgrade to PHP 8.2 or above to use intersection types."),
                );
            }

            if !intersection_hint.left.is_intersectable() {
                let val = context.get_code_snippet(intersection_hint.left);

                context.report(
                    Issue::error(format!("Type `{val}` cannot be part of an intersection."))
                        .with_annotation(
                            Annotation::primary(intersection_hint.left.span())
                                .with_message("Invalid intersection type."),
                        )
                        .with_annotation(
                            Annotation::secondary(intersection_hint.ampersand)
                                .with_message("Intersection operator `&` used here."),
                        )
                        .with_note("Union and standalone types cannot be part of an intersection.")
                        .with_help("Replace the type or remove it from the intersection."),
                );
            }

            if !intersection_hint.right.is_intersectable() {
                let val = context.get_code_snippet(intersection_hint.right);

                context.report(
                    Issue::error(format!("Type `{val}` cannot be part of an intersection."))
                        .with_annotation(
                            Annotation::primary(intersection_hint.right.span())
                                .with_message("Invalid intersection type."),
                        )
                        .with_annotation(
                            Annotation::secondary(intersection_hint.ampersand)
                                .with_message("Intersection operator `&` used here."),
                        )
                        .with_note("Union and standalone types cannot be part of an intersection.")
                        .with_help("Replace the type or remove it from the intersection."),
                );
            }
        }
        Hint::True(hint) if !context.version.is_supported(Feature::TrueTypeHint) => {
            context.report(
                Issue::error("The `true` type hint is only available in PHP 8.2 and above.")
                    .with_annotation(Annotation::primary(hint.span()).with_message("`true` type hint used here."))
                    .with_help("Upgrade to PHP 8.2 or above to use the `true` type hint."),
            );
        }
        Hint::False(hint) if context.hint_depth == 1 && !context.version.is_supported(Feature::FalseTypeHint) => {
            context.report(
                Issue::error("The `false` type hint is only available in PHP 8.2 and above.")
                    .with_annotation(Annotation::primary(hint.span()).with_message("`false` type hint used here."))
                    .with_help("Upgrade to PHP 8.2 or above to use the `false` type hint."),
            );
        }
        Hint::False(hint)
            if context.hint_depth != 1 && !context.version.is_supported(Feature::FalseCompoundTypeHint) =>
        {
            context.report(
                Issue::error("The compound `false` type hint is only available in PHP 8.0 and above.")
                    .with_annotation(Annotation::primary(hint.span()).with_message("`false` type hint used here."))
                    .with_help("Upgrade to PHP 8.0 or above to use the `false` type hint."),
            );
        }
        Hint::Null(hint) if context.hint_depth == 1 && !context.version.is_supported(Feature::NullTypeHint) => {
            context.report(
                Issue::error("The `null` type hint is only available in PHP 8.2 and above.")
                    .with_annotation(Annotation::primary(hint.span()).with_message("`null` type hint used here."))
                    .with_help("Upgrade to PHP 8.2 or above to use the `null` type hint."),
            );
        }
        Hint::Null(hint) if context.hint_depth != 1 && !context.version.is_supported(Feature::NullCompoundTypeHint) => {
            context.report(
                Issue::error("The compound `null` type hint is only available in PHP 8.0 and above.")
                    .with_annotation(Annotation::primary(hint.span()).with_message("`null` type hint used here."))
                    .with_help("Upgrade to PHP 8.0 or above to use the `null` type hint."),
            );
        }
        Hint::Iterable(hint) if !context.version.is_supported(Feature::IterableTypeHint) => {
            context.report(
                Issue::error("The `iterable` type hint is only available in PHP 7.1 and above.")
                    .with_annotation(Annotation::primary(hint.span()).with_message("`iterable` type hint used here."))
                    .with_help("Upgrade to PHP 7.1 or above to use the `iterable` type hint."),
            );
        }
        Hint::Void(hint) if !context.version.is_supported(Feature::VoidTypeHint) => {
            context.report(
                Issue::error("The `void` type hint is only available in PHP 7.1 and above.")
                    .with_annotation(Annotation::primary(hint.span()).with_message("`void` type hint used here."))
                    .with_help("Upgrade to PHP 7.1 or above to use the `void` type hint."),
            );
        }
        Hint::Mixed(hint) if !context.version.is_supported(Feature::MixedTypeHint) => {
            context.report(
                Issue::error("The `mixed` type hint is only available in PHP 8.0 and above.")
                    .with_annotation(Annotation::primary(hint.span()).with_message("`mixed` type hint used here."))
                    .with_help("Upgrade to PHP 8.0 or above to use the `mixed` type hint."),
            );
        }
        Hint::Never(hint) if !context.version.is_supported(Feature::NeverTypeHint) => {
            context.report(
                Issue::error("The `never` type hint is only available in PHP 8.1 and above.")
                    .with_annotation(Annotation::primary(hint.span()).with_message("`never` type hint used here."))
                    .with_help("Upgrade to PHP 8.1 or above to use the `never` type hint."),
            );
        }
        _ => {}
    }

    if context.version.is_supported(Feature::DisjunctiveNormalForm) {
        return;
    }

    let is_dnf = match hint {
        Hint::Intersection(inter) if inter.left.is_union() || inter.right.is_union() => true,
        Hint::Union(union) if union.left.is_intersection() || union.right.is_intersection() => true,
        _ => false,
    };

    if !is_dnf {
        return;
    }

    context.report(
        Issue::error("Disjunctive Normal Form (DNF) types are only available in PHP 8.2 and above.")
            .with_annotation(Annotation::primary(hint.span()).with_message("DNF type used here.")),
    );
}