use std::rc::Rc;
use mago_atom::Atom;
use mago_codex::ttype::TType;
use mago_codex::ttype::TypeRef;
use mago_codex::ttype::atomic::TAtomic;
use mago_codex::ttype::atomic::reference::TReference;
use mago_codex::ttype::builder::get_type_from_string;
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_docblock::document::Element;
use mago_docblock::document::TagKind;
use mago_docblock::tag::parse_var_tag;
use mago_reporting::Annotation;
use mago_reporting::Issue;
use mago_span::HasSpan;
use mago_span::Span;
use mago_syntax::ast::Expression;
use crate::artifacts::AnalysisArtifacts;
use crate::code::IssueCode;
use crate::context::Context;
use crate::context::block::BlockContext;
pub fn populate_docblock_variables<'ctx>(
context: &mut Context<'ctx, '_>,
block_context: &mut BlockContext<'ctx>,
artifacts: &mut AnalysisArtifacts,
override_existing: bool,
) {
populate_docblock_variables_excluding(context, block_context, artifacts, override_existing, None);
}
pub fn populate_docblock_variables_excluding<'ctx>(
context: &mut Context<'ctx, '_>,
block_context: &mut BlockContext<'ctx>,
artifacts: &mut AnalysisArtifacts,
override_existing: bool,
exclude_variable: Option<Atom>,
) {
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.as_str() == excluded) {
continue;
}
insert_variable_from_docblock(
context,
block_context,
variable_name,
variable_type,
variable_type_span,
override_existing,
);
}
}
pub fn get_docblock_variables<'ctx>(
context: &mut Context<'ctx, '_>,
block_context: &BlockContext<'ctx>,
artifacts: &mut AnalysisArtifacts,
allow_tracing: bool,
) -> Vec<(Option<mago_atom::Atom>, TUnion, Span)> {
context.get_parsed_docblocks()
.into_iter()
.filter_map(|element| match element {
Element::Tag(tag) => Some(tag),
_ => None,
})
.filter_map(|tag| {
if allow_tracing && let TagKind::PsalmTrace = tag.kind {
let variable_name = tag.description.trim();
let variable_atom = mago_atom::atom(variable_name);
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(tag.description_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(tag.description_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;
}
if !matches!(tag.kind, TagKind::Var | TagKind::PsalmVar | TagKind::PhpstanVar) {
return None;
}
let tag_content = tag.description;
let var_tag = parse_var_tag(tag_content, tag.description_span).ok()?;
let variable_name = var_tag.variable.map(|v| mago_atom::Atom::from(&v.name));
let type_string = var_tag.type_string;
match get_type_from_string(
&type_string.value,
type_string.span,
&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, type_string.span))
}
Err(type_error) => {
context.collector.report_with_code(
IssueCode::InvalidDocblock,
Issue::error(format!(
"Invalid type in `@var` tag for variable `{}`.",
variable_name.as_deref().unwrap_or("expression")
))
.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>(
context: &mut Context<'ctx, '_>,
block_context: &BlockContext<'ctx>,
artifacts: &mut AnalysisArtifacts,
value_expression_variable_id: Option<&str>,
mut allow_unnamed: bool,
) -> Option<(TUnion, Span)> {
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_str()) == value_expression_variable_id => true,
_ => false,
})
.map(|(_, variable_type, variable_type_span)| (variable_type, variable_type_span))
}
pub fn insert_variable_from_docblock<'ctx>(
context: &mut Context<'ctx, '_>,
block_context: &mut BlockContext<'ctx>,
variable_name: mago_atom::Atom,
variable_type: TUnion,
variable_type_span: Span,
override_existing: bool,
) {
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 is_impossible = !is_redundant
&& !is_super
&& !is_sub
&& !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(
context: &mut Context<'_, '_>,
value_expression_variable_id: Option<&str>,
value_expression_span: Span,
inferred_type: &TUnion,
docblock_type: &TUnion,
dockblock_type_span: Span,
source_expression: Option<&Expression>,
) {
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
&& !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 {
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 {
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 {
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 {
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);
}
}