oxc_semantic 0.137.0

A collection of JavaScript tools written in Rust.
Documentation
use std::borrow::Cow;

use itertools::Itertools;
use rustc_hash::FxHashMap;

use oxc_ast::{AstKind, ast::*};
use oxc_ecmascript::BoundNames;
use oxc_span::{GetSpan, Span};
use oxc_str::Str;

use crate::{builder::SemanticBuilder, diagnostics};

pub fn check_ts_type_annotation(annotation: &TSTypeAnnotation<'_>, ctx: &SemanticBuilder<'_>) {
    let (modifier, is_start, span_with_illegal_modifier) = match &annotation.type_annotation {
        TSType::JSDocNonNullableType(ty) => ('!', !ty.postfix, ty.span()),
        TSType::JSDocNullableType(ty) => ('?', !ty.postfix, ty.span()),
        _ => {
            return;
        }
    };

    let valid_type_span = if is_start {
        span_with_illegal_modifier.shrink_left(1)
    } else {
        span_with_illegal_modifier.shrink_right(1)
    };

    let suggestion = &ctx.source_text[valid_type_span];
    let suggestion = if modifier == '?' {
        Cow::Owned(format!("{suggestion} | null | undefined"))
    } else {
        Cow::Borrowed(suggestion)
    };

    ctx.error(diagnostics::jsdoc_type_in_annotation(
        modifier,
        is_start,
        span_with_illegal_modifier,
        &suggestion,
    ));
}

pub fn check_ts_type_predicate(predicate: &TSTypePredicate<'_>, ctx: &SemanticBuilder<'_>) {
    if !is_allowed_type_predicate_position(ctx) {
        ctx.error(diagnostics::type_predicate_only_in_return_type(predicate.span));
    }
}

fn is_allowed_type_predicate_position(ctx: &SemanticBuilder<'_>) -> bool {
    let mut ancestors = ctx.ancestry().ancestor_kinds();

    let Some(AstKind::TSTypeAnnotation(_)) = ancestors.next() else {
        return false;
    };

    match ancestors.next() {
        Some(AstKind::Function(_)) => match ancestors.next() {
            Some(AstKind::MethodDefinition(method)) => method.kind.is_method(),
            Some(AstKind::ObjectProperty(property)) => !property.kind.is_accessor(),
            _ => true,
        },
        Some(
            AstKind::ArrowFunctionExpression(_)
            | AstKind::TSFunctionType(_)
            | AstKind::TSCallSignatureDeclaration(_),
        ) => true,
        Some(AstKind::TSMethodSignature(signature)) => {
            signature.kind == TSMethodSignatureKind::Method
        }
        _ => false,
    }
}

pub fn check_ts_infer_type<'a>(infer_type: &TSInferType<'a>, ctx: &SemanticBuilder<'a>) {
    let is_in_conditional_extends_clause = ctx.ancestry().ancestor_kinds().any(|kind| {
        kind.as_ts_conditional_type().is_some_and(|conditional| {
            conditional.extends_type.span().contains_inclusive(infer_type.span)
        })
    });

    if !is_in_conditional_extends_clause {
        ctx.error(diagnostics::infer_declaration_only_permitted_in_extends_clause(infer_type.span));
    }
}

pub fn check_formal_parameters(params: &FormalParameters, ctx: &SemanticBuilder<'_>) {
    if params.kind == FormalParameterKind::Signature && params.items.len() > 1 {
        check_duplicate_bound_names(params, ctx);
    }

    let mut has_optional = false;

    for param in &params.items {
        // function a(optional?: number, required: number) { }
        if param.optional {
            has_optional = true;
        } else if has_optional && param.initializer.is_none() {
            ctx.error(diagnostics::required_parameter_after_optional_parameter(param.span));
        }
    }
}

fn check_duplicate_bound_names<'a, T: BoundNames<'a>>(bound_names: &T, ctx: &SemanticBuilder<'_>) {
    let mut idents: FxHashMap<Str<'a>, Span> = FxHashMap::default();
    bound_names.bound_names(&mut |ident| {
        if let Some(old_span) = idents.insert(ident.name.into(), ident.span) {
            ctx.error(diagnostics::redeclaration(&ident.name, old_span, ident.span));
        }
    });
}

pub fn check_ts_module_declaration<'a>(decl: &TSModuleDeclaration<'a>, ctx: &SemanticBuilder<'a>) {
    check_ts_module_or_global_declaration(decl.span, ctx);
    check_ts_export_assignment_in_module_decl(decl, ctx);
}

pub fn check_ts_global_declaration<'a>(decl: &TSGlobalDeclaration<'a>, ctx: &SemanticBuilder<'a>) {
    check_ts_module_or_global_declaration(decl.span, ctx);

    if !decl.declare && !ctx.in_declare_scope() {
        ctx.error(diagnostics::global_scope_augmentation_should_have_declare_modifier(
            decl.global_span,
        ));
    }
}

fn check_ts_module_or_global_declaration(span: Span, ctx: &SemanticBuilder<'_>) {
    // skip current node
    for kind in ctx.ancestry().ancestor_kinds() {
        match kind {
            AstKind::Program(_)
            | AstKind::TSModuleBlock(_)
            | AstKind::TSModuleDeclaration(_)
            | AstKind::TSGlobalDeclaration(_) => {
                break;
            }
            m if m.is_module_declaration() => {
                // We need to check the parent of the parent
            }
            _ => {
                ctx.error(diagnostics::not_allowed_namespace_declaration(span));
            }
        }
    }
}

pub fn check_ts_enum_declaration<'a>(decl: &TSEnumDeclaration<'a>, ctx: &SemanticBuilder<'a>) {
    let mut need_initializer = false;

    decl.body.members.iter().for_each(|member| {
        #[expect(clippy::unnested_or_patterns)]
        if let Some(initializer) = &member.initializer {
            need_initializer = !matches!(
                initializer.without_parentheses(),
                // A = 1
                Expression::NumericLiteral(_)
                    // B = A
                    | Expression::Identifier(_)
                    // C = E.D
                    | match_member_expression!(Expression)
                    // D = 1 + 2
                    | Expression::BinaryExpression(_)
                    // E = -1
                    | Expression::UnaryExpression(_)
            );
        } else if need_initializer {
            ctx.error(diagnostics::enum_member_must_have_initializer(member.span));
        }
    });
}

pub fn check_class<'a>(class: &Class<'a>, ctx: &SemanticBuilder<'a>) {
    if !class.r#abstract {
        for elem in &class.body.body {
            if elem.is_abstract() {
                let span = elem.property_key().map_or_else(|| elem.span(), GetSpan::span);
                ctx.error(diagnostics::abstract_elem_in_concrete_class(elem.is_property(), span));
            }
        }
    }

    if !class.r#declare && !ctx.in_declare_scope() {
        let mut is_in_overload_group = false;
        for (a, b) in class.body.body.iter().map(Some).chain(vec![None]).tuple_windows() {
            if let Some(ClassElement::MethodDefinition(a)) = a
                && !a.r#type.is_abstract()
                && !a.optional
                && a.value.r#type == FunctionType::TSEmptyBodyFunctionExpression
            {
                let next_is_same = b.is_some_and(|b| {
                    matches!(b,
                        ClassElement::MethodDefinition(b)
                            if b.key.static_name() == a.key.static_name()
                    )
                });
                if next_is_same {
                    is_in_overload_group = true;
                } else if a.key.static_name().is_some() || is_in_overload_group {
                    // Report error for:
                    // 1. Methods with static names that are not followed by an implementation
                    // 2. The last overload in a computed-name overload group (e.g. [Symbol.iterator])
                    if a.kind.is_constructor() {
                        ctx.error(diagnostics::constructor_implementation_missing(a.key.span()));
                    } else {
                        ctx.error(diagnostics::function_implementation_missing(a.key.span()));
                    }
                    is_in_overload_group = false;
                } else {
                    is_in_overload_group = false;
                }
            } else {
                is_in_overload_group = false;
            }
        }
    }
}

pub fn check_method_definition<'a>(method: &MethodDefinition<'a>, ctx: &SemanticBuilder<'a>) {
    let is_abstract = method.r#type.is_abstract();

    if is_abstract {
        // constructors cannot be abstract, no matter what
        if method.kind.is_constructor() {
            ctx.error(diagnostics::illegal_abstract_modifier(method.key.span()));
        }
        // abstract cannot be used with private identifiers
        if method.key.is_private_identifier() {
            ctx.error(diagnostics::abstract_cannot_be_used_with_private_identifier(
                method.key.span(),
            ));
        }
    }

    let is_empty_body = method.value.r#type == FunctionType::TSEmptyBodyFunctionExpression;
    // Illegal to have `constructor(public foo);`
    if method.kind.is_constructor() && is_empty_body {
        for param in &method.value.params.items {
            if param.has_modifier() {
                ctx.error(diagnostics::parameter_property_only_in_constructor_impl(param.span));
            }
        }
    }

    // Illegal to have `get foo();` or `set foo(a)`
    if method.kind.is_accessor() && is_empty_body && !is_abstract {
        let is_declare = ctx.class_table_builder.current_class_id.map_or(
            ctx.source_type.is_typescript_definition(),
            |id| {
                let node_id = ctx.class_table_builder.classes.declarations[id];
                let AstKind::Class(class) = ctx.ancestry().find_kind_by_node_id(node_id) else {
                    #[cfg(debug_assertions)]
                    panic!("current_class_id is set, but does not point to a Class node.");
                    #[cfg(not(debug_assertions))]
                    return ctx.source_type.is_typescript_definition();
                };
                class.declare || ctx.source_type.is_typescript_definition()
            },
        );
        if !is_declare {
            ctx.error(diagnostics::accessor_without_body(method.key.span()));
        }
    }
}

pub fn check_for_statement_left(left: &ForStatementLeft, is_for_in: bool, ctx: &SemanticBuilder) {
    let ForStatementLeft::VariableDeclaration(decls) = left else {
        return;
    };

    for decl in &decls.declarations {
        if decl.type_annotation.is_some() {
            let span = decl.id.span();
            ctx.error(diagnostics::type_annotation_in_for_left(span, is_for_in));
        }
    }
}

pub fn check_ts_export_assignment_in_program<'a>(program: &Program<'a>, ctx: &SemanticBuilder<'a>) {
    if !ctx.source_type.is_typescript() {
        return;
    }
    check_ts_export_assignment_in_statements(&program.body, ctx);
}

fn check_ts_export_assignment_in_module_decl<'a>(
    module_decl: &TSModuleDeclaration<'a>,
    ctx: &SemanticBuilder<'a>,
) {
    let Some(body) = &module_decl.body else {
        return;
    };
    match body {
        TSModuleDeclarationBody::TSModuleDeclaration(nested) => {
            check_ts_export_assignment_in_module_decl(nested, ctx);
        }
        TSModuleDeclarationBody::TSModuleBlock(block) => {
            check_ts_export_assignment_in_statements(&block.body, ctx);
        }
    }
}

fn check_ts_export_assignment_in_statements<'a>(
    statements: &[Statement<'a>],
    ctx: &SemanticBuilder<'a>,
) {
    let mut export_assignment_spans = vec![];
    let mut has_other_exports = false;

    for stmt in statements {
        match stmt {
            Statement::TSExportAssignment(export_assignment) => {
                export_assignment_spans.push(export_assignment.span);
            }
            Statement::ExportNamedDeclaration(export_decl) => {
                // ignore `export {}`
                if export_decl.declaration.is_none() && export_decl.specifiers.is_empty() {
                    continue;
                }
                has_other_exports = true;
            }
            Statement::ExportDefaultDeclaration(_) | Statement::ExportAllDeclaration(_) => {
                has_other_exports = true;
            }
            _ => {}
        }
    }

    if has_other_exports {
        for span in export_assignment_spans {
            ctx.error(diagnostics::ts_export_assignment_cannot_be_used_with_other_exports(span));
        }
    }
}