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 ¶ms.items {
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<'_>) {
for kind in ctx.ancestry().ancestor_kinds() {
match kind {
AstKind::Program(_)
| AstKind::TSModuleBlock(_)
| AstKind::TSModuleDeclaration(_)
| AstKind::TSGlobalDeclaration(_) => {
break;
}
m if m.is_module_declaration() => {
}
_ => {
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(),
Expression::NumericLiteral(_)
| Expression::Identifier(_)
| match_member_expression!(Expression)
| Expression::BinaryExpression(_)
| 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 {
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 {
if method.kind.is_constructor() {
ctx.error(diagnostics::illegal_abstract_modifier(method.key.span()));
}
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;
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));
}
}
}
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) => {
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));
}
}
}