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_parameter<'a>(param: &TSTypeParameter<'a>, ctx: &SemanticBuilder<'a>) {
check_type_name_is_reserved(¶m.name, ctx, "Type parameter");
if param.r#in || param.out {
let is_allowed_node = matches!(
ctx.nodes.ancestor_kinds(ctx.current_node_id).nth(1),
Some(
AstKind::TSInterfaceDeclaration(_)
| AstKind::Class(_)
| AstKind::TSTypeAliasDeclaration(_)
)
);
if !is_allowed_node {
if param.r#in {
ctx.error(diagnostics::can_only_appear_on_a_type_parameter_of_a_class_interface_or_type_alias(
"in", param.span,
));
}
if param.out {
ctx.error(diagnostics::can_only_appear_on_a_type_parameter_of_a_class_interface_or_type_alias(
"out", param.span,
));
}
}
}
}
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_alias_declaration<'a>(
decl: &TSTypeAliasDeclaration<'a>,
ctx: &SemanticBuilder<'a>,
) {
check_type_name_is_reserved(&decl.id, ctx, "Type alias");
}
pub fn check_ts_infer_type<'a>(infer_type: &TSInferType<'a>, ctx: &SemanticBuilder<'a>) {
let is_in_conditional_extends_clause =
ctx.nodes.ancestor_kinds(ctx.current_node_id).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 node in ctx.nodes.ancestors(ctx.current_node_id) {
match node.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));
}
});
check_type_name_is_reserved(&decl.id, ctx, "Enum");
}
pub fn check_ts_import_equals_declaration<'a>(
decl: &TSImportEqualsDeclaration<'a>,
ctx: &SemanticBuilder<'a>,
) {
if decl.import_kind.is_type() && !decl.module_reference.is_external() {
ctx.error(diagnostics::import_alias_cannot_use_import_type(decl.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;
}
}
}
if let Some(id) = &class.id {
check_type_name_is_reserved(id, ctx, "Class");
}
}
pub fn check_ts_interface_declaration<'a>(
decl: &TSInterfaceDeclaration<'a>,
ctx: &SemanticBuilder<'a>,
) {
check_type_name_is_reserved(&decl.id, ctx, "Interface");
}
fn check_type_name_is_reserved<'a>(
id: &BindingIdentifier<'a>,
ctx: &SemanticBuilder<'a>,
syntax_name: &str,
) {
match id.name.as_str() {
"any" | "unknown" | "never" | "number" | "bigint" | "boolean" | "string" | "symbol"
| "void" | "object" | "undefined" => {
ctx.error(diagnostics::reserved_type_name(id.span, id.name.as_str(), syntax_name));
}
_ => {}
}
}
pub fn check_method_definition<'a>(method: &MethodDefinition<'a>, ctx: &SemanticBuilder<'a>) {
let is_abstract = method.r#type.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.nodes.get_node(node_id).kind() 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_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 && !is_declare {
ctx.error(diagnostics::accessor_without_body(method.key.span()));
}
}
pub fn check_property_definition(prop: &PropertyDefinition, ctx: &SemanticBuilder<'_>) {
if prop.r#type.is_abstract() && prop.key.is_private_identifier() {
ctx.error(diagnostics::abstract_cannot_be_used_with_private_identifier(prop.key.span()));
}
}
pub fn check_object_property(prop: &ObjectProperty, ctx: &SemanticBuilder<'_>) {
if let Expression::FunctionExpression(func) = &prop.value
&& prop.kind.is_accessor()
&& matches!(func.r#type, FunctionType::TSEmptyBodyFunctionExpression)
{
ctx.error(diagnostics::accessor_without_body(prop.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_jsx_expression_container(
container: &JSXExpressionContainer,
ctx: &SemanticBuilder<'_>,
) {
if matches!(container.expression, JSXExpression::SequenceExpression(_)) {
ctx.error(diagnostics::jsx_expressions_may_not_use_the_comma_operator(
container.expression.span(),
));
}
}
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));
}
}
}