perl_lsp_diagnostics/lints/package_subroutine.rs
1//! Package and subroutine diagnostic lint checks
2//!
3//! This module implements diagnostic checks for package and subroutine declarations.
4//!
5//! # Diagnostic codes
6//!
7//! | Code | Severity | Description | Status |
8//! |-------|----------|--------------------------------------------------|-------------|
9//! | PL200 | Warning | Missing package declaration in file | Implemented |
10//! | PL201 | Warning | Package name declared more than once in file | Implemented |
11//! | PL300 | Warning | Subroutine name defined more than once in file | Implemented |
12//! | PL301 | Warning | Subroutine has no explicit return statement | Deferred |
13//! | PL402 | Warning | Return value of expression used implicitly | Deferred |
14
15// PL301 (MissingReturn): Deferred. Correct implementation requires full
16// control-flow analysis of every branch in a subroutine body. In idiomatic
17// Perl, implicit return of the last expression is correct style; emitting
18// on every sub without an explicit `return` would be extremely noisy.
19// Revisit when a control-flow graph is available in the AST.
20
21// PL402 (ImplicitReturn): Deferred. In Perl, every expression is an
22// implicit return value. This lint would fire on virtually every subroutine
23// body. The code is reserved for future use with a narrower trigger condition.
24
25use std::collections::HashMap;
26use std::path::Path;
27
28use perl_diagnostics_codes::DiagnosticCode;
29use perl_lsp_diagnostic_types::{Diagnostic, DiagnosticSeverity};
30use perl_parser_core::ast::{Node, NodeKind};
31
32use super::super::walker::walk_node;
33
34/// Check for missing package declaration (PL200).
35///
36/// Walks the top-level statements of the `Program` node only (not recursive).
37/// If no `Package` node is found at the top level, emits a warning at position `(0, 0)`.
38///
39/// Only the `Program` node's direct children are examined. Package declarations
40/// inside `eval` blocks or other nested constructs are not counted — they do not
41/// establish the file's package namespace in the same way.
42pub fn check_missing_package_declaration(
43 node: &Node,
44 source: &str,
45 source_path: Option<&Path>,
46 diagnostics: &mut Vec<Diagnostic>,
47) {
48 let statements = match &node.kind {
49 NodeKind::Program { statements } => statements,
50 _ => return,
51 };
52
53 if should_skip_missing_package_declaration(source, source_path) {
54 return;
55 }
56
57 let has_package = statements.iter().any(|stmt| matches!(&stmt.kind, NodeKind::Package { .. }));
58
59 if !has_package {
60 diagnostics.push(Diagnostic {
61 range: (0, 0),
62 severity: DiagnosticSeverity::Warning,
63 code: Some(DiagnosticCode::MissingPackageDeclaration.as_str().to_string()),
64 message: "This file has no package declaration. \
65 Add 'package MyModule;' to declare the package namespace."
66 .to_string(),
67 related_information: Vec::new(),
68 tags: Vec::new(),
69 suggestion: Some("Add 'package MyModule;' at the top of the file".to_string()),
70 });
71 }
72}
73
74fn should_skip_missing_package_declaration(source: &str, source_path: Option<&Path>) -> bool {
75 if let Some(extension) =
76 source_path.and_then(|path| path.extension()).and_then(|ext| ext.to_str())
77 {
78 let extension = extension.to_ascii_lowercase();
79 if matches!(extension.as_str(), "pl" | "t" | "cgi" | "psgi" | "plx") {
80 return true;
81 }
82 }
83
84 source.trim_start().starts_with("#!")
85}
86
87/// Check for duplicate package declarations (PL201).
88///
89/// Walks the entire AST. For each package name seen more than once,
90/// emits a warning on the second and every subsequent occurrence.
91/// The first declaration is always clean.
92pub fn check_duplicate_package(node: &Node, diagnostics: &mut Vec<Diagnostic>) {
93 let mut seen: HashMap<String, usize> = HashMap::new();
94
95 walk_node(node, &mut |n| {
96 if let NodeKind::Package { name, name_span, .. } = &n.kind {
97 let count = seen.entry(name.clone()).or_insert(0);
98 *count += 1;
99 if *count > 1 {
100 diagnostics.push(Diagnostic {
101 range: (name_span.start, name_span.end),
102 severity: DiagnosticSeverity::Warning,
103 code: Some(DiagnosticCode::DuplicatePackage.as_str().to_string()),
104 message: format!("Package '{}' is declared more than once in this file", name),
105 related_information: Vec::new(),
106 tags: Vec::new(),
107 suggestion: Some(format!(
108 "Remove the duplicate 'package {};' declaration",
109 name
110 )),
111 });
112 }
113 }
114 });
115}
116
117/// Check for duplicate named subroutine definitions (PL300).
118///
119/// Walks the entire AST. For each fully-qualified subroutine name seen more than once,
120/// emits a warning on the second and every subsequent occurrence.
121/// Anonymous subroutines (`name: None`) are excluded.
122/// `Method` nodes are excluded — class method redefinition semantics differ.
123///
124/// Subroutine names are qualified by the current package context so that
125/// `package Foo; sub new { }` and `package Bar; sub new { }` are treated as
126/// distinct subroutines (`Foo::new` vs `Bar::new`) and do not trigger PL300.
127pub fn check_duplicate_subroutine(node: &Node, diagnostics: &mut Vec<Diagnostic>) {
128 // Collect all (qualified_name, span) pairs in source order, tracking
129 // the current package as we encounter Package nodes.
130 let mut subs: Vec<(String, (usize, usize))> = Vec::new();
131 let mut current_package = String::from("main");
132
133 walk_node(node, &mut |n| {
134 match &n.kind {
135 NodeKind::Package { name, .. } => {
136 current_package = name.clone();
137 }
138 NodeKind::Subroutine { name: Some(name), name_span: Some(span), .. } => {
139 // Build a fully-qualified key so that Foo::new and Bar::new are distinct.
140 // If the name already contains "::" it is explicitly qualified by the author.
141 let qualified = if name.contains("::") {
142 name.clone()
143 } else {
144 format!("{}::{}", current_package, name)
145 };
146 subs.push((qualified, (span.start, span.end)));
147 }
148 _ => {}
149 }
150 });
151
152 // Second pass: find duplicates in the collected list.
153 let mut seen: HashMap<String, usize> = HashMap::new();
154 for (qualified, span) in subs {
155 let count = seen.entry(qualified.clone()).or_insert(0);
156 *count += 1;
157 if *count > 1 {
158 // Display only the bare name in the message (after the last "::").
159 let display_name = qualified.rsplit("::").next().unwrap_or(&qualified).to_string();
160 diagnostics.push(Diagnostic {
161 range: span,
162 severity: DiagnosticSeverity::Warning,
163 code: Some(DiagnosticCode::DuplicateSubroutine.as_str().to_string()),
164 message: format!("Subroutine '{}' is defined more than once", display_name),
165 related_information: Vec::new(),
166 tags: Vec::new(),
167 suggestion: Some(format!(
168 "Remove or rename the duplicate 'sub {}' definition",
169 display_name
170 )),
171 });
172 }
173 }
174}