Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 62 additions & 55 deletions crates/emmylua_code_analysis/src/semantic_model/flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ use std::collections::HashSet;
use std::rc::Rc;

use emmylua_parser::{
BinaryOperator, LuaAssignStat, LuaAstNode, LuaCallExprStat, LuaDocTagAs, LuaExpr,
LuaLiteralToken, UnaryOperator,
BinaryOperator, LuaAssignStat, LuaAst, LuaAstNode, LuaCallExprStat, LuaComment, LuaDocTagAs,
LuaExpr, LuaLiteralToken, UnaryOperator,
};
use rowan::TextSize;

Expand Down Expand Up @@ -440,7 +440,11 @@ pub fn type_of_expr_at(
match &expr {
LuaExpr::NameExpr(name_expr) => {
if let Some(decl) = model.resolve_name(name_expr.get_position()) {
return model.type_of_decl_at(&decl, offset);
return apply_inline_as_casts(
model,
expr_syntax,
model.type_of_decl_at(&decl, offset),
);
}
model.type_of_expr(expr_syntax)
}
Expand Down Expand Up @@ -619,7 +623,7 @@ pub fn type_of_expr_at(
let result = binary_type(model, op_token.get_op(), &left_ty, &right_ty);
apply_expr_casts(model, expr_syntax, result)
}
_ => model.type_of_expr(expr_syntax),
_ => apply_inline_as_casts(model, expr_syntax, model.type_of_expr(expr_syntax)),
}
}

Expand Down Expand Up @@ -1020,10 +1024,33 @@ fn apply_expr_casts(
model: &SemanticModel,
expr_syntax: emmylua_parser::LuaSyntaxId,
base: LuaType,
) -> LuaType {
apply_bound_casts(model, expr_syntax, base, true)
}

/// Apply only expression-local @as assertions, without importing statement casts
/// into ordinary declaration inference or name-expression queries.
pub(super) fn apply_inline_as_casts(
model: &SemanticModel,
expr_syntax: emmylua_parser::LuaSyntaxId,
base: LuaType,
) -> LuaType {
apply_bound_casts(model, expr_syntax, base, false)
}

fn apply_bound_casts(
model: &SemanticModel,
expr_syntax: emmylua_parser::LuaSyntaxId,
base: LuaType,
include_tag_casts: bool,
) -> LuaType {
let Some(tree) = model.flow_tree() else {
return base;
};
// FlowTree records both TagCast and AsCast nodes in this flag.
if !tree.has_tag_cast() {
return base;
}
// Inline casts may be bound to the expression itself or an ancestor node (e.g. an assignment statement),
// so check both the expression and all ancestors' flow bindings.
let mut flow_ids = Vec::new();
Expand All @@ -1046,7 +1073,7 @@ fn apply_expr_casts(
for flow_id in flow_ids {
for effect in tree.get_flow_effects(flow_id) {
match effect {
FlowEffect::TagCast(cast_ptr) => {
FlowEffect::TagCast(cast_ptr) if include_tag_casts => {
let Some(chunk) = model.chunk() else {
continue;
};
Expand Down Expand Up @@ -1084,7 +1111,10 @@ fn apply_expr_casts(
let Some(as_tag) = as_ptr.to_node(&chunk) else {
continue;
};
if let Some(doc_type) = as_tag.get_type() {
if inline_as_target(&as_tag)
.is_some_and(|target| target.get_syntax_id() == expr_syntax)
&& let Some(doc_type) = as_tag.get_type()
{
ty = model.doc_type_lua(doc_type.get_syntax_id());
}
}
Expand All @@ -1095,6 +1125,23 @@ fn apply_expr_casts(
ty
}

/// An inline assertion applies to its attached expression. Local/assignment/
/// return statements target their last value; call statements target their call.
/// A table-field owner targets its value. Other non-expression owners are ignored.
fn inline_as_target(as_tag: &LuaDocTagAs) -> Option<LuaExpr> {
let owner = as_tag.get_parent::<LuaComment>()?.get_owner()?;
match owner {
LuaAst::LuaLocalStat(stat) => stat.get_value_exprs().last(),
LuaAst::LuaAssignStat(stat) => stat.get_var_and_expr_list().1.last().cloned(),
LuaAst::LuaReturnStat(stat) => stat.get_expr_list().last(),
LuaAst::LuaCallExprStat(stat) => stat.get_call_expr().map(LuaExpr::CallExpr),
LuaAst::LuaTableField(field) => field.get_value_expr(),
// Expression owners are exact targets; blocks/control-flow statements
// have no asserted value and intentionally produce None.
owner => LuaExpr::cast(owner.syntax().clone()),
}
}

/// `---@cast` path operation: widen / remove / replace.
#[derive(Clone)]
enum CastOp {
Expand Down Expand Up @@ -1465,11 +1512,9 @@ fn trace_decl_uncached(
collect_decl_cast(model, decl, cast_ptr, path);
}
}
FlowNodeKind::AsCast(as_ptr) => {
if options.casts {
collect_decl_as_cast(model, decl, as_ptr, path);
}
}
// An @as assertion belongs to its expression, not every declaration
// whose flow path passes this comment. Assignment RHS queries apply it.
FlowNodeKind::AsCast(_) => {}
// Backtracked to the start (e.g. function parameters with no DeclPosition node): use the declaration type as the base and apply path guards/casts.
FlowNodeKind::Start | FlowNodeKind::Unreachable => {
if matches!(node.kind, FlowNodeKind::Unreachable) && mode == TraceMode::MergeBranch
Expand Down Expand Up @@ -1591,7 +1636,11 @@ fn trace_member(
narrowings: Vec::new(),
casts: path.casts.clone(),
};
let mut value_ty = model.type_of_expr(*value_syntax);
let mut value_ty = apply_inline_as_casts(
model,
*value_syntax,
model.type_of_expr(*value_syntax),
);
if let Some(annotation) = member_annotation_type(model, member) {
if model.type_check(&annotation, &value_ty) {
value_ty = widen_const_type(value_ty);
Expand All @@ -1606,9 +1655,7 @@ fn trace_member(
FlowNodeKind::TagCast(cast_ptr) => {
collect_member_cast(model, member, cast_ptr, path);
}
FlowNodeKind::AsCast(as_ptr) => {
collect_member_as_cast(model, member, as_ptr, path);
}
FlowNodeKind::AsCast(_) => {}
FlowNodeKind::TrueCondition(cond_ptr) => {
if let Some(narrowing) = narrow_member_condition(model, member, cond_ptr, true) {
path.narrowings.push(narrowing);
Expand Down Expand Up @@ -1876,46 +1923,6 @@ fn collect_member_cast(
}
}

/// Inline assertion `--[[@as T]]`: when matching the current decl's path, replace the path type directly with T.
fn collect_decl_as_cast(
model: &SemanticModel,
_decl: &SemanticId,
as_ptr: &emmylua_parser::LuaAstPtr<LuaDocTagAs>,
path: &mut PathState,
) {
let Some(ty) = as_cast_type(model, as_ptr) else {
return;
};
if !matches!(ty, LuaType::Unknown) {
path.casts.push(CastOp::Replace(ty));
}
}

/// Inline assertion `--[[@as T]]`: when matching the current member's path, replace the path type directly with T.
fn collect_member_as_cast(
model: &SemanticModel,
_member: &SemanticId,
as_ptr: &emmylua_parser::LuaAstPtr<LuaDocTagAs>,
path: &mut PathState,
) {
let Some(ty) = as_cast_type(model, as_ptr) else {
return;
};
if !matches!(ty, LuaType::Unknown) {
path.casts.push(CastOp::Replace(ty));
}
}

fn as_cast_type(
model: &SemanticModel,
as_ptr: &emmylua_parser::LuaAstPtr<LuaDocTagAs>,
) -> Option<LuaType> {
let chunk = model.chunk()?;
let as_tag = as_ptr.to_node(&chunk)?;
let doc_type = as_tag.get_type()?;
Some(model.doc_type_lua(doc_type.get_syntax_id()))
}

/// RHS type of an assignment: if decl is a multi-return slot on this assignment, take the matching return type by `return_index`;
/// otherwise fall back to ordinary expression inference.
fn assigned_value_type(
Expand Down
Loading
Loading