use crate::ast::{Arg, AstNode, AstToken, Expr, Parameters, QuoteSym, TypeAnnotation};
use crate::linter::diagnostic::{Diagnostic, Severity};
use crate::linter::rules::matchers;
use crate::linter::rules::{Example, Rule, RuleContext};
use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxNode};
pub struct KwargDefaultMismatch;
const CONCRETE_CORE_TYPES: &[&str] = &[
"Int", "Int8", "Int16", "Int32", "Int64", "Int128", "UInt", "UInt8", "UInt16", "UInt32",
"UInt64", "UInt128", "Float16", "Float32", "Float64", "Bool", "Char", "String", "Symbol",
];
#[derive(Clone, Copy, PartialEq, Eq)]
enum LiteralType {
Int,
Float64,
Float32,
Bool,
Char,
String,
Symbol,
}
impl LiteralType {
fn of(expr: &Expr) -> Option<Self> {
match expr {
Expr::Literal(literal) => Self::of_literal(literal.syntax()),
Expr::StringLiteral(string) => {
let plain = string.prefix().is_none()
&& string.suffix().is_none()
&& string.interpolations().next().is_none();
plain.then_some(Self::String)
}
Expr::Other(node) if node.kind() == SyntaxKind::QUOTE_SYM => {
let quoted = QuoteSym::cast(node.clone())?.expr()?;
matches!(quoted, Expr::Name(_)).then_some(Self::Symbol)
}
_ => None,
}
}
fn of_literal(node: &SyntaxNode) -> Option<Self> {
let mut tokens = node
.children_with_tokens()
.filter_map(|el| el.into_token())
.filter(|token| !matches!(token.kind(), SyntaxKind::WHITESPACE | SyntaxKind::COMMENT));
let first = tokens.next()?;
let value = match first.kind() {
SyntaxKind::MINUS | SyntaxKind::PLUS => tokens.next()?,
_ => first.clone(),
};
if tokens.next().is_some() {
return None;
}
match value.kind() {
SyntaxKind::INTEGER => {
fits_in_int64(value.text(), first.kind() == SyntaxKind::MINUS).then_some(Self::Int)
}
SyntaxKind::FLOAT => Some(Self::Float64),
SyntaxKind::FLOAT32 => Some(Self::Float32),
SyntaxKind::TRUE_KW | SyntaxKind::FALSE_KW => Some(Self::Bool),
SyntaxKind::CHAR => Some(Self::Char),
_ => None,
}
}
fn name(self) -> &'static str {
match self {
Self::Int => "Int",
Self::Float64 => "Float64",
Self::Float32 => "Float32",
Self::Bool => "Bool",
Self::Char => "Char",
Self::String => "String",
Self::Symbol => "Symbol",
}
}
fn satisfies(self, annotation: &str) -> bool {
match self {
Self::Int => matches!(annotation, "Int" | "Int64" | "Int32"),
other => annotation == other.name(),
}
}
}
fn fits_in_int64(text: &str, negated: bool) -> bool {
let digits: String = text.chars().filter(|c| *c != '_').collect();
let Ok(magnitude) = digits.parse::<u128>() else {
return false;
};
let limit = if negated {
1u128 << 63
} else {
(1u128 << 63) - 1
};
magnitude <= limit
}
impl Rule for KwargDefaultMismatch {
fn id(&self) -> &'static str {
"kwarg-default-mismatch"
}
fn default_severity(&self) -> Severity {
Severity::Error
}
fn description(&self) -> &'static str {
"Flag a keyword parameter whose literal default cannot be an instance \
of its declared type, as in `g(; y::Int = 1.0)`. A keyword's `::T` is \
not an implicit `convert`: Julia lowers it into a dispatch constraint \
on the inner method the default is passed to, so `g()` raises a \
`MethodError` every time. The check is exact, like dispatch itself — \
`y::Float64 = 1` and `y::Int8 = 1` are mismatches too — and fires only \
when both sides are certain: a bare, concrete `Core` type that \
resolves to Base, and a default whose own spelling pins its type down. \
Abstract and parametric annotations (`Real`, `Vector{Int}`), computed \
defaults, and the literals whose type follows their digit count \
(`0x01`) are all left alone."
}
fn examples(&self) -> &'static [Example] {
&[
Example {
caption: "`y` is declared `Int`, so the `Float64` default never matches it:",
source: "function scale(xs; y::Int = 1.0)\n xs .* y\nend\n",
},
Example {
caption: "Julia does not promote here either — the default has to be \
an `Int8` already:",
source: "counter(; start::Int8 = 0) = start\n",
},
]
}
fn interests(&self) -> &'static [SyntaxKind] {
&[SyntaxKind::PARAMETERS]
}
fn check(&self, el: &SyntaxElement, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let Some(node) = el.as_node() else {
return;
};
let Some(params) = Parameters::cast(node.clone()) else {
return;
};
if !declares_keyword_parameters(node) {
return;
}
for arg in params.args() {
check_parameter(self.id(), &arg, ctx, sink);
}
}
}
fn declares_keyword_parameters(params: &SyntaxNode) -> bool {
params
.parent()
.filter(|parent| parent.kind() == SyntaxKind::ARG_LIST)
.and_then(|arg_list| arg_list.parent())
.filter(|call| call.kind() == SyntaxKind::CALL_EXPR)
.is_some_and(|call| matchers::in_signature_position(&call))
}
fn check_parameter(id: &'static str, arg: &Arg, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let Some(Expr::AssignmentExpr(assignment)) = arg.expr() else {
return;
};
if assignment
.op()
.is_none_or(|op| op.syntax().kind() != SyntaxKind::EQ)
{
return;
}
let Some(Expr::TypeAnnotation(annotation)) = assignment.lhs() else {
return;
};
let Some(name) = annotation
.pattern()
.and_then(|pattern| pattern.name_ident())
else {
return;
};
let Some(declared) = concrete_core_type(&annotation, ctx) else {
return;
};
let Some(default) = assignment.rhs() else {
return;
};
let Some(actual) = LiteralType::of(&default) else {
return;
};
if actual.satisfies(declared) {
return;
}
let literal = default.syntax().text();
let mut diagnostic = Diagnostic::new(
id,
assignment.syntax().text_range(),
format!(
"keyword argument `{}` is declared `::{declared}`, but its default `{literal}` has \
type `{}`",
name.text(),
actual.name(),
),
);
diagnostic.message = diagnostic.message.with_suggestion(
"a keyword's `::T` is a dispatch constraint, not a `convert`, so the default raises a \
`MethodError`",
);
sink.push(diagnostic);
}
fn concrete_core_type(annotation: &TypeAnnotation, ctx: &RuleContext<'_>) -> Option<&'static str> {
let Some(Expr::Name(ty)) = annotation.ty() else {
return None;
};
let ident = ty.ident()?;
let known = CONCRETE_CORE_TYPES
.iter()
.find(|name| **name == ident.text())?;
ctx.read_resolves_to_base(ident.syntax()).then_some(*known)
}