use wdl_analysis::Diagnostics;
use wdl_analysis::Example;
use wdl_analysis::LabeledSnippet;
use wdl_analysis::VisitReason;
use wdl_analysis::Visitor;
use wdl_ast::AstNode;
use wdl_ast::AstToken;
use wdl_ast::Diagnostic;
use wdl_ast::Span;
use wdl_ast::SyntaxKind;
use wdl_ast::v1::BoundDecl;
use wdl_ast::v1::Expr;
use wdl_ast::v1::LiteralExpr;
use crate::Rule;
use crate::Tag;
use crate::TagSet;
const ID: &str = "DenyGlobStar";
fn glob_star_diagnostic(span: Span) -> Diagnostic {
Diagnostic::warning("glob pattern \"*\" matches all files")
.with_rule(ID)
.with_highlight(span)
.with_fix("use a more specific pattern to avoid capturing unintended files")
}
#[derive(Clone, Copy, Debug, Default)]
pub struct DenyGlobStar;
impl Rule for DenyGlobStar {
fn id(&self) -> &'static str {
ID
}
fn description(&self) -> &'static str {
"Ensures `glob(\"*\")` is not used in output declarations."
}
fn explanation(&self) -> &'static str {
"`glob(\"*\")` captures all files; as a task grows, you may include unintended files and \
cause unnecessary aggregation. Prefer explicit patterns to opt in only to the files you \
need, keeping tasks easier to debug/reproduce."
}
fn examples(&self) -> &'static [Example] {
&[Example {
negative: LabeledSnippet {
label: None,
snippet: r#"version 1.2
task generate_files {
command <<<
touch foo.txt
touch bar.txt
>>>
output {
Array[File] files = glob("*")
}
}
"#,
},
revised: Some(LabeledSnippet {
label: None,
snippet: r#"version 1.2
task generate_files {
command <<<
touch foo.txt
touch bar.txt
>>>
output {
# Specifically collect the .txt files
Array[File] files = glob("*.txt")
}
}
"#,
}),
}]
}
fn tags(&self) -> TagSet {
TagSet::new(&[Tag::Correctness, Tag::Clarity])
}
fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
Some(&[
SyntaxKind::VersionStatementNode,
SyntaxKind::OutputSectionNode,
SyntaxKind::BoundDeclNode,
])
}
fn related_rules(&self) -> &'static [&'static str] {
&[]
}
}
impl Visitor for DenyGlobStar {
fn reset(&mut self) {
*self = Self;
}
fn bound_decl(&mut self, diagnostics: &mut Diagnostics, reason: VisitReason, decl: &BoundDecl) {
if decl
.inner()
.parent()
.is_none_or(|p| p.kind() != SyntaxKind::OutputSectionNode)
|| reason != VisitReason::Enter
{
return;
}
if let Expr::Call(call) = decl.expr()
&& call.target().text() == "glob"
{
for argument in call.arguments() {
if let Expr::Literal(LiteralExpr::String(s)) = argument
&& s.text().is_some_and(|t| t.text() == "*")
{
diagnostics.exceptable_add(
glob_star_diagnostic(s.span()),
decl.inner(),
&self.exceptable_nodes(),
);
}
}
}
}
}