use wdl_analysis::Diagnostics;
use wdl_analysis::Example;
use wdl_analysis::LabeledSnippet;
use wdl_analysis::VisitReason;
use wdl_analysis::Visitor;
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::Decl;
use wdl_ast::v1::InputSection;
use wdl_ast::v1::UnboundDecl;
use crate::Rule;
use crate::Tag;
use crate::TagSet;
const ID: &str = "InputName";
fn decl_identifier_too_short(span: Span) -> Diagnostic {
Diagnostic::note("declaration identifier must be at least 3 characters")
.with_rule(ID)
.with_highlight(span)
.with_fix("rename the identifier to be at least 3 characters long")
}
fn decl_identifier_starts_with_in(span: Span) -> Diagnostic {
Diagnostic::note("declaration identifier starts with 'in'")
.with_rule(ID)
.with_highlight(span)
.with_fix("rename the identifier to not start with 'in'")
}
fn decl_identifier_starts_with_input(span: Span) -> Diagnostic {
Diagnostic::note("declaration identifier starts with 'input'")
.with_rule(ID)
.with_highlight(span)
.with_fix("rename the identifier to not start with 'input'")
}
#[derive(Default, Debug, Clone, Copy)]
pub struct InputNameRule {
input_section: bool,
}
impl Rule for InputNameRule {
fn id(&self) -> &'static str {
ID
}
fn description(&self) -> &'static str {
"Ensures input names are meaningful (e.g. not generic like 'input', 'in', or too short)."
}
fn explanation(&self) -> &'static str {
"Any input name matching these regular expressions will be flagged: [`/^[iI]n[A-Z_]/`](https://regex101.com/r/V0AFIG/2), \
[`/^input/i`](https://regex101.com/r/Ox8oYb/1) or [`/^..?$/`](https://regex101.com/r/IS1d49/1).\n\n\
It is redundant and needlessly verbose to use an input's name to \
specify that it is an input. Input names should be short yet descriptive. Prefixing a \
name with in or input adds length to the name without adding clarity or context. \
Additionally, names with only 2 characters can lead to confusion and obfuscates the \
content of an input. Input names should be at least 3 characters long."
}
fn examples(&self) -> &'static [Example] {
&[Example {
negative: LabeledSnippet {
label: None,
snippet: r#"version 1.2
task say_hello {
input {
String input_name
}
command <<<
echo "Hello, ~{input_name}!"
>>>
}
"#,
},
revised: Some(LabeledSnippet {
label: None,
snippet: r#"version 1.2
task say_hello {
meta {
description: "Says hello for the given name"
}
input {
String name
}
command <<<
echo "Hello, ~{name}!"
>>>
}
"#,
}),
}]
}
fn tags(&self) -> TagSet {
TagSet::new(&[Tag::Naming, Tag::Style])
}
fn exceptable_nodes(&self) -> Option<&'static [wdl_ast::SyntaxKind]> {
Some(&[
SyntaxKind::VersionStatementNode,
SyntaxKind::InputSectionNode,
SyntaxKind::BoundDeclNode,
SyntaxKind::UnboundDeclNode,
])
}
fn related_rules(&self) -> &'static [&'static str] {
&["OutputName", "DeclarationName"]
}
}
impl Visitor for InputNameRule {
fn reset(&mut self) {
*self = Self::default();
}
fn input_section(&mut self, _: &mut Diagnostics, reason: VisitReason, _: &InputSection) {
self.input_section = reason == VisitReason::Enter;
}
fn bound_decl(&mut self, diagnostics: &mut Diagnostics, reason: VisitReason, decl: &BoundDecl) {
if reason == VisitReason::Enter && self.input_section {
check_decl_name(
diagnostics,
&Decl::Bound(decl.clone()),
&self.exceptable_nodes(),
);
}
}
fn unbound_decl(
&mut self,
diagnostics: &mut Diagnostics,
reason: VisitReason,
decl: &UnboundDecl,
) {
if reason == VisitReason::Enter && self.input_section {
check_decl_name(
diagnostics,
&Decl::Unbound(decl.clone()),
&self.exceptable_nodes(),
);
}
}
}
fn check_decl_name(
diagnostics: &mut Diagnostics,
decl: &Decl,
exceptable_nodes: &Option<&'static [SyntaxKind]>,
) {
let name = decl.name();
let name = name.text();
let length = name.len();
if length < 3 {
diagnostics.exceptable_add(
decl_identifier_too_short(decl.name().span()),
decl.inner(),
exceptable_nodes,
);
}
let mut name = name.chars().peekable();
if let Some(c) = name.next()
&& (c == 'i' || c == 'I')
&& let Some('n') = name.peek()
{
name.next();
if let Some(c) = name.peek() {
if c.is_ascii_uppercase() || c == &'_' {
diagnostics.exceptable_add(
decl_identifier_starts_with_in(decl.name().span()),
decl.inner(),
exceptable_nodes,
);
} else {
let s: String = name.take(3).collect();
if s == "put" {
diagnostics.exceptable_add(
decl_identifier_starts_with_input(decl.name().span()),
decl.inner(),
exceptable_nodes,
);
}
}
}
}
}