Skip to main content

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}