use std::collections::HashSet;
use std::fmt;
use convert_case::Boundary;
use convert_case::Case;
use convert_case::Converter;
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::SyntaxNode;
use wdl_ast::v1::BoundDecl;
use wdl_ast::v1::InputSection;
use wdl_ast::v1::OutputSection;
use wdl_ast::v1::StructDefinition;
use wdl_ast::v1::TaskDefinition;
use wdl_ast::v1::UnboundDecl;
use wdl_ast::v1::WorkflowDefinition;
use crate::Config;
use crate::Rule;
use crate::Tag;
use crate::TagSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Context {
Task,
Workflow,
Struct,
Input,
Output,
PrivateDecl,
}
impl fmt::Display for Context {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Task => write!(f, "task"),
Self::Workflow => write!(f, "workflow"),
Self::Struct => write!(f, "struct member"),
Self::Input => write!(f, "input"),
Self::Output => write!(f, "output"),
Self::PrivateDecl => write!(f, "private declaration"),
}
}
}
const ID: &str = "SnakeCase";
fn snake_case(context: Context, name: &str, properly_cased_name: &str, span: Span) -> Diagnostic {
Diagnostic::warning(format!("{context} name `{name}` is not snake_case"))
.with_rule(ID)
.with_label("this name must be snake_case", span)
.with_fix(format!("replace `{name}` with `{properly_cased_name}`"))
}
fn check_name(
allowed_names: &HashSet<String>,
context: Context,
name: &str,
span: Span,
diagnostics: &mut Diagnostics,
node: &SyntaxNode,
exceptable_nodes: &Option<&'static [SyntaxKind]>,
) {
if allowed_names.contains(name) {
return;
}
let converter = Converter::new()
.remove_boundaries(&[Boundary::DigitLower, Boundary::LowerDigit])
.to_case(Case::Snake);
let properly_cased_name = converter.convert(name);
if name != properly_cased_name {
let warning = snake_case(context, name, &properly_cased_name, span);
diagnostics.exceptable_add(warning, node, exceptable_nodes);
}
}
#[derive(Debug, Clone)]
pub struct SnakeCaseRule {
within_struct: bool,
within_input: bool,
within_output: bool,
allowed_names: HashSet<String>,
}
impl SnakeCaseRule {
pub fn new(config: &Config) -> Self {
Self {
within_struct: false,
within_input: false,
within_output: false,
allowed_names: HashSet::from_iter(config.allowed_names.iter().cloned()),
}
}
}
impl SnakeCaseRule {
fn determine_decl_context(&self) -> Context {
if self.within_struct {
Context::Struct
} else if self.within_input {
Context::Input
} else if self.within_output {
Context::Output
} else {
Context::PrivateDecl
}
}
}
impl Rule for SnakeCaseRule {
fn id(&self) -> &'static str {
ID
}
fn description(&self) -> &'static str {
"Ensures that tasks, workflows, and variables are defined with snake_case names."
}
fn explanation(&self) -> &'static str {
"Workflow, task, and variable names should be in snake case. Maintaining a consistent \
naming convention makes the code easier to read and understand."
}
fn examples(&self) -> &'static [Example] {
&[Example {
negative: LabeledSnippet {
label: None,
snippet: r#"version 1.2
task SayHello {
command <<<
echo "Hello, World!"
>>>
}
"#,
},
revised: Some(LabeledSnippet {
label: None,
snippet: r#"version 1.2
task say_hello {
command <<<
echo "Hello, World!"
>>>
}
"#,
}),
}]
}
fn tags(&self) -> TagSet {
TagSet::new(&[Tag::Naming, Tag::Style, Tag::Clarity])
}
fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
Some(&[
SyntaxKind::VersionStatementNode,
SyntaxKind::StructDefinitionNode,
SyntaxKind::TaskDefinitionNode,
SyntaxKind::WorkflowDefinitionNode,
SyntaxKind::InputSectionNode,
SyntaxKind::OutputSectionNode,
SyntaxKind::BoundDeclNode,
SyntaxKind::UnboundDeclNode,
])
}
fn related_rules(&self) -> &'static [&'static str] {
&["PascalCase"]
}
}
impl Visitor for SnakeCaseRule {
fn reset(&mut self) {
*self = Self {
allowed_names: std::mem::take(&mut self.allowed_names),
within_struct: false,
within_input: false,
within_output: false,
};
}
fn struct_definition(
&mut self,
_diagnostics: &mut Diagnostics,
reason: VisitReason,
_def: &StructDefinition,
) {
match reason {
VisitReason::Enter => {
self.within_struct = true;
}
VisitReason::Exit => {
self.within_struct = false;
}
}
}
fn input_section(
&mut self,
_diagnostics: &mut Diagnostics,
reason: VisitReason,
_section: &InputSection,
) {
match reason {
VisitReason::Enter => {
self.within_input = true;
}
VisitReason::Exit => {
self.within_input = false;
}
}
}
fn output_section(
&mut self,
_diagnostics: &mut Diagnostics,
reason: VisitReason,
_section: &OutputSection,
) {
match reason {
VisitReason::Enter => {
self.within_output = true;
}
VisitReason::Exit => {
self.within_output = false;
}
}
}
fn task_definition(
&mut self,
diagnostics: &mut Diagnostics,
reason: VisitReason,
task: &TaskDefinition,
) {
if reason == VisitReason::Exit {
return;
}
let name = task.name();
check_name(
&self.allowed_names,
Context::Task,
name.text(),
name.span(),
diagnostics,
task.inner(),
&self.exceptable_nodes(),
);
}
fn workflow_definition(
&mut self,
diagnostics: &mut Diagnostics,
reason: VisitReason,
workflow: &WorkflowDefinition,
) {
if reason == VisitReason::Exit {
return;
}
let name = workflow.name();
check_name(
&self.allowed_names,
Context::Workflow,
name.text(),
name.span(),
diagnostics,
workflow.inner(),
&self.exceptable_nodes(),
);
}
fn bound_decl(&mut self, diagnostics: &mut Diagnostics, reason: VisitReason, decl: &BoundDecl) {
if reason == VisitReason::Exit {
return;
}
let name = decl.name();
let context = self.determine_decl_context();
check_name(
&self.allowed_names,
context,
name.text(),
name.span(),
diagnostics,
decl.inner(),
&self.exceptable_nodes(),
);
}
fn unbound_decl(
&mut self,
diagnostics: &mut Diagnostics,
reason: VisitReason,
decl: &UnboundDecl,
) {
if reason == VisitReason::Exit {
return;
}
let name = decl.name();
let context = self.determine_decl_context();
check_name(
&self.allowed_names,
context,
name.text(),
name.span(),
diagnostics,
decl.inner(),
&self.exceptable_nodes(),
);
}
}