use perl_diagnostics_codes::DiagnosticCode;
use perl_parser_core::ast::{Node, NodeKind};
use super::super::walker::walk_node;
use perl_lsp_diagnostic_types::{Diagnostic, DiagnosticSeverity};
const FEATURE_VERSIONS: &[(&str, u32, u32)] = &[
("say", 5, 10),
("state", 5, 10),
("postfix_deref", 5, 20),
("signatures", 5, 36),
("try", 5, 34),
("class", 5, 38),
("field", 5, 38),
];
fn parse_perl_version(module: &str) -> Option<(u32, u32)> {
let s = module.strip_prefix('v').unwrap_or(module);
let parts: Vec<&str> = s.splitn(3, '.').collect();
let major: u32 = parts.first()?.parse().ok()?;
let minor: u32 = parts.get(1)?.parse().ok()?;
Some((major, minor))
}
fn features_enabled_by_version(major: u32, minor: u32) -> Vec<&'static str> {
let mut features = Vec::new();
if (major, minor) >= (5, 10) {
features.extend_from_slice(&["say", "state"]);
}
if (major, minor) >= (5, 20) {
features.push("postfix_deref");
}
if (major, minor) >= (5, 34) {
features.push("try");
}
if (major, minor) >= (5, 36) {
features.push("signatures");
}
if (major, minor) >= (5, 38) {
features.extend_from_slice(&["class", "field", "method"]);
}
features
}
pub fn check_version_compat(node: &Node, diagnostics: &mut Vec<Diagnostic>) {
let statements = match &node.kind {
NodeKind::Program { statements } => statements,
_ => return,
};
let mut declared_version: Option<(u32, u32)> = None;
let mut explicit_features: Vec<String> = Vec::new();
for stmt in statements {
if let NodeKind::Use { module, args, .. } = &stmt.kind {
if (module.starts_with('v')
|| module.chars().next().is_some_and(|c| c.is_ascii_digit()))
&& let Some(version) = parse_perl_version(module)
{
match declared_version {
None => declared_version = Some(version),
Some(existing) if version > existing => declared_version = Some(version),
_ => {}
}
}
if module == "feature" {
for arg in args {
let name = arg.trim_matches(|c| c == '\'' || c == '"');
explicit_features.push(name.to_string());
}
}
}
}
let (major, minor) = match declared_version {
Some(v) => v,
None => return,
};
let mut effective_features = features_enabled_by_version(major, minor);
for feat in &explicit_features {
if !effective_features.contains(&feat.as_str()) {
for (known, _, _) in FEATURE_VERSIONS {
if *known == feat.as_str() && !effective_features.contains(known) {
effective_features.push(known);
}
}
}
}
walk_node(node, &mut |n| {
match &n.kind {
NodeKind::Class { .. } => {
if !effective_features.contains(&"class") {
let min = feature_min_version("class");
diagnostics.push(make_diagnostic(n, "class", major, minor, min));
}
}
NodeKind::Try { .. } => {
if !effective_features.contains(&"try") {
let min = feature_min_version("try");
diagnostics.push(make_diagnostic(n, "try/catch", major, minor, min));
}
}
NodeKind::FunctionCall { name, .. } if name == "say" => {
if !effective_features.contains(&"say") {
let min = feature_min_version("say");
diagnostics.push(make_diagnostic(n, "say", major, minor, min));
}
}
NodeKind::VariableDeclaration { declarator, .. } if declarator == "state" => {
if !effective_features.contains(&"state") {
let min = feature_min_version("state");
diagnostics.push(make_diagnostic(n, "state", major, minor, min));
}
}
NodeKind::Unary { op, .. }
if op == "->@*" || op == "->%*" || op == "->$*" || op == "->@[" || op == "->@{" =>
{
if !effective_features.contains(&"postfix_deref") {
let min = feature_min_version("postfix_deref");
diagnostics.push(make_diagnostic(n, "postfix deref", major, minor, min));
}
}
NodeKind::Subroutine { signature: Some(_), .. } => {
if !effective_features.contains(&"signatures") {
let min = feature_min_version("signatures");
diagnostics.push(make_diagnostic(
n,
"subroutine signatures",
major,
minor,
min,
));
}
}
_ => {}
}
});
}
fn feature_min_version(feature: &str) -> (u32, u32) {
FEATURE_VERSIONS
.iter()
.find(|(name, _, _)| *name == feature)
.map(|(_, maj, min)| (*maj, *min))
.unwrap_or((5, 0))
}
fn make_diagnostic(
node: &Node,
display: &str,
declared_major: u32,
declared_minor: u32,
min_version: (u32, u32),
) -> Diagnostic {
let message = format!(
"'{}' requires Perl v{}.{}+; declared version is v{}.{}",
display, min_version.0, min_version.1, declared_major, declared_minor,
);
Diagnostic {
range: (node.location.start, node.location.end),
severity: DiagnosticSeverity::Warning,
code: Some(DiagnosticCode::VersionIncompatFeature.as_str().to_string()),
message,
related_information: vec![],
tags: vec![],
suggestion: Some(format!(
"Update 'use v{}.{}' to 'use v{}.{}' or add 'use feature \"{}\";'",
declared_major, declared_minor, min_version.0, min_version.1, display,
)),
}
}