use mago_allocator::Arena;
use std::rc::Rc;
use mago_codex::ttype::TType;
use mago_codex::ttype::TypeRef;
use mago_codex::ttype::atomic::TAtomic;
use mago_codex::ttype::atomic::object::TObject;
use mago_codex::ttype::atomic::reference::TReference;
use mago_codex::ttype::builder::get_union_from_type;
use mago_codex::ttype::comparator::ComparisonResult;
use mago_codex::ttype::comparator::union_comparator::can_expression_types_be_identical;
use mago_codex::ttype::comparator::union_comparator::is_contained_by;
use mago_codex::ttype::expander;
use mago_codex::ttype::expander::TypeExpansionOptions;
use mago_codex::ttype::union::TUnion;
use mago_codex::ttype::union::populate_union_type;
use mago_phpdoc_syntax::cst::Element;
use mago_phpdoc_syntax::cst::TagValue;
use mago_reporting::Annotation;
use mago_reporting::Issue;
use mago_span::HasSpan;
use mago_span::Span;
use mago_syntax::comments::docblock::PrecedingDocblocks;
use mago_syntax::cst::Expression;
use mago_word::Word;
use crate::artifacts::AnalysisArtifacts;
use crate::code::IssueCode;
use crate::context::Context;
use crate::context::block::BlockContext;
use mago_bytes::BytesDisplay;
pub fn populate_docblock_variables<'ctx, A>(
context: &mut Context<'ctx, '_, A>,
block_context: &mut BlockContext<'ctx>,
artifacts: &mut AnalysisArtifacts,
override_existing: bool,
) where
A: Arena,
{
populate_docblock_variables_excluding(context, block_context, artifacts, override_existing, None);
}
pub fn populate_docblock_variables_excluding<'ctx, A>(
context: &mut Context<'ctx, '_, A>,
block_context: &mut BlockContext<'ctx>,
artifacts: &mut AnalysisArtifacts,
override_existing: bool,
exclude_variable: Option<Word>,
) where
A: Arena,
{
if PrecedingDocblocks::new(context.comments, context.statement_span.start.offset).next().is_none() {
return;
}
for (name, variable_type, variable_type_span) in get_docblock_variables(context, block_context, artifacts, true) {
for type_ref in variable_type.get_all_child_nodes() {
let TypeRef::Atomic(TAtomic::Reference(TReference::Symbol { name: ref_name, .. })) = type_ref else {
continue;
};
context.collector.report_with_code(
IssueCode::NonExistentClassLike,
Issue::error(format!("Cannot find class, interface, enum, or type alias `{ref_name}`."))
.with_annotation(
Annotation::primary(variable_type_span)
.with_message(format!("`{ref_name}` is not defined in the current codebase")),
)
.with_note("This error occurs when a type is referenced but not found in any analyzed source files or stubs.")
.with_note("If this type comes from an optional dependency or extension, you can safely suppress this issue using `@mago-ignore` or `@mago-expect`.")
.with_help("Verify the type name is spelled correctly, the file containing it is included in analysis, and any required `use` statements are present."),
);
}
let Some(variable_name) = name else {
continue;
};
if exclude_variable.is_some_and(|excluded| variable_name == excluded) {
continue;
}
insert_variable_from_docblock(
context,
block_context,
variable_name,
variable_type,
variable_type_span,
override_existing,
);
}
}
pub fn get_docblock_variables<'ctx, A>(
context: &mut Context<'ctx, '_, A>,
block_context: &BlockContext<'ctx>,
artifacts: &mut AnalysisArtifacts,
allow_tracing: bool,
) -> Vec<(Option<mago_word::Word>, TUnion, Span)>
where
A: Arena,
{
context.get_parsed_docblocks()
.into_iter()
.filter_map(|element| match element {
Element::Tag(tag) => Some(tag),
_ => None,
})
.filter_map(|tag| {
if allow_tracing && let TagValue::Trace(trace) = &tag.value {
let variable_name_bytes = trace.variable.value;
let variable_atom = mago_word::word(variable_name_bytes);
let variable_name = BytesDisplay(variable_name_bytes);
match block_context.locals.get(&variable_atom) {
Some(variable_type) => {
let variable_type_str = variable_type.get_id();
context.collector.report_with_code(
IssueCode::PsalmTrace,
Issue::note(format!(
"Trace: Type of `{variable_name}` is `{variable_type_str}`"
))
.with_annotation(
Annotation::primary(trace.variable.span)
.with_message(format!("Type is: `{variable_type_str}`")),
)
.with_note(
"Spotted a `@psalm-trace` tag! While this works for compatibility, Mago has a more powerful way to inspect types.",
)
.with_help(
"For more flexible debugging, try using `Mago\\inspect()` directly in your code. It can inspect any expression, not just variables (e.g., `Mago\\inspect($foo->bar());`)."
),
);
}
None => {
context.collector.report_with_code(
IssueCode::InvalidDocblock,
Issue::error(format!(
"Invalid `@psalm-trace`: Variable `{variable_name}` not found in this scope."
))
.with_annotation(Annotation::primary(trace.variable.span).with_message(
"This variable is not defined or is out of scope here",
))
.with_help(
"Check for typos or ensure the variable is defined on a path that reaches this docblock.",
),
);
}
}
return None;
}
let TagValue::Var(var_tag) = &tag.value else {
return None;
};
let variable_name = var_tag.variable.as_ref().map(|v| mago_word::word(v.value));
match get_union_from_type(
var_tag.r#type,
&context.scope,
&context.type_resolution_context,
block_context.scope.get_class_like_name(),
) {
Ok(mut variable_type) => {
populate_union_type(
&mut variable_type,
&context.codebase.symbols,
block_context.scope.get_reference_source().as_ref(),
&mut artifacts.symbol_references,
true,
);
expander::expand_union(
context.codebase,
&mut variable_type,
&TypeExpansionOptions {
self_class: block_context.scope.get_class_like_name(),
..Default::default()
},
);
Some((variable_name, variable_type, var_tag.r#type.span()))
}
Err(type_error) => {
context.collector.report_with_code(
IssueCode::InvalidDocblock,
Issue::error(match variable_name {
Some(w) => format!("Invalid type in `@var` tag for variable `{w}`."),
None => "Invalid type in `@var` tag for variable `expression`.".to_string(),
})
.with_annotation(Annotation::primary(type_error.span()).with_message(type_error.to_string()))
.with_note(type_error.note())
.with_help(type_error.help()),
);
None
}
}
})
.collect::<Vec<_>>()
}
pub fn get_type_from_var_docblock<'ctx, A>(
context: &mut Context<'ctx, '_, A>,
block_context: &BlockContext<'ctx>,
artifacts: &mut AnalysisArtifacts,
value_expression_variable_id: Option<&[u8]>,
mut allow_unnamed: bool,
) -> Option<(TUnion, Span)>
where
A: Arena,
{
allow_unnamed =
allow_unnamed && !block_context.flags.inside_return() && !block_context.flags.inside_loop_expressions();
get_docblock_variables(context, block_context, artifacts, false)
.into_iter()
.rfind(|(var_name, _, _)| match var_name {
None if allow_unnamed => true,
Some(name) if Some(name.as_bytes()) == value_expression_variable_id => true,
_ => false,
})
.map(|(_, variable_type, variable_type_span)| (variable_type, variable_type_span))
}
pub fn insert_variable_from_docblock<'ctx, A>(
context: &mut Context<'ctx, '_, A>,
block_context: &mut BlockContext<'ctx>,
variable_name: mago_word::Word,
variable_type: TUnion,
variable_type_span: Span,
override_existing: bool,
) where
A: Arena,
{
if !override_existing && block_context.locals.contains_key(&variable_name) {
return;
}
if let Some(previous_type) = block_context.locals.remove(&variable_name) {
let is_super = is_contained_by(
context.codebase,
&variable_type,
&previous_type,
false,
false,
false,
&mut ComparisonResult::default(),
);
let is_sub = is_contained_by(
context.codebase,
&previous_type,
&variable_type,
false,
false,
false,
&mut ComparisonResult::default(),
);
let is_redundant = is_super
&& is_sub
&& !variable_type.is_mixed()
&& !previous_type.is_mixed()
&& !variable_type.is_generic_parameter()
&& !previous_type.is_generic_parameter()
&& !previous_type.contains_placeholder();
let could_share_object_runtime_instance =
unions_could_share_object_runtime_instance(&previous_type, &variable_type);
let is_impossible = !is_redundant
&& !is_super
&& !is_sub
&& !could_share_object_runtime_instance
&& !can_expression_types_be_identical(context.codebase, &previous_type, &variable_type, false, false);
if is_impossible {
let variable_type_str = variable_type.get_id();
let previous_type_str = previous_type.get_id();
context.collector.report_with_code(
IssueCode::DocblockTypeMismatch,
Issue::error(format!("Docblock type mismatch for variable `{variable_name}`."))
.with_annotation(
Annotation::primary(variable_type_span)
.with_message(format!("This docblock asserts the type should be `{variable_type_str}`, but it was previously defined as `{previous_type_str}`.")),
)
.with_note("The type of the variable defined in the docblock does not match the previously defined type.")
.with_help(format!(
"Change the docblock type to match `{previous_type_str}`, or update the variable definition to a compatible type `{variable_type_str}`."
)),
);
} else if is_redundant {
context.collector.report_with_code(
IssueCode::RedundantDocblockType,
Issue::warning(format!("Redundant docblock type for variable `{variable_name}`."))
.with_annotation(Annotation::primary(variable_type_span).with_message(format!(
"This docblock asserts the type should be `{}`, which is identical to the previously defined type.",
variable_type.get_id(),
)))
.with_help("You can remove this redundant `@var` docblock tag."),
);
}
}
block_context.locals.insert(variable_name, Rc::new(variable_type));
}
pub fn check_docblock_type_incompatibility<A>(
context: &mut Context<'_, '_, A>,
value_expression_variable_id: Option<&[u8]>,
value_expression_span: Span,
inferred_type: &TUnion,
docblock_type: &TUnion,
dockblock_type_span: Span,
source_expression: Option<&Expression>,
) where
A: Arena,
{
let is_super = is_contained_by(
context.codebase,
docblock_type,
inferred_type,
false,
false,
false,
&mut ComparisonResult::default(),
);
let is_sub = is_contained_by(
context.codebase,
inferred_type,
docblock_type,
false,
false,
false,
&mut ComparisonResult::default(),
);
let is_redundant = is_super
&& is_sub
&& !docblock_type.is_mixed()
&& !inferred_type.is_mixed()
&& !docblock_type.is_generic_parameter()
&& !inferred_type.is_generic_parameter()
&& !inferred_type.contains_placeholder();
let is_impossible = !is_redundant
&& !is_super
&& !is_sub
&& !unions_could_share_object_runtime_instance(inferred_type, docblock_type)
&& !can_expression_types_be_identical(context.codebase, inferred_type, docblock_type, false, true);
if is_impossible {
let docblock_type_str = docblock_type.get_id();
let inferred_type_str = inferred_type.get_id();
let mut issue = if let Some(value_expression_variable_id) = value_expression_variable_id {
let value_expression_variable_id = BytesDisplay(value_expression_variable_id);
Issue::error(format!("Docblock type mismatch for variable `{value_expression_variable_id}`."))
.with_annotation(
Annotation::primary(dockblock_type_span)
.with_message(format!("This docblock asserts the type should be `{docblock_type_str}`...")),
)
} else {
Issue::error("Docblock type mismatch for expression.".to_string()).with_annotation(
Annotation::primary(dockblock_type_span)
.with_message(format!("This docblock asserts the type should be `{docblock_type_str}`...")),
)
};
if let Some(value_expression_variable_id) = value_expression_variable_id {
let value_expression_variable_id = BytesDisplay(value_expression_variable_id);
if let Some(source_expression) = source_expression {
issue = issue.with_annotation(Annotation::secondary(source_expression.span()).with_message(format!(
"...but this expression provides an incompatible type `{inferred_type_str}`."
)));
}
issue = issue.with_annotation(
Annotation::secondary(value_expression_span)
.with_message(format!("The assignment to `{value_expression_variable_id}` here is invalid.")),
) .with_note(
"The type of the assigned value and the `@var` docblock type have no overlap, making this assignment impossible."
)
.with_help(format!(
"Change the assigned value to match `{docblock_type_str}`, or update the `@var` tag to a compatible type."
));
} else {
issue = issue.with_annotation(
Annotation::secondary(value_expression_span)
.with_message(format!("...but this expression provides an incompatible type `{inferred_type_str}`.")),
)
.with_note(
"The type resolved from the docblock and the type of the expression have no overlap, making the docblock type invalid.",
)
.with_help(format!(
"Change the expression to match `{docblock_type_str}`, or update the `@var` tag to a compatible type."
));
}
context.collector.report_with_code(IssueCode::DocblockTypeMismatch, issue);
return;
}
if is_redundant {
let docblock_type_str = docblock_type.get_id();
let inferred_type_str = inferred_type.get_id();
let mut issue = if let Some(value_expression_variable_id) = value_expression_variable_id {
let value_expression_variable_id = BytesDisplay(value_expression_variable_id);
Issue::warning(format!("Redundant docblock type for variable `{value_expression_variable_id}`."))
.with_annotation(Annotation::primary(dockblock_type_span).with_message(format!(
"This docblock asserts the type should be `{docblock_type_str}`, which is identical to the inferred type."
)))
} else {
Issue::warning("Redundant docblock type for expression.".to_string()).with_annotation(
Annotation::primary(dockblock_type_span).with_message(format!(
"This docblock asserts the type should be `{docblock_type_str}`, which is identical to the inferred type."
)),
)
};
if let Some(value_expression_variable_id) = value_expression_variable_id {
let value_expression_variable_id = BytesDisplay(value_expression_variable_id);
issue = issue
.with_annotation(Annotation::secondary(value_expression_span).with_message(format!(
"The variable `{value_expression_variable_id}` type is known to be `{inferred_type_str}` here."
)))
.with_note("The type defined in this docblock is identical to the inferred type of the variable.")
.with_help("You can remove this redundant `@var` docblock tag.");
} else {
issue = issue
.with_annotation(
Annotation::secondary(value_expression_span)
.with_message(format!("This expression's type is already inferred as `{inferred_type_str}`.")),
)
.with_note("The type defined in this docblock is identical to the inferred type of the expression.")
.with_help("You can remove this redundant `@var` docblock tag.");
}
context.collector.report_with_code(IssueCode::RedundantDocblockType, issue);
}
}
fn unions_could_share_object_runtime_instance(inferred_type: &TUnion, docblock_type: &TUnion) -> bool {
for inferred_atomic in inferred_type.types.iter() {
let TAtomic::Object(TObject::Named(inferred_named)) = inferred_atomic else {
continue;
};
for docblock_atomic in docblock_type.types.iter() {
let TAtomic::Object(TObject::Named(docblock_named)) = docblock_atomic else {
continue;
};
if inferred_named.name.as_bytes().eq_ignore_ascii_case(docblock_named.name.as_bytes()) {
return true;
}
}
}
false
}